{"name":"Shopify scale workflow","nodes":[{"id":"3f0a0001-31d0-4a1b-8c2e-9b7d5e6f0001","name":"Daily Spend Review Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[-100,300],"parameters":{"rule":{"interval":[{"field":"days","daysInterval":1}]}}},{"id":"3f0a0002-31d0-4a1b-8c2e-9b7d5e6f0002","name":"Fetch Shopify Orders","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[160,300],"parameters":{"url":"https://STORE.myshopify.com/admin/api/2024-10/orders.json","sendQuery":true,"queryParameters":{"parameters":[{"name":"status","value":"any"},{"name":"financial_status","value":"paid"},{"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,test,financial_status,total_price,total_discounts,total_tax,total_shipping_price_set,line_items,refunds,customer"},{"name":"limit","value":"250"}]},"options":{}}},{"id":"3f0a0003-31d0-4a1b-8c2e-9b7d5e6f0003","name":"Fetch Meta Ad Spend","type":"n8n-nodes-base.facebookGraphApi","typeVersion":1,"position":[160,60],"parameters":{"hostUrl":"graph.facebook.com","httpRequestMethod":"GET","graphApiVersion":"v21.0","node":"act_ID/insights","options":{}}},{"id":"3f0a0004-31d0-4a1b-8c2e-9b7d5e6f0004","name":"Fetch Google Ads Spend","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[160,560],"parameters":{"method":"POST","url":"https://googleads.googleapis.com/v18/customers/CUSTOMER_ID/googleAds:searchStream","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ query: \"SELECT metrics.cost_micros, metrics.impressions, metrics.clicks, metrics.conversions, metrics.conversions_value FROM customer WHERE segments.date DURING YESTERDAY\" }) }}","options":{}}},{"id":"3f0a0005-31d0-4a1b-8c2e-9b7d5e6f0005","name":"Normalize Shopify Orders","type":"n8n-nodes-base.code","typeVersion":2,"position":[420,300],"parameters":{"jsCode":"// ---- Shopify orders -> one daily commerce summary --------------------------\n// Input: raw pages from /admin/api/2024-10/orders.json (each item = { orders: [...] })\nconst raw = $input.all().flatMap(i => i.json.orders || (i.json.id ? [i.json] : []));\n\n// 1. dedupe (pagination overlap is common on the Shopify REST cursor API)\nconst byId = new Map();\nfor (const o of raw) byId.set(String(o.id), o);\nconst orders = [...byId.values()]\n  // 2. drop anything that is not a real, paid, live order\n  .filter(o => !o.test && !o.cancelled_at && o.financial_status !== 'voided');\n\nconst num = v => (v === null || v === undefined ? 0 : parseFloat(v)) || 0;\n\nlet gross = 0, discounts = 0, refunds = 0, shipping = 0, tax = 0;\nlet newCustomerOrders = 0, returningOrders = 0, newCustomerRevenue = 0, units = 0;\n\nfor (const o of orders) {\n  const line = (o.line_items || []).reduce((s, li) => s + num(li.price) * (li.quantity || 0), 0);\n  const orderRefunds = (o.refunds || []).reduce((s, r) =>\n    s + (r.transactions || []).reduce((t, tr) => t + num(tr.amount), 0), 0);\n  const orderDiscount = num(o.total_discounts);\n  const net = num(o.total_price) - orderRefunds - num(o.total_tax) - num(o.total_shipping_price_set?.shop_money?.amount);\n\n  gross += line;\n  discounts += orderDiscount;\n  refunds += orderRefunds;\n  tax += num(o.total_tax);\n  shipping += num(o.total_shipping_price_set?.shop_money?.amount);\n  units += (o.line_items || []).reduce((s, li) => s + (li.quantity || 0), 0);\n\n  // Shopify sets customer.orders_count to the lifetime count INCLUDING this order,\n  // so 1 means this order is the customer's first purchase.\n  const lifetime = o.customer?.orders_count ?? 1;\n  if (lifetime <= 1) { newCustomerOrders++; newCustomerRevenue += Math.max(net, 0); }\n  else { returningOrders++; }\n}\n\nconst totalOrders = orders.length;\nconst netRevenue = gross - discounts - refunds;\n\nreturn [{ json: {\n  date: new Date(Date.now() - 864e5).toISOString().slice(0, 10),\n  orders: totalOrders,\n  units,\n  gross_revenue: +gross.toFixed(2),\n  discounts: +discounts.toFixed(2),\n  refunds: +refunds.toFixed(2),\n  shipping: +shipping.toFixed(2),\n  tax: +tax.toFixed(2),\n  net_revenue: +netRevenue.toFixed(2),\n  new_customer_orders: newCustomerOrders,\n  returning_orders: returningOrders,\n  new_customer_revenue: +newCustomerRevenue.toFixed(2),\n  new_customer_rate: totalOrders ? +(newCustomerOrders / totalOrders).toFixed(4) : 0,\n  aov: totalOrders ? +(netRevenue / totalOrders).toFixed(2) : 0,\n}}];"}},{"id":"3f0a0006-31d0-4a1b-8c2e-9b7d5e6f0006","name":"Normalize Meta Spend","type":"n8n-nodes-base.code","typeVersion":2,"position":[420,60],"parameters":{"jsCode":"// ---- Meta Marketing API insights -> one spend row ---------------------------\nconst rows = $input.all().flatMap(i => i.json.data || [i.json]);\n\nlet spend = 0, impressions = 0, clicks = 0, purchases = 0, purchaseValue = 0, freqW = 0;\n\nfor (const r of rows) {\n  const s = parseFloat(r.spend) || 0;\n  spend += s;\n  impressions += parseInt(r.impressions || 0, 10);\n  clicks += parseInt(r.clicks || 0, 10);\n  freqW += (parseFloat(r.frequency) || 0) * s; // spend-weighted frequency\n\n  const purch = (r.actions || []).find(a => a.action_type === 'purchase');\n  purchases += parseFloat(purch?.value || 0);\n\n  // purchase_roas comes back as [{ action_type, value }]\n  const roas = Array.isArray(r.purchase_roas) ? parseFloat(r.purchase_roas[0]?.value || 0) : parseFloat(r.purchase_roas || 0);\n  purchaseValue += roas * s;\n}\n\nreturn [{ json: {\n  channel: 'meta',\n  spend: +spend.toFixed(2),\n  impressions,\n  clicks,\n  frequency: spend ? +(freqW / spend).toFixed(2) : 0,\n  cpm: impressions ? +((spend / impressions) * 1000).toFixed(2) : 0,\n  ctr: impressions ? +((clicks / impressions) * 100).toFixed(3) : 0,\n  platform_purchases: Math.round(purchases),\n  platform_revenue: +purchaseValue.toFixed(2),\n  platform_roas: spend ? +(purchaseValue / spend).toFixed(2) : 0,\n}}];"}},{"id":"3f0a0007-31d0-4a1b-8c2e-9b7d5e6f0007","name":"Normalize Google Spend","type":"n8n-nodes-base.code","typeVersion":2,"position":[420,560],"parameters":{"jsCode":"// ---- Google Ads searchStream -> one spend row -------------------------------\nconst rows = $input.all().flatMap(i => i.json.results || i.json[0]?.results || [i.json]);\n\nlet costMicros = 0, impressions = 0, clicks = 0, conversions = 0, convValue = 0;\n\nfor (const r of rows) {\n  const m = r.metrics || r;\n  costMicros += parseInt(m.costMicros ?? m.cost_micros ?? 0, 10);\n  impressions += parseInt(m.impressions || 0, 10);\n  clicks += parseInt(m.clicks || 0, 10);\n  conversions += parseFloat(m.conversions || 0);\n  convValue += parseFloat(m.conversionsValue ?? m.conversions_value ?? 0);\n}\n\nconst spend = costMicros / 1e6;\n\nreturn [{ json: {\n  channel: 'google',\n  spend: +spend.toFixed(2),\n  impressions,\n  clicks,\n  conversions: +conversions.toFixed(2),\n  platform_revenue: +convValue.toFixed(2),\n  platform_roas: spend ? +(convValue / spend).toFixed(2) : 0,\n  cpc: clicks ? +(spend / clicks).toFixed(2) : 0,\n}}];"}},{"id":"3f0a0008-31d0-4a1b-8c2e-9b7d5e6f0008","name":"Merge Paid Channels","type":"n8n-nodes-base.merge","typeVersion":3,"position":[680,120],"parameters":{"mode":"combine","combineBy":"combineByPosition","options":{}}},{"id":"3f0a0009-31d0-4a1b-8c2e-9b7d5e6f0009","name":"Flatten Paid Spend","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[900,120],"parameters":{"assignments":{"assignments":[{"id":"p1","name":"meta_spend","value":"={{ $json.spend }}","type":"number"},{"id":"p2","name":"meta_platform_roas","value":"={{ $json.platform_roas }}","type":"number"},{"id":"p3","name":"meta_frequency","value":"={{ $json.frequency }}","type":"number"},{"id":"p4","name":"meta_cpm","value":"={{ $json.cpm }}","type":"number"},{"id":"p5","name":"google_spend","value":"={{ $json.spend_1 }}","type":"number"},{"id":"p6","name":"google_platform_roas","value":"={{ $json.platform_roas_1 }}","type":"number"}]},"options":{}}},{"id":"3f0a0010-31d0-4a1b-8c2e-9b7d5e6f0010","name":"Merge Commerce And Spend","type":"n8n-nodes-base.merge","typeVersion":3,"position":[1160,300],"parameters":{"mode":"combine","combineBy":"combineByPosition","options":{}}},{"id":"3f0a0011-31d0-4a1b-8c2e-9b7d5e6f0011","name":"Compute CAC And Blended MER","type":"n8n-nodes-base.code","typeVersion":2,"position":[1420,300],"parameters":{"jsCode":"// ---- Blend commerce + paid media -> CAC / MER / aMER -----------------------\n// Merge (combineByPosition) gives us one item carrying the Shopify summary and\n// the paid-media summary side by side.\nconst j = $input.first().json;\n\nconst metaSpend   = j.meta_spend   || 0;\nconst googleSpend = j.google_spend || 0;\nconst totalSpend  = metaSpend + googleSpend;\n\nconst netRevenue        = j.net_revenue || 0;\nconst newCustRevenue    = j.new_customer_revenue || 0;\nconst newCustomerOrders = j.new_customer_orders || 0;\nconst orders            = j.orders || 0;\n\n// --- economics ---------------------------------------------------------------\nconst GROSS_MARGIN   = 0.62;   // product margin after COGS\nconst SHIP_COST_RATE = 0.07;   // fulfilment + shipping as % of net revenue\nconst TARGET_MER     = 3.20;   // blended revenue / blended ad spend we must hold\nconst TARGET_NC_CAC  = 42.00;  // max we will pay to acquire a NEW customer\nconst REPEAT_LTV_90D = 1.35;   // a new customer is worth 1.35x their first order in 90d\n\nconst contributionMargin = netRevenue * (GROSS_MARGIN - SHIP_COST_RATE);\n\nconst mer  = totalSpend ? netRevenue / totalSpend : 0;              // blended MER\nconst aMer = totalSpend ? newCustRevenue / totalSpend : 0;          // acquisition MER\nconst ncCac = newCustomerOrders ? totalSpend / newCustomerOrders : 0; // new-customer CAC\nconst blendedCac = orders ? totalSpend / orders : 0;\nconst ltvCac = ncCac ? (j.aov || 0) * REPEAT_LTV_90D * (GROSS_MARGIN - SHIP_COST_RATE) / ncCac : 0;\nconst contributionAfterAds = contributionMargin - totalSpend;\n\n// Break-even MER = 1 / contribution margin rate. Above it we make money.\nconst cmRate = GROSS_MARGIN - SHIP_COST_RATE;\nconst breakEvenMer = +(1 / cmRate).toFixed(2);\n\n// --- health score (0-100), used to size tomorrow's move ----------------------\nconst clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));\nconst merScore  = clamp((mer / TARGET_MER) * 45, 0, 45);\nconst cacScore  = clamp((TARGET_NC_CAC / (ncCac || TARGET_NC_CAC)) * 35, 0, 35);\nconst mixScore  = clamp((j.new_customer_rate || 0) / 0.65 * 20, 0, 20);\nconst healthScore = Math.round(merScore + cacScore + mixScore);\n\nreturn [{ json: {\n  ...j,\n  meta_spend: +metaSpend.toFixed(2),\n  google_spend: +googleSpend.toFixed(2),\n  total_spend: +totalSpend.toFixed(2),\n  mer: +mer.toFixed(2),\n  acquisition_mer: +aMer.toFixed(2),\n  break_even_mer: breakEvenMer,\n  new_customer_cac: +ncCac.toFixed(2),\n  blended_cac: +blendedCac.toFixed(2),\n  ltv_cac_ratio: +ltvCac.toFixed(2),\n  contribution_margin: +contributionMargin.toFixed(2),\n  contribution_after_ads: +contributionAfterAds.toFixed(2),\n  health_score: healthScore,\n  target_mer: TARGET_MER,\n  target_nc_cac: TARGET_NC_CAC,\n}}];"}},{"id":"3f0a0012-31d0-4a1b-8c2e-9b7d5e6f0012","name":"Unit Economics Healthy?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1680,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"c1","leftValue":"={{ $json.mer }}","rightValue":"={{ $json.target_mer }}","operator":{"type":"number","operation":"gte"}},{"id":"c2","leftValue":"={{ $json.new_customer_cac }}","rightValue":"={{ $json.target_nc_cac }}","operator":{"type":"number","operation":"lte"}}]},"options":{}}},{"id":"3f0a0013-31d0-4a1b-8c2e-9b7d5e6f0013","name":"Size Scale-Up Ceiling","type":"n8n-nodes-base.code","typeVersion":2,"position":[1940,120],"parameters":{"jsCode":"// ---- Healthy day: how hard do we push tomorrow? ----------------------------\nconst j = $input.first().json;\n\n// Scale step is a function of how far above target we are AND how confident the\n// day is (volume). Thin days do not earn a big increase.\nconst merHeadroom = (j.mer - j.target_mer) / j.target_mer;      // e.g. 0.18 = 18% above target\nconst cacHeadroom = (j.target_nc_cac - j.new_customer_cac) / j.target_nc_cac;\nconst confidence  = Math.min(1, (j.new_customer_orders || 0) / 25); // 25 new custs = full trust\n\nlet stepPct = (merHeadroom * 0.55 + cacHeadroom * 0.45) * confidence;\nstepPct = Math.min(stepPct, 0.30);            // never more than +30% in one day\nif (j.health_score >= 90) stepPct = Math.max(stepPct, 0.12); // strong day: floor the push\nstepPct = Math.max(stepPct, 0.02);\n\nconst proposed = j.total_spend * (1 + stepPct);\n\n// Split the increase toward whichever channel is returning better today.\nconst metaRoas = j.meta_platform_roas || 0;\nconst googleRoas = j.google_platform_roas || 0;\nconst total = metaRoas + googleRoas;\nconst metaShare = total ? metaRoas / total : 0.6;\n\nreturn [{ json: {\n  ...j,\n  decision: 'SCALE',\n  step_pct: +(stepPct * 100).toFixed(1),\n  proposed_ceiling: +proposed.toFixed(2),\n  proposed_meta_budget: +(proposed * metaShare).toFixed(2),\n  proposed_google_budget: +(proposed * (1 - metaShare)).toFixed(2),\n  reason: `MER ${j.mer} vs target ${j.target_mer}, new-cust CAC $${j.new_customer_cac} vs $${j.target_nc_cac}, health ${j.health_score}`,\n}}];"}},{"id":"3f0a0014-31d0-4a1b-8c2e-9b7d5e6f0014","name":"Size Pullback Ceiling","type":"n8n-nodes-base.code","typeVersion":2,"position":[1940,500],"parameters":{"jsCode":"// ---- Unhealthy day: cut, or hold and let it recover? -----------------------\nconst j = $input.first().json;\n\nconst merGap = (j.target_mer - j.mer) / j.target_mer;    // how far BELOW target\nconst cacGap = (j.new_customer_cac - j.target_nc_cac) / j.target_nc_cac;\nconst belowBreakEven = j.mer < j.break_even_mer;\n\n// Cut proportional to the worse of the two gaps, doubled if we are literally\n// burning cash (MER under break-even), capped at -35% so we do not reset learning.\nlet cutPct = Math.max(merGap, cacGap) * 0.6;\nif (belowBreakEven) cutPct *= 2;\ncutPct = Math.min(Math.max(cutPct, 0), 0.35);\n\n// A shallow miss on a low-volume day is noise, not a signal - hold instead.\nconst noisyDay = (j.new_customer_orders || 0) < 8 && merGap < 0.15;\nif (noisyDay) cutPct = 0;\n\nconst proposed = j.total_spend * (1 - cutPct);\nconst metaRoas = j.meta_platform_roas || 0;\nconst googleRoas = j.google_platform_roas || 0;\n// Cut deeper on the weaker channel: give the stronger one a bigger share.\nconst total = metaRoas + googleRoas;\nconst metaShare = total ? metaRoas / total : 0.5;\n\nreturn [{ json: {\n  ...j,\n  decision: noisyDay ? 'HOLD' : (belowBreakEven ? 'CUT_HARD' : 'CUT'),\n  step_pct: +(-cutPct * 100).toFixed(1),\n  proposed_ceiling: +proposed.toFixed(2),\n  proposed_meta_budget: +(proposed * metaShare).toFixed(2),\n  proposed_google_budget: +(proposed * (1 - metaShare)).toFixed(2),\n  reason: noisyDay\n    ? `Only ${j.new_customer_orders} new customers - miss is inside noise, holding spend`\n    : `MER ${j.mer} under target ${j.target_mer}${belowBreakEven ? ' and under break-even ' + j.break_even_mer : ''}, CAC $${j.new_customer_cac}`,\n}}];"}},{"id":"3f0a0015-31d0-4a1b-8c2e-9b7d5e6f0015","name":"Apply Pacing Guardrails","type":"n8n-nodes-base.code","typeVersion":2,"position":[2200,300],"parameters":{"jsCode":"// ---- Pacing guardrails + month projection ----------------------------------\nconst j = $input.first().json;\n\nconst FLOOR = 400;      // never go below this or we lose delivery entirely\nconst CEILING = 6000;   // hard cash limit\nconst MONTH_BUDGET = 120000;\n\nlet ceiling = j.proposed_ceiling;\nconst notes = [];\n\nif (ceiling < FLOOR)   { ceiling = FLOOR;   notes.push('raised to daily floor'); }\nif (ceiling > CEILING) { ceiling = CEILING; notes.push('clipped to daily hard cap'); }\n\n// Month pacing: if holding this ceiling would blow the monthly budget, pace it down.\nconst now = new Date();\nconst daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();\nconst dayOfMonth = now.getDate();\nconst daysLeft = Math.max(1, daysInMonth - dayOfMonth + 1);\nconst spentSoFar = (j.mtd_spend != null) ? j.mtd_spend : j.total_spend * dayOfMonth; // fallback estimate\nconst remaining = MONTH_BUDGET - spentSoFar;\nconst paceCeiling = remaining / daysLeft;\n\nif (remaining <= 0) {\n  ceiling = FLOOR;\n  notes.push('monthly budget exhausted - holding at floor');\n} else if (ceiling > paceCeiling) {\n  ceiling = paceCeiling;\n  notes.push(`paced to $${paceCeiling.toFixed(0)}/day to land on the $${MONTH_BUDGET} month`);\n}\n\n// Round to something a media buyer would actually type into Ads Manager.\nceiling = Math.round(ceiling / 25) * 25;\n\nconst deltaAbs = ceiling - j.total_spend;\nconst deltaPct = j.total_spend ? (deltaAbs / j.total_spend) * 100 : 0;\nconst split = (j.proposed_meta_budget || 0) + (j.proposed_google_budget || 0) || 1;\n\nreturn [{ json: {\n  ...j,\n  final_ceiling: ceiling,\n  meta_budget: +(ceiling * ((j.proposed_meta_budget || 0) / split)).toFixed(2),\n  google_budget: +(ceiling * ((j.proposed_google_budget || 0) / split)).toFixed(2),\n  delta_vs_today: +deltaAbs.toFixed(2),\n  delta_pct: +deltaPct.toFixed(1),\n  projected_month_spend: +(spentSoFar + ceiling * daysLeft).toFixed(2),\n  month_budget: MONTH_BUDGET,\n  days_left_in_month: daysLeft,\n  guardrail_notes: notes.join('; ') || 'within all guardrails',\n  material_change: Math.abs(deltaPct) >= 10,\n  logged_at: new Date().toISOString(),\n}}];"}},{"id":"3f0a0016-31d0-4a1b-8c2e-9b7d5e6f0016","name":"Material Budget Change?","type":"n8n-nodes-base.if","typeVersion":2,"position":[2460,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"m1","leftValue":"={{ $json.material_change }}","rightValue":"","operator":{"type":"boolean","operation":"true","singleValue":true}}]},"options":{}}},{"id":"3f0a0017-31d0-4a1b-8c2e-9b7d5e6f0017","name":"Alert Buyer In Slack","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[2720,120],"parameters":{"select":"channel","channelId":{"__rl":true,"value":"#ecom-growth","mode":"name"},"text":"=*{{ $json.decision }}* — tomorrow's ceiling: *${{ $json.final_ceiling }}* ({{ $json.delta_pct }}% vs today's ${{ $json.total_spend }})\nMER {{ $json.mer }} (target {{ $json.target_mer }}, break-even {{ $json.break_even_mer }}) · new-cust CAC ${{ $json.new_customer_cac }} · aMER {{ $json.acquisition_mer }} · health {{ $json.health_score }}/100\nSplit: Meta ${{ $json.meta_budget }} / Google ${{ $json.google_budget }}\n{{ $json.reason }}\nGuardrails: {{ $json.guardrail_notes }} · month projection ${{ $json.projected_month_spend }} of ${{ $json.month_budget }}","otherOptions":{}}},{"id":"3f0a0018-31d0-4a1b-8c2e-9b7d5e6f0018","name":"Queue Minor Adjustment","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[2720,500],"parameters":{"assignments":{"assignments":[{"id":"q1","name":"decision","value":"={{ $json.decision }}","type":"string"},{"id":"q2","name":"notify","value":"no-alert (<10% move, applied silently)","type":"string"},{"id":"q3","name":"final_ceiling","value":"={{ $json.final_ceiling }}","type":"number"},{"id":"q4","name":"delta_pct","value":"={{ $json.delta_pct }}","type":"number"}]},"options":{}}},{"id":"3f0a0019-31d0-4a1b-8c2e-9b7d5e6f0019","name":"Log Decision To Sheet","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[2980,300],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"Spend Decisions"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"3f0a0020-31d0-4a1b-8c2e-9b7d5e6f0020","name":"Note Ingest","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[120,-200],"parameters":{"content":"## 1. INGEST\nYesterday's Shopify orders (deduped, test/cancelled stripped, new-vs-returning split via customer.orders_count) plus Meta insights and Google Ads cost_micros.","height":220,"width":460,"color":4}},{"id":"3f0a0021-31d0-4a1b-8c2e-9b7d5e6f0021","name":"Note Blend","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[1120,-200],"parameters":{"content":"## 2. BLEND + DECIDE\nNet revenue over total ad spend = blended MER. Spend over new customers = new-customer CAC. Health score (MER 45 / CAC 35 / new-cust mix 20) sizes tomorrow's move up or down.","height":220,"width":460,"color":5}},{"id":"3f0a0022-31d0-4a1b-8c2e-9b7d5e6f0022","name":"Note Guardrails","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[2180,-200],"parameters":{"content":"## 3. GUARDRAIL + LOG\nFloor $400, hard cap $6000, month-budget pacing, rounded to $25. Moves of 10%+ ping Slack; smaller ones apply silently. Everything lands in the decision sheet.","height":220,"width":460,"color":3}}],"connections":{"Daily Spend Review Trigger":{"main":[[{"node":"Fetch Shopify Orders","type":"main","index":0},{"node":"Fetch Meta Ad Spend","type":"main","index":0},{"node":"Fetch Google Ads Spend","type":"main","index":0}]]},"Fetch Shopify Orders":{"main":[[{"node":"Normalize Shopify Orders","type":"main","index":0}]]},"Fetch Meta Ad Spend":{"main":[[{"node":"Normalize Meta Spend","type":"main","index":0}]]},"Fetch Google Ads Spend":{"main":[[{"node":"Normalize Google Spend","type":"main","index":0}]]},"Normalize Meta Spend":{"main":[[{"node":"Merge Paid Channels","type":"main","index":0}]]},"Normalize Google Spend":{"main":[[{"node":"Merge Paid Channels","type":"main","index":1}]]},"Merge Paid Channels":{"main":[[{"node":"Flatten Paid Spend","type":"main","index":0}]]},"Normalize Shopify Orders":{"main":[[{"node":"Merge Commerce And Spend","type":"main","index":0}]]},"Flatten Paid Spend":{"main":[[{"node":"Merge Commerce And Spend","type":"main","index":1}]]},"Merge Commerce And Spend":{"main":[[{"node":"Compute CAC And Blended MER","type":"main","index":0}]]},"Compute CAC And Blended MER":{"main":[[{"node":"Unit Economics Healthy?","type":"main","index":0}]]},"Unit Economics Healthy?":{"main":[[{"node":"Size Scale-Up Ceiling","type":"main","index":0}],[{"node":"Size Pullback Ceiling","type":"main","index":0}]]},"Size Scale-Up Ceiling":{"main":[[{"node":"Apply Pacing Guardrails","type":"main","index":0}]]},"Size Pullback Ceiling":{"main":[[{"node":"Apply Pacing Guardrails","type":"main","index":0}]]},"Apply Pacing Guardrails":{"main":[[{"node":"Material Budget Change?","type":"main","index":0}]]},"Material Budget Change?":{"main":[[{"node":"Alert Buyer In Slack","type":"main","index":0}],[{"node":"Queue Minor Adjustment","type":"main","index":0}]]},"Alert Buyer In Slack":{"main":[[{"node":"Log Decision To Sheet","type":"main","index":0}]]},"Queue Minor Adjustment":{"main":[[{"node":"Log Decision To Sheet","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"},"pinData":{}}