{"name":"Dropshipping scale workflow","nodes":[{"id":"d5a10023-0001-4a23-8b23-000000000001","name":"Daily Scaling Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[-300,300],"parameters":{"rule":{"interval":[{"field":"days","daysInterval":1}]}}},{"id":"d5a10023-0002-4a23-8b23-000000000002","name":"Fetch Shopify Orders","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-40,140],"parameters":{"url":"https://STORE.myshopify.com/admin/api/2024-10/orders.json","sendQuery":true,"queryParameters":{"parameters":[{"name":"status","value":"any"},{"name":"created_at_min","value":"={{ $now.minus({ days: 1 }).startOf(\"day\").toISO() }}"},{"name":"created_at_max","value":"={{ $now.startOf(\"day\").toISO() }}"},{"name":"fields","value":"id,created_at,cancelled_at,financial_status,line_items,refunds,total_discounts"},{"name":"limit","value":"250"}]},"options":{}}},{"id":"d5a10023-0003-4a23-8b23-000000000003","name":"Fetch Meta Ad Spend","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-40,460],"parameters":{"url":"https://graph.facebook.com/v21.0/act_ID/insights","sendQuery":true,"queryParameters":{"parameters":[{"name":"level","value":"adset"},{"name":"fields","value":"adset_id,adset_name,spend,impressions,clicks,frequency,ctr,cpc,purchase_roas,actions,daily_budget"},{"name":"date_preset","value":"yesterday"},{"name":"limit","value":"500"}]},"options":{}}},{"id":"d5a10023-0004-4a23-8b23-000000000004","name":"Roll Up Product Revenue","type":"n8n-nodes-base.code","typeVersion":2,"position":[220,140],"parameters":{"jsCode":"// Shopify Admin API -> per-product daily sales rollup (last complete day)\nconst orders = $input.all().map(i => i.json).flatMap(o => o.orders || [o]);\nconst byProduct = {};\nconst seenOrders = new Set();\n\nfor (const order of orders) {\n  // dedupe: the same order can arrive twice across paginated pulls\n  if (!order || seenOrders.has(order.id)) continue;\n  seenOrders.add(order.id);\n  if (order.cancelled_at) continue;\n  if (order.financial_status && !['paid', 'partially_refunded'].includes(order.financial_status)) continue;\n\n  const refundedByLine = {};\n  for (const r of order.refunds || []) {\n    for (const rli of r.refund_line_items || []) {\n      refundedByLine[rli.line_item_id] = (refundedByLine[rli.line_item_id] || 0) + Number(rli.quantity || 0);\n    }\n  }\n\n  for (const li of order.line_items || []) {\n    const pid = String(li.product_id);\n    const qty = Number(li.quantity || 0) - Number(refundedByLine[li.id] || 0);\n    if (qty <= 0) continue;\n\n    const unitPrice = Number(li.price || 0);\n    const lineDiscount = (li.discount_allocations || [])\n      .reduce((s, d) => s + Number(d.amount || 0), 0);\n    const netRevenue = unitPrice * qty - lineDiscount;\n\n    const p = byProduct[pid] || (byProduct[pid] = {\n      product_id: pid,\n      product_title: li.title || li.name || 'unknown',\n      sku: li.sku || null,\n      units: 0, net_revenue: 0, cogs: 0, order_ids: new Set(),\n    });\n    // COGS: Shopify does not return cost on the order line, so the sourcing sheet\n    // stamps a \"cogs\" line-item property at checkout; fall back to 38% of price.\n    const cogsTag = (li.properties || []).find(x => x.name === 'cogs');\n    const unitCogs = cogsTag ? Number(cogsTag.value) : unitPrice * 0.38;\n\n    p.units += qty;\n    p.net_revenue += netRevenue;\n    p.cogs += unitCogs * qty;\n    // count distinct orders, not line items — a 3-line order is still one order\n    p.order_ids.add(order.id);\n  }\n}\n\nreturn Object.values(byProduct).map(p => {\n  const orders = p.order_ids.size;\n  return { json: {\n    source: 'shopify',\n    product_id: p.product_id,\n    product_title: p.product_title,\n    sku: p.sku,\n    units: p.units,\n    orders,\n    net_revenue: Math.round(p.net_revenue * 100) / 100,\n    cogs: Math.round(p.cogs * 100) / 100,\n    aov: orders ? Math.round((p.net_revenue / orders) * 100) / 100 : 0,\n  } };\n});"}},{"id":"d5a10023-0005-4a23-8b23-000000000005","name":"Roll Up Product Ad Spend","type":"n8n-nodes-base.code","typeVersion":2,"position":[220,460],"parameters":{"jsCode":"// Meta Marketing API insights -> one row per product ad set.\n// Ad sets are named \"<product_id> | <product> | <angle>\" by the launch workflow.\nconst rows = $input.all().flatMap(i => i.json.data || [i.json]);\nconst out = {};\n\nfor (const r of rows) {\n  if (!r || !r.adset_name) continue;\n  const pid = String(r.adset_name).split('|')[0].trim();\n  if (!/^\\d+$/.test(pid)) continue;\n\n  const spend = Number(r.spend || 0);\n  const impressions = Number(r.impressions || 0);\n  const clicks = Number(r.clicks || 0);\n  const roasEntry = (r.purchase_roas || []).find(a => a.action_type === 'omni_purchase');\n  const purchases = (r.actions || [])\n    .filter(a => a.action_type === 'omni_purchase' || a.action_type === 'purchase')\n    .reduce((s, a) => s + Number(a.value || 0), 0);\n\n  const p = out[pid] || (out[pid] = {\n    product_id: pid, spend: 0, impressions: 0, clicks: 0, purchases: 0,\n    reported_revenue: 0, frequency_num: 0, adsets: [],\n  });\n  p.spend += spend;\n  p.impressions += impressions;\n  p.clicks += clicks;\n  p.purchases += purchases;\n  p.reported_revenue += spend * Number(roasEntry ? roasEntry.value : 0);\n  p.frequency_num += Number(r.frequency || 0) * impressions;\n  p.adsets.push({\n    adset_id: r.adset_id,\n    adset_name: r.adset_name,\n    daily_budget: Number(r.daily_budget || 0) / 100,\n    spend: spend,\n    purchase_roas: roasEntry ? Number(roasEntry.value) : 0,\n  });\n}\n\nreturn Object.values(out).map(p => ({ json: {\n  source: 'meta',\n  product_id: p.product_id,\n  spend: Math.round(p.spend * 100) / 100,\n  impressions: p.impressions,\n  clicks: p.clicks,\n  purchases: p.purchases,\n  cpc: p.clicks ? Math.round((p.spend / p.clicks) * 100) / 100 : 0,\n  ctr: p.impressions ? Math.round((p.clicks / p.impressions) * 10000) / 100 : 0,\n  cpa: p.purchases ? Math.round((p.spend / p.purchases) * 100) / 100 : 0,\n  frequency: p.impressions ? Math.round((p.frequency_num / p.impressions) * 100) / 100 : 0,\n  platform_roas: p.spend ? Math.round((p.reported_revenue / p.spend) * 100) / 100 : 0,\n  adsets: p.adsets,\n} }));"}},{"id":"d5a10023-0006-4a23-8b23-000000000006","name":"Merge Sales And Spend","type":"n8n-nodes-base.merge","typeVersion":3,"position":[480,300],"parameters":{"mode":"append","options":{}}},{"id":"d5a10023-0007-4a23-8b23-000000000007","name":"Compute Contribution Margin","type":"n8n-nodes-base.code","typeVersion":2,"position":[740,300],"parameters":{"jsCode":"// Join Shopify truth with Meta spend and compute contribution margin after COGS + fees.\nconst PAYMENT_RATE = 0.029;      // Shopify Payments %\nconst PAYMENT_FIXED = 0.30;      // per order\nconst SHIP_PER_UNIT = 4.20;      // blended fulfilment + pick/pack\nconst REFUND_RESERVE = 0.04;     // % of revenue held back for chargebacks/refunds\nconst OVERHEAD_PER_ORDER = 1.10; // apps, support, 3PL admin\nconst TARGET_MARGIN_PCT = 0.15;  // we scale anything clearing 15% net\n\nconst merged = {};\nfor (const item of $input.all()) {\n  const r = item.json;\n  const pid = String(r.product_id);\n  merged[pid] = Object.assign({ product_id: pid }, merged[pid] || {}, r);\n}\n\nconst results = Object.values(merged).map(p => {\n  const units = Number(p.units || 0);\n  const orders = Number(p.orders || 0);\n  const revenue = Number(p.net_revenue || 0);\n  const cogs = Number(p.cogs || 0);\n  const spend = Number(p.spend || 0);\n\n  const fees = revenue * PAYMENT_RATE + orders * PAYMENT_FIXED;\n  const shipping = units * SHIP_PER_UNIT;\n  const refunds = revenue * REFUND_RESERVE;\n  const overhead = orders * OVERHEAD_PER_ORDER;\n\n  const gross_profit = revenue - cogs - fees - shipping - refunds - overhead;\n  const contribution = gross_profit - spend;\n  const margin_pct = revenue > 0 ? contribution / revenue : -1;\n  const breakeven_roas = revenue > 0 ? revenue / Math.max(gross_profit, 0.01) : 99;\n  const true_roas = spend > 0 ? revenue / spend : (revenue > 0 ? 99 : 0);\n  const mer_gap = true_roas - breakeven_roas;\n\n  // Scale score: profitability headroom, weighted by volume confidence and\n  // penalised by creative fatigue (frequency) and thin click economics.\n  const volumeConfidence = Math.min(1, orders / 10);\n  const fatiguePenalty = Math.max(0, (Number(p.frequency || 0) - 2.2)) * 0.35;\n  const ctrBonus = Number(p.ctr || 0) >= 1.2 ? 0.15 : 0;\n  const score = Math.round(\n    ((mer_gap * 1.6) + (margin_pct * 3) + ctrBonus - fatiguePenalty) * volumeConfidence * 100\n  ) / 100;\n\n  // Pacing: what tomorrow's spend should be if we keep margin at target.\n  const spendHeadroom = Math.max(0, gross_profit * (1 - TARGET_MARGIN_PCT) - spend);\n  const projected_spend = Math.round((spend + spendHeadroom * 0.5) * 100) / 100;\n\n  let decision = 'hold';\n  if (contribution > 0 && margin_pct >= TARGET_MARGIN_PCT && orders >= 3) decision = 'scale';\n  else if (contribution < 0) decision = 'cut';\n\n  return { json: {\n    product_id: p.product_id,\n    product_title: p.product_title || 'unknown',\n    sku: p.sku || null,\n    units, orders, spend,\n    revenue: Math.round(revenue * 100) / 100,\n    cogs: Math.round(cogs * 100) / 100,\n    fees: Math.round((fees + shipping + refunds + overhead) * 100) / 100,\n    gross_profit: Math.round(gross_profit * 100) / 100,\n    contribution_margin: Math.round(contribution * 100) / 100,\n    margin_pct: Math.round(margin_pct * 1000) / 10,\n    true_roas: Math.round(true_roas * 100) / 100,\n    breakeven_roas: Math.round(breakeven_roas * 100) / 100,\n    mer_gap: Math.round(mer_gap * 100) / 100,\n    frequency: Number(p.frequency || 0),\n    ctr: Number(p.ctr || 0),\n    cpa: Number(p.cpa || 0),\n    score,\n    projected_spend,\n    decision,\n    adsets: p.adsets || [],\n    day: new Date(Date.now() - 864e5).toISOString().slice(0, 10),\n  } };\n});\n\n// Best opportunities first so the digest reads top-down.\nreturn results.sort((a, b) => b.json.score - a.json.score);"}},{"id":"d5a10023-0008-4a23-8b23-000000000008","name":"Drop Low-Signal Products","type":"n8n-nodes-base.filter","typeVersion":2,"position":[1000,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"f1","leftValue":"={{ $json.spend }}","rightValue":15,"operator":{"type":"number","operation":"gte"}},{"id":"f2","leftValue":"={{ $json.units }}","rightValue":1,"operator":{"type":"number","operation":"gte"}}]},"options":{}}},{"id":"d5a10023-0009-4a23-8b23-000000000009","name":"Margin Positive?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1260,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"c1","leftValue":"={{ $json.contribution_margin }}","rightValue":0,"operator":{"type":"number","operation":"gt"}},{"id":"c2","leftValue":"={{ $json.margin_pct }}","rightValue":15,"operator":{"type":"number","operation":"gte"}}]},"options":{}}},{"id":"d5a10023-0010-4a23-8b23-000000000010","name":"Build Budget Increases","type":"n8n-nodes-base.code","typeVersion":2,"position":[1520,80],"parameters":{"jsCode":"// Turn a scale decision into a concrete budget step, one row per ad set.\n// Never more than +25% a day (Meta relearning), never above the daily cap.\nconst MAX_STEP = 0.25;\nconst DAILY_CAP = 400;\nconst items = $input.all().map(i => i.json);\nconst out = [];\n\nfor (const p of items) {\n  const step = Math.min(MAX_STEP, 0.08 + Math.max(0, p.score) * 0.05);\n  const adsets = (p.adsets || []).filter(a => a.spend > 0);\n  if (!adsets.length) continue;\n\n  // Give the increase to the ad sets carrying the product, weighted by their ROAS.\n  const roasSum = adsets.reduce((s, a) => s + Math.max(a.purchase_roas, 0.1), 0);\n  for (const a of adsets) {\n    const share = Math.max(a.purchase_roas, 0.1) / roasSum;\n    const current = a.daily_budget || Math.max(a.spend, 10);\n    const proposed = current * (1 + step * share * adsets.length);\n    const next = Math.min(DAILY_CAP, Math.max(current + 2, Math.round(proposed)));\n    if (next <= current) continue;\n    out.push({ json: {\n      product_id: p.product_id,\n      product_title: p.product_title,\n      adset_id: a.adset_id,\n      adset_name: a.adset_name,\n      old_daily_budget: current,\n      new_daily_budget: next,\n      new_daily_budget_cents: Math.round(next * 100),\n      pct_increase: Math.round(((next / current) - 1) * 1000) / 10,\n      action: 'scale',\n      reason: 'margin ' + p.margin_pct + '% at ' + p.true_roas + 'x vs breakeven ' + p.breakeven_roas + 'x',\n      score: p.score,\n      contribution_margin: p.contribution_margin,\n    } });\n  }\n}\nreturn out;"}},{"id":"d5a10023-0011-4a23-8b23-000000000011","name":"Raise Ad Set Daily Budget","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1780,80],"parameters":{"method":"POST","url":"=https://graph.facebook.com/v21.0/{{ $json.adset_id }}","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ daily_budget: $json.new_daily_budget_cents, status: \"ACTIVE\" }) }}","options":{}}},{"id":"d5a10023-0012-4a23-8b23-000000000012","name":"Losing Money Badly?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1520,520],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"c3","leftValue":"={{ $json.contribution_margin }}","rightValue":-50,"operator":{"type":"number","operation":"lt"}}]},"options":{}}},{"id":"d5a10023-0022-4a23-8b23-000000000022","name":"Build Pause List","type":"n8n-nodes-base.code","typeVersion":2,"position":[1780,440],"parameters":{"jsCode":"// Expand each deep-loser product into one item per live ad set.\n// Pausing only the first ad set would leave the rest of the product's spend running.\nconst out = [];\nfor (const p of $input.all().map(i => i.json)) {\n  const adsets = (p.adsets || []).filter(a => a.adset_id && Number(a.spend || 0) > 0);\n  if (!adsets.length) continue;\n  // Kill the worst performers first so the cheapest ROAS stops burning soonest.\n  adsets.sort((a, b) => Number(a.purchase_roas || 0) - Number(b.purchase_roas || 0));\n  for (const a of adsets) {\n    out.push({ json: {\n      product_id: p.product_id,\n      product_title: p.product_title,\n      adset_id: a.adset_id,\n      adset_name: a.adset_name,\n      adset_spend: a.spend,\n      adset_roas: a.purchase_roas,\n      spend: p.spend,\n      contribution_margin: p.contribution_margin,\n      margin_pct: p.margin_pct,\n      reason: 'lost $' + Math.abs(p.contribution_margin) + ' on $' + p.spend + ' spend at ' + p.true_roas + 'x',\n    } });\n  }\n}\nreturn out;"}},{"id":"d5a10023-0013-4a23-8b23-000000000013","name":"Pause Unprofitable Ad Sets","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2040,440],"parameters":{"method":"POST","url":"=https://graph.facebook.com/v21.0/{{ $json.adset_id }}","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ status: \"PAUSED\" }) }}","options":{}}},{"id":"d5a10023-0014-4a23-8b23-000000000014","name":"Flag Watchlist Product","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[1780,700],"parameters":{"assignments":{"assignments":[{"id":"w1","name":"action","value":"watch","type":"string"},{"id":"w2","name":"product_title","value":"={{ $json.product_title }}","type":"string"},{"id":"w3","name":"margin_pct","value":"={{ $json.margin_pct }}","type":"number"},{"id":"w4","name":"contribution_margin","value":"={{ $json.contribution_margin }}","type":"number"},{"id":"w5","name":"spend","value":"={{ $json.spend }}","type":"number"},{"id":"w6","name":"note","value":"=Negative but shallow — needs a COGS or offer fix before cutting","type":"string"}]},"options":{}}},{"id":"d5a10023-0015-4a23-8b23-000000000015","name":"Tag Paused Products","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[2300,560],"parameters":{"assignments":{"assignments":[{"id":"p1","name":"action","value":"pause","type":"string"},{"id":"p2","name":"product_title","value":"={{ $(\"Build Pause List\").item.json.product_title }}","type":"string"},{"id":"p3","name":"contribution_margin","value":"={{ $(\"Build Pause List\").item.json.contribution_margin }}","type":"number"},{"id":"p4","name":"spend","value":"={{ $(\"Build Pause List\").item.json.spend }}","type":"number"},{"id":"p5","name":"adset_name","value":"={{ $(\"Build Pause List\").item.json.adset_name }}","type":"string"},{"id":"p6","name":"reason","value":"={{ $(\"Build Pause List\").item.json.reason }}","type":"string"},{"id":"p7","name":"adset_spend","value":"={{ $(\"Build Pause List\").item.json.adset_spend }}","type":"number"}]},"options":{}}},{"id":"d5a10023-0016-4a23-8b23-000000000016","name":"Log Decisions To Sheet","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[2300,300],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"Scale Log"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"d5a10023-0017-4a23-8b23-000000000017","name":"Build Margin Digest","type":"n8n-nodes-base.code","typeVersion":2,"position":[2560,300],"parameters":{"jsCode":"// Fold every branch back into one Slack-ready digest.\nconst rows = $input.all().map(i => i.json);\nconst scaled = rows.filter(r => r.action === 'scale');\nconst paused = rows.filter(r => r.action === 'pause');\nconst watched = rows.filter(r => r.action === 'watch');\n\nconst addedBudget = scaled.reduce((s, r) => s + (r.new_daily_budget - r.old_daily_budget), 0);\n// paused rows are one per ad set — use ad set spend so a multi-adset product isn't counted twice\nconst savedBudget = paused.reduce((s, r) => s + Number(r.adset_spend != null ? r.adset_spend : r.spend || 0), 0);\n// contribution margin is a per-product figure, so de-dupe before summing across expanded rows\nconst marginByProduct = {};\nfor (const r of rows) {\n  const key = r.product_id || r.product_title || r.adset_name;\n  if (key && !(key in marginByProduct)) marginByProduct[key] = Number(r.contribution_margin || 0);\n}\nconst totalMargin = Object.values(marginByProduct).reduce((s, v) => s + v, 0);\n\nconst line = r => '• ' + (r.product_title || r.adset_name) + ' — ' +\n  (r.action === 'scale'\n    ? '$' + r.old_daily_budget + ' → $' + r.new_daily_budget + '/day (' + r.reason + ')'\n    : r.action === 'pause'\n      ? 'PAUSED, lost $' + Math.abs(r.contribution_margin) + ' on $' + r.spend + ' spend'\n      : 'watch, margin ' + r.margin_pct + '%');\n\nconst text = [\n  '*Dropshipping scale run — ' + new Date(Date.now() - 864e5).toISOString().slice(0, 10) + '*',\n  'Contribution margin: $' + Math.round(totalMargin) +\n    ' | scaled ' + scaled.length + ' (+$' + Math.round(addedBudget) + '/day)' +\n    ' | paused ' + paused.length + ' (-$' + Math.round(savedBudget) + '/day)' +\n    ' | watching ' + watched.length,\n  '',\n  scaled.length ? '*Scaled*' : '',\n  ...scaled.slice(0, 10).map(line),\n  paused.length ? '*Paused*' : '',\n  ...paused.slice(0, 10).map(line),\n  watched.length ? '*Watchlist*' : '',\n  ...watched.slice(0, 10).map(line),\n].filter(Boolean).join('\\n');\n\nreturn [{ json: { text, scaled: scaled.length, paused: paused.length, watched: watched.length,\n  contribution_margin: Math.round(totalMargin * 100) / 100 } }];"}},{"id":"d5a10023-0018-4a23-8b23-000000000018","name":"Post Digest To Slack","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[2820,300],"parameters":{"select":"channel","channelId":{"__rl":true,"value":"#store-scaling","mode":"name"},"text":"={{ $json.text }}","otherOptions":{}}},{"id":"d5a10023-0019-4a23-8b23-000000000019","name":"Note Ingest","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[-360,-80],"parameters":{"content":"## 1. INGEST\nEvery morning pull yesterday's Shopify orders (revenue, units, refunds, COGS tags) and yesterday's Meta ad set insights. Ad sets are named \"<product_id> | product | angle\" so spend can be attributed per product.","height":320,"width":520,"color":4}},{"id":"d5a10023-0020-4a23-8b23-000000000020","name":"Note Margin Math","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[700,-140],"parameters":{"content":"## 2. TRUE MARGIN\nContribution = revenue - COGS - payment fees - shipping - refund reserve - overhead - ad spend.\nProducts under $15 spend or 0 units are dropped (no signal). Score blends margin headroom vs breakeven ROAS, order-count confidence, CTR and frequency fatigue.","height":380,"width":520,"color":5}},{"id":"d5a10023-0021-4a23-8b23-000000000021","name":"Note Actions","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[1480,-280],"parameters":{"content":"## 3. ACT\nProfitable (>=15% margin): raise ad set budgets max +25%/day, weighted by ROAS, capped at $400/day.\nDeep losers (< -$50): pause.\nShallow losers: watchlist, fix COGS or offer first.\nEverything logged to Sheets and digested to Slack.","height":340,"width":520,"color":3}}],"connections":{"Daily Scaling Trigger":{"main":[[{"node":"Fetch Shopify Orders","type":"main","index":0},{"node":"Fetch Meta Ad Spend","type":"main","index":0}]]},"Fetch Shopify Orders":{"main":[[{"node":"Roll Up Product Revenue","type":"main","index":0}]]},"Fetch Meta Ad Spend":{"main":[[{"node":"Roll Up Product Ad Spend","type":"main","index":0}]]},"Roll Up Product Revenue":{"main":[[{"node":"Merge Sales And Spend","type":"main","index":0}]]},"Roll Up Product Ad Spend":{"main":[[{"node":"Merge Sales And Spend","type":"main","index":1}]]},"Merge Sales And Spend":{"main":[[{"node":"Compute Contribution Margin","type":"main","index":0}]]},"Compute Contribution Margin":{"main":[[{"node":"Drop Low-Signal Products","type":"main","index":0}]]},"Drop Low-Signal Products":{"main":[[{"node":"Margin Positive?","type":"main","index":0}]]},"Margin Positive?":{"main":[[{"node":"Build Budget Increases","type":"main","index":0}],[{"node":"Losing Money Badly?","type":"main","index":0}]]},"Build Budget Increases":{"main":[[{"node":"Raise Ad Set Daily Budget","type":"main","index":0}]]},"Raise Ad Set Daily Budget":{"main":[[{"node":"Log Decisions To Sheet","type":"main","index":0}]]},"Losing Money Badly?":{"main":[[{"node":"Build Pause List","type":"main","index":0}],[{"node":"Flag Watchlist Product","type":"main","index":0}]]},"Build Pause List":{"main":[[{"node":"Pause Unprofitable Ad Sets","type":"main","index":0}]]},"Pause Unprofitable Ad Sets":{"main":[[{"node":"Tag Paused Products","type":"main","index":0}]]},"Tag Paused Products":{"main":[[{"node":"Log Decisions To Sheet","type":"main","index":0}]]},"Flag Watchlist Product":{"main":[[{"node":"Log Decisions To Sheet","type":"main","index":0}]]},"Log Decisions To Sheet":{"main":[[{"node":"Build Margin Digest","type":"main","index":0}]]},"Build Margin Digest":{"main":[[{"node":"Post Digest To Slack","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"},"pinData":{}}