{"name":"Google Shopping ads workflow","nodes":[{"id":"3f1a0c10-1001-4a01-8b01-0000000000a1","name":"Daily Feed Audit Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[-240,300],"parameters":{"rule":{"interval":[{"field":"days","daysInterval":1}]}}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000a2","name":"Fetch Merchant Center Product Statuses","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[40,140],"parameters":{"url":"https://shoppingcontent.googleapis.com/content/v2.1/MERCHANT_ID/productstatuses","sendQuery":true,"queryParameters":{"parameters":[{"name":"maxResults","value":"250"},{"name":"destinations","value":"Shopping"}]},"options":{}}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000a3","name":"Fetch Shopping Performance (GAQL)","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[40,460],"parameters":{"method":"POST","url":"https://googleads.googleapis.com/v18/customers/CUSTOMER_ID/googleAds:searchStream","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ query: \"SELECT segments.product_item_id, segments.product_title, metrics.cost_micros, metrics.impressions, metrics.clicks, metrics.conversions, metrics.conversions_value FROM shopping_performance_view WHERE segments.date DURING LAST_30_DAYS\" }) }}","options":{}}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000a4","name":"Normalize Feed Statuses","type":"n8n-nodes-base.code","typeVersion":2,"position":[320,140],"parameters":{"jsCode":"// Merchant Center productstatuses -> one clean row per offer_id.\n// Critical attributes block serving; recommended ones only demote impressions.\nconst CRITICAL = ['gtin', 'brand', 'image_link', 'availability', 'price'];\nconst RECOMMENDED = ['product_type', 'google_product_category', 'description', 'shipping', 'color', 'size'];\n\nconst byOffer = new Map();\n\nfor (const item of $input.all()) {\n  const p = item.json.resource || item.json;\n  // productId looks like \"online:en:US:SKU-1234\" -> keep the SKU\n  const offerId = String(p.productId || p.offerId || '').split(':').pop();\n  if (!offerId) continue;\n\n  const issues = Array.isArray(p.itemLevelIssues) ? p.itemLevelIssues : [];\n  const disapprovals = issues.filter((i) => i.servability === 'disapproved');\n  const demotions = issues.filter((i) => i.servability === 'demoted');\n\n  // Missing attributes = empty in the payload OR called out by Google itself.\n  const flagged = new Set();\n  for (const a of CRITICAL.concat(RECOMMENDED)) {\n    const v = p[a];\n    if (v === undefined || v === null || v === '' || v === 'null') flagged.add(a);\n  }\n  for (const i of issues) {\n    if (i.attributeName) flagged.add(i.attributeName);\n  }\n\n  const missing = [...flagged];\n  const missingCritical = missing.filter((a) => CRITICAL.includes(a));\n\n  const row = {\n    source: 'feed',\n    offer_id: offerId,\n    title: p.title || '',\n    brand: p.brand || '',\n    price: Number(p.price && p.price.value ? p.price.value : 0),\n    availability: p.availability || 'unknown',\n    disapproved: disapprovals.length > 0,\n    demoted: demotions.length > 0,\n    disapproval_codes: disapprovals.map((i) => i.code),\n    top_resolution: (disapprovals[0] || demotions[0] || {}).resolution || 'none',\n    missing_attributes: missing,\n    missing_critical: missingCritical,\n    issue_count: issues.length,\n  };\n\n  // Dedupe: the same SKU can appear per-country. Keep the worst status.\n  const prev = byOffer.get(offerId);\n  if (!prev || (row.disapproved && !prev.disapproved) || row.issue_count > prev.issue_count) {\n    byOffer.set(offerId, row);\n  }\n}\n\nreturn [...byOffer.values()].map((r) => ({ json: r }));"}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000a5","name":"Normalize Product ROAS","type":"n8n-nodes-base.code","typeVersion":2,"position":[320,460],"parameters":{"jsCode":"// searchStream returns chunks: [{ results: [ { segments, metrics } ] }]\nconst LOOKBACK_DAYS = 30;\nconst rows = [];\n\nfor (const item of $input.all()) {\n  const chunk = item.json;\n  const results = chunk.results || (chunk.segments ? [chunk] : []);\n  for (const r of results) rows.push(r);\n}\n\nconst agg = new Map();\n\nfor (const r of rows) {\n  const seg = r.segments || {};\n  const m = r.metrics || {};\n  const offerId = String(seg.productItemId || seg.product_item_id || '').trim();\n  if (!offerId) continue;\n\n  const cur = agg.get(offerId) || {\n    offer_id: offerId,\n    product_title: seg.productTitle || seg.product_title || '',\n    cost_micros: 0, impressions: 0, clicks: 0, conversions: 0, conversions_value: 0,\n  };\n\n  cur.cost_micros += Number(m.costMicros || m.cost_micros || 0);\n  cur.impressions += Number(m.impressions || 0);\n  cur.clicks += Number(m.clicks || 0);\n  cur.conversions += Number(m.conversions || 0);\n  cur.conversions_value += Number(m.conversionsValue || m.conversions_value || 0);\n  agg.set(offerId, cur);\n}\n\nreturn [...agg.values()].map((c) => {\n  const spend = c.cost_micros / 1000000;\n  const roas = spend > 0 ? c.conversions_value / spend : 0;\n  const cpc = c.clicks > 0 ? spend / c.clicks : 0;\n  const ctr = c.impressions > 0 ? c.clicks / c.impressions : 0;\n  const cvr = c.clicks > 0 ? c.conversions / c.clicks : 0;\n  return { json: {\n    source: 'perf',\n    offer_id: c.offer_id,\n    product_title: c.product_title,\n    spend: Number(spend.toFixed(2)),\n    impressions: c.impressions,\n    clicks: c.clicks,\n    conversions: Number(c.conversions.toFixed(2)),\n    conversions_value: Number(c.conversions_value.toFixed(2)),\n    roas: Number(roas.toFixed(2)),\n    cpc: Number(cpc.toFixed(2)),\n    ctr: Number((ctr * 100).toFixed(2)),\n    cvr: Number((cvr * 100).toFixed(2)),\n    lookback_days: LOOKBACK_DAYS,\n    daily_spend: Number((spend / LOOKBACK_DAYS).toFixed(2)),\n  } };\n});"}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000a6","name":"Merge Feed & Performance","type":"n8n-nodes-base.merge","typeVersion":3,"position":[600,300],"parameters":{"mode":"append","options":{}}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000a7","name":"Score & Bucket Products","type":"n8n-nodes-base.code","typeVersion":2,"position":[880,300],"parameters":{"jsCode":"// Join feed rows + performance rows on offer_id, score feed health,\n// then bucket every SKU into scale / fix / exclude / maintain.\nconst TARGET_ROAS = 2.5;      // account break-even target\nconst MIN_SPEND = 25;         // below this the ROAS signal is noise\nconst SCALE_MULT = 1.3;       // needs 130% of target to earn more budget\nconst KILL_MULT = 0.6;        // under 60% of target it burns money\nconst MIN_IMPRESSIONS = 300;  // enough delivery to judge the listing\n\nconst feed = new Map();\nconst perf = new Map();\nfor (const i of $input.all()) {\n  const r = i.json;\n  if (r.source === 'feed') feed.set(r.offer_id, r);\n  else if (r.source === 'perf') perf.set(r.offer_id, r);\n}\n\nconst out = [];\nconst offerIds = new Set([...feed.keys(), ...perf.keys()]);\n\nfor (const offerId of offerIds) {\n  const f = feed.get(offerId) || { missing_attributes: [], missing_critical: [], disapproved: false, demoted: false, disapproval_codes: [], top_resolution: 'none' };\n  const p = perf.get(offerId) || { spend: 0, roas: 0, impressions: 0, clicks: 0, conversions: 0, conversions_value: 0, ctr: 0, cvr: 0, daily_spend: 0 };\n\n  // ---- feed health score (0-100) ----\n  let health = 100;\n  if (f.disapproved) health -= 45;\n  if (f.demoted) health -= 15;\n  health -= Math.min(20, (f.missing_critical || []).length * 10);\n  health -= Math.min(15, Math.max(0, (f.missing_attributes || []).length - (f.missing_critical || []).length) * 3);\n  if (f.availability === 'out of stock') health -= 20;\n  health = Math.max(0, Math.min(100, health));\n\n  const blocking = Boolean(f.disapproved) || (f.missing_critical || []).length > 0;\n  const credible = p.spend >= MIN_SPEND && p.impressions >= MIN_IMPRESSIONS;\n\n  // ---- bucket ----\n  let bucket = 'maintain';\n  let reason = 'stable - leave bids alone';\n  if (blocking) {\n    bucket = 'fix';\n    reason = f.disapproved\n      ? 'disapproved: ' + ((f.disapproval_codes || []).join(', ') || 'unknown')\n      : 'missing critical attributes: ' + (f.missing_critical || []).join(', ');\n  } else if (credible && p.roas >= TARGET_ROAS * SCALE_MULT && health >= 70) {\n    bucket = 'scale';\n    reason = 'ROAS ' + p.roas + ' vs target ' + TARGET_ROAS + ' on $' + p.spend;\n  } else if (credible && p.roas < TARGET_ROAS * KILL_MULT) {\n    bucket = 'exclude';\n    reason = 'ROAS ' + p.roas + ' after $' + p.spend + ' spend';\n  } else if (!credible && p.spend >= MIN_SPEND && p.conversions === 0) {\n    bucket = 'exclude';\n    reason = 'no conversions on $' + p.spend + ' with thin delivery';\n  } else if (health < 70) {\n    bucket = 'fix';\n    reason = 'feed health ' + health + ' - ' + ((f.missing_attributes || []).join(', ') || 'demoted listing');\n  }\n\n  // ---- bid modifier + 30d pacing projection ----\n  let bidModifier = 1;\n  if (bucket === 'scale') bidModifier = Math.min(1.5, Math.max(1.1, Number((p.roas / TARGET_ROAS).toFixed(2))));\n  if (bucket === 'exclude') bidModifier = 0;\n  if (bucket === 'fix') bidModifier = 0.8;\n\n  const projected30d = Number((p.daily_spend * 30 * bidModifier).toFixed(2));\n  const projectedValue = Number((projected30d * (bucket === 'scale' ? p.roas * 0.9 : p.roas)).toFixed(2));\n\n  // priority = money at stake, weighted by how broken the listing is\n  const priority = Number((p.spend * (1 + (100 - health) / 100) + Math.abs(TARGET_ROAS - p.roas) * 10).toFixed(1));\n\n  out.push({ json: {\n    offer_id: offerId,\n    title: f.title || p.product_title || offerId,\n    brand: f.brand || '',\n    bucket,\n    reason,\n    blocking,\n    feed_health: health,\n    disapproved: Boolean(f.disapproved),\n    missing_attributes: (f.missing_attributes || []).join('|'),\n    missing_critical: (f.missing_critical || []).join('|'),\n    resolution: f.top_resolution || 'none',\n    spend: p.spend,\n    roas: p.roas,\n    conversions: p.conversions,\n    conversions_value: p.conversions_value,\n    impressions: p.impressions,\n    ctr: p.ctr,\n    cvr: p.cvr,\n    bid_modifier: bidModifier,\n    projected_30d_spend: projected30d,\n    projected_30d_value: projectedValue,\n    priority,\n    audited_at: new Date().toISOString().slice(0, 10),\n  } });\n}\n\nout.sort((a, b) => b.json.priority - a.json.priority);\nreturn out;"}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000a8","name":"Has Blocking Feed Issue?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1160,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"c1","leftValue":"={{ $json.bucket }}","rightValue":"fix","operator":{"type":"string","operation":"equals"}}]},"options":{}}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000a9","name":"Build Feed Fix Tasks","type":"n8n-nodes-base.code","typeVersion":2,"position":[1440,60],"parameters":{"jsCode":"// Turn each broken SKU into an actionable ticket, ordered by revenue at risk.\nconst PLAYBOOK = {\n  gtin: 'Pull GTIN from supplier sheet and push to the gtin column',\n  brand: 'Set brand to the manufacturer name (not the store name)',\n  image_link: 'Upload a 1200px white-background image, no text overlay',\n  availability: 'Re-sync inventory - listing is serving with stale stock',\n  price: 'Price mismatch with landing page - re-sync price + sale_price',\n  product_type: 'Add a 3-level product_type breadcrumb',\n  google_product_category: 'Map to the closest Google taxonomy leaf node',\n  description: 'Write a 300+ char description with the key attributes up front',\n  shipping: 'Add a shipping rule for the target country',\n};\n\nreturn $input.all().map((i) => {\n  const r = i.json;\n  const attrs = (r.missing_critical || r.missing_attributes || '').split('|').filter(Boolean);\n  const steps = attrs.map((a) => PLAYBOOK[a] || ('Fix attribute: ' + a));\n  if (r.disapproved) steps.unshift('Resolve disapproval: ' + (r.resolution || 'see Merchant Center diagnostics'));\n  const revenueAtRisk = Number((r.conversions_value || 0).toFixed(2));\n  return { json: {\n    task_type: 'feed_fix',\n    offer_id: r.offer_id,\n    title: r.title,\n    severity: r.disapproved ? 'P1 - not serving' : (r.feed_health < 55 ? 'P2 - demoted' : 'P3 - hygiene'),\n    feed_health: r.feed_health,\n    missing_attributes: attrs.join(', ') || 'none',\n    action_steps: steps.join(' | '),\n    revenue_at_risk: revenueAtRisk,\n    spend_30d: r.spend,\n    roas_30d: r.roas,\n    priority: r.priority,\n    audited_at: r.audited_at,\n  } };\n}).sort((a, b) => b.json.priority - a.json.priority);"}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000b1","name":"Append Feed Fix Queue","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[1720,60],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=111111","mode":"list","cachedResultName":"Feed Fix Queue"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000b2","name":"Alert Disapproved SKUs","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[2000,60],"parameters":{"select":"channel","channelId":{"__rl":true,"value":"#shopping-feed","mode":"name"},"text":"=🛒 {{ $json.severity }} — {{ $json.title }} ({{ $json.offer_id }})\nHealth {{ $json.feed_health }}/100 · ${{ $json.revenue_at_risk }} revenue at risk\nMissing: {{ $json.missing_attributes }}\nFix: {{ $json.action_steps }}","otherOptions":{}}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000b3","name":"Is Scale Candidate?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1440,480],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"c1","leftValue":"={{ $json.bucket }}","rightValue":"scale","operator":{"type":"string","operation":"equals"}}]},"options":{}}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000b4","name":"Build Scale Feed Rows","type":"n8n-nodes-base.code","typeVersion":2,"position":[1720,380],"parameters":{"jsCode":"// Winners get promoted into a high-intent custom label + a bid boost.\nreturn $input.all().map((i) => {\n  const r = i.json;\n  const tier = r.roas >= 6 ? 'hero' : r.roas >= 4 ? 'strong' : 'winner';\n  return { json: {\n    offer_id: r.offer_id,\n    title: r.title,\n    custom_label_0: 'roas_' + tier,\n    custom_label_1: 'scale',\n    custom_label_2: 'health_' + (r.feed_health >= 90 ? 'clean' : 'ok'),\n    excluded_destination: '',\n    bid_modifier: r.bid_modifier,\n    action: 'raise target ROAS bid by ' + Math.round((r.bid_modifier - 1) * 100) + '%',\n    roas_30d: r.roas,\n    spend_30d: r.spend,\n    projected_30d_spend: r.projected_30d_spend,\n    projected_30d_value: r.projected_30d_value,\n    reason: r.reason,\n    audited_at: r.audited_at,\n  } };\n});"}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000b5","name":"Build Exclude & Maintain Rows","type":"n8n-nodes-base.code","typeVersion":2,"position":[1720,680],"parameters":{"jsCode":"// Losers get excluded from Shopping; everything else keeps a neutral label\n// so the supplemental feed always has one row per SKU (no stale overrides).\nreturn $input.all().map((i) => {\n  const r = i.json;\n  const isExclude = r.bucket === 'exclude';\n  return { json: {\n    offer_id: r.offer_id,\n    title: r.title,\n    custom_label_0: isExclude ? 'roas_loser' : 'roas_neutral',\n    custom_label_1: isExclude ? 'exclude' : 'maintain',\n    custom_label_2: 'health_' + (r.feed_health >= 70 ? 'ok' : 'watch'),\n    excluded_destination: isExclude ? 'Shopping_ads' : '',\n    bid_modifier: isExclude ? 0 : 1,\n    action: isExclude\n      ? 'exclude from Shopping — saves ~$' + (r.spend || 0).toFixed(2) + ' / 30d'\n      : 'hold — not enough signal to act',\n    roas_30d: r.roas,\n    spend_30d: r.spend,\n    projected_30d_spend: isExclude ? 0 : r.projected_30d_spend,\n    projected_30d_value: isExclude ? 0 : r.projected_30d_value,\n    reason: r.reason,\n    audited_at: r.audited_at,\n  } };\n});"}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000b6","name":"Merge Supplemental Rows","type":"n8n-nodes-base.merge","typeVersion":3,"position":[2000,520],"parameters":{"mode":"append","options":{}}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000b7","name":"Update Supplemental Feed Sheet","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[2280,520],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"Supplemental Feed"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000b8","name":"Build Audit Summary","type":"n8n-nodes-base.code","typeVersion":2,"position":[2560,520],"parameters":{"jsCode":"// One digest email for the whole run.\nconst rows = $input.all().map((i) => i.json);\nconst scale = rows.filter((r) => r.custom_label_1 === 'scale');\nconst exclude = rows.filter((r) => r.custom_label_1 === 'exclude');\nconst maintain = rows.filter((r) => r.custom_label_1 === 'maintain');\n\nconst sum = (arr, k) => Number(arr.reduce((a, r) => a + (Number(r[k]) || 0), 0).toFixed(2));\nconst savings = sum(exclude, 'spend_30d');\nconst scaleSpend = sum(scale, 'spend_30d');\nconst scaleValue = sum(scale, 'projected_30d_value');\nconst blendedRoas = scaleSpend > 0 ? Number((scaleValue / sum(scale, 'projected_30d_spend')).toFixed(2)) : 0;\n\nconst line = (r) => '<li><b>' + r.title + '</b> (' + r.offer_id + ') — ROAS ' + r.roas_30d + ' on $' + r.spend_30d + ' — ' + r.action + '</li>';\n\nconst html = [\n  '<h2>Shopping feed audit — ' + new Date().toISOString().slice(0, 10) + '</h2>',\n  '<p>' + rows.length + ' SKUs bucketed: ' + scale.length + ' scale · ' + exclude.length + ' exclude · ' + maintain.length + ' maintain.</p>',\n  '<p>Reclaimed budget from exclusions: <b>$' + savings + '</b> / 30d. Projected blended ROAS on the scale set: <b>' + blendedRoas + '</b>.</p>',\n  '<h3>Scaling</h3><ul>' + scale.slice(0, 15).map(line).join('') + '</ul>',\n  '<h3>Excluding</h3><ul>' + exclude.slice(0, 15).map(line).join('') + '</ul>',\n].join('');\n\nreturn [{ json: { html, total: rows.length, scale_count: scale.length, exclude_count: exclude.length, maintain_count: maintain.length, reclaimed_spend: savings, projected_blended_roas: blendedRoas } }];"}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000b9","name":"Email Shopping Audit Digest","type":"n8n-nodes-base.gmail","typeVersion":2.1,"position":[2840,520],"parameters":{"sendTo":"ecom@brand.com","subject":"=Shopping feed audit {{ $now.format('yyyy-LL-dd') }} — {{ $json.scale_count }} scale / {{ $json.exclude_count }} exclude","message":"={{ $json.html }}","options":{}}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000c1","name":"Feed Audit Notes","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[-280,-60],"parameters":{"content":"## 1. PULL BOTH SIDES\nMerchant Center product statuses (disapprovals + item level issues) and 30d Shopping performance from the Google Ads API, keyed on offer_id / segments.product_item_id.","height":300,"width":520,"color":4}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000c2","name":"Scoring Notes","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[840,-60],"parameters":{"content":"## 2. SCORE + BUCKET\nFeed health 0-100 (disapproval -45, missing critical attr -10 each). Then:\n• ROAS >= 1.3x target on >= $25 spend and health >= 70 -> SCALE\n• ROAS < 0.6x target on >= $25 spend -> EXCLUDE\n• disapproved or missing gtin/image/price -> FIX\n• everything else -> MAINTAIN","height":320,"width":520,"color":5}},{"id":"3f1a0c10-1001-4a01-8b01-0000000000c3","name":"Output Notes","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[1960,800],"parameters":{"content":"## 3. WRITE BACK\nFix tickets go to the Feed Fix Queue tab + Slack. Scale/exclude/maintain rows write custom_label_0..2 and excluded_destination into the supplemental feed sheet Merchant Center pulls from. Digest email closes the loop.","height":300,"width":520,"color":3}}],"connections":{"Daily Feed Audit Trigger":{"main":[[{"node":"Fetch Merchant Center Product Statuses","type":"main","index":0},{"node":"Fetch Shopping Performance (GAQL)","type":"main","index":0}]]},"Fetch Merchant Center Product Statuses":{"main":[[{"node":"Normalize Feed Statuses","type":"main","index":0}]]},"Fetch Shopping Performance (GAQL)":{"main":[[{"node":"Normalize Product ROAS","type":"main","index":0}]]},"Normalize Feed Statuses":{"main":[[{"node":"Merge Feed & Performance","type":"main","index":0}]]},"Normalize Product ROAS":{"main":[[{"node":"Merge Feed & Performance","type":"main","index":1}]]},"Merge Feed & Performance":{"main":[[{"node":"Score & Bucket Products","type":"main","index":0}]]},"Score & Bucket Products":{"main":[[{"node":"Has Blocking Feed Issue?","type":"main","index":0}]]},"Has Blocking Feed Issue?":{"main":[[{"node":"Build Feed Fix Tasks","type":"main","index":0}],[{"node":"Is Scale Candidate?","type":"main","index":0}]]},"Build Feed Fix Tasks":{"main":[[{"node":"Append Feed Fix Queue","type":"main","index":0}]]},"Append Feed Fix Queue":{"main":[[{"node":"Alert Disapproved SKUs","type":"main","index":0}]]},"Is Scale Candidate?":{"main":[[{"node":"Build Scale Feed Rows","type":"main","index":0}],[{"node":"Build Exclude & Maintain Rows","type":"main","index":0}]]},"Build Scale Feed Rows":{"main":[[{"node":"Merge Supplemental Rows","type":"main","index":0}]]},"Build Exclude & Maintain Rows":{"main":[[{"node":"Merge Supplemental Rows","type":"main","index":1}]]},"Merge Supplemental Rows":{"main":[[{"node":"Update Supplemental Feed Sheet","type":"main","index":0}]]},"Update Supplemental Feed Sheet":{"main":[[{"node":"Build Audit Summary","type":"main","index":0}]]},"Build Audit Summary":{"main":[[{"node":"Email Shopping Audit Digest","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"},"pinData":{}}