{"name":"ROAS tracking workflow","nodes":[{"id":"9c1f0001-47a0-4b21-8f10-000000000001","name":"Daily ROAS Snapshot Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[-320,300],"parameters":{"rule":{"interval":[{"field":"days","daysInterval":1}]}}},{"id":"9c1f0002-47a0-4b21-8f10-000000000002","name":"Set Reporting Window","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[-100,300],"parameters":{"assignments":{"assignments":[{"id":"w1","name":"report_date","value":"={{ $now.minus({ days: 1 }).format('yyyy-LL-dd') }}","type":"string"},{"id":"w2","name":"meta_account","value":"act_ID","type":"string"},{"id":"w3","name":"google_customer_id","value":"CUSTOMER_ID","type":"string"},{"id":"w4","name":"shop_domain","value":"brand.myshopify.com","type":"string"}]},"options":{}}},{"id":"9c1f0003-47a0-4b21-8f10-000000000003","name":"Fetch Meta Spend And ROAS","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[140,100],"parameters":{"url":"=https://graph.facebook.com/v21.0/{{ $json.meta_account }}/insights","sendQuery":true,"queryParameters":{"parameters":[{"name":"level","value":"campaign"},{"name":"fields","value":"campaign_name,spend,impressions,clicks,frequency,purchase_roas,action_values,actions"},{"name":"time_range","value":"={{ JSON.stringify({ since: $json.report_date, until: $json.report_date }) }}"},{"name":"action_attribution_windows","value":"7d_click,1d_view"},{"name":"limit","value":"200"}]},"options":{}}},{"id":"9c1f0004-47a0-4b21-8f10-000000000004","name":"Fetch Google Ads Cost","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[140,300],"parameters":{"method":"POST","url":"=https://googleads.googleapis.com/v18/customers/{{ $json.google_customer_id }}/googleAds:search","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ query: \"select campaign.name, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.impressions, metrics.clicks from campaign where segments.date = '\" + $json.report_date + \"'\" }) }}","options":{}}},{"id":"9c1f0005-47a0-4b21-8f10-000000000005","name":"Fetch TikTok Ads Spend","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[140,500],"parameters":{"url":"https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/","sendQuery":true,"queryParameters":{"parameters":[{"name":"advertiser_id","value":"ADVERTISER_ID"},{"name":"report_type","value":"BASIC"},{"name":"data_level","value":"AUCTION_CAMPAIGN"},{"name":"dimensions","value":"[\"campaign_id\"]"},{"name":"metrics","value":"[\"campaign_name\",\"spend\",\"impressions\",\"clicks\",\"complete_payment\",\"total_purchase_value\",\"complete_payment_roas\"]"},{"name":"start_date","value":"={{ $json.report_date }}"},{"name":"end_date","value":"={{ $json.report_date }}"}]},"options":{}}},{"id":"9c1f0006-47a0-4b21-8f10-000000000006","name":"Merge Ad Platform Data","type":"n8n-nodes-base.merge","typeVersion":3,"position":[360,300],"parameters":{"mode":"append","numberInputs":3,"options":{}}},{"id":"9c1f0007-47a0-4b21-8f10-000000000007","name":"Normalize Platform Spend","type":"n8n-nodes-base.code","typeVersion":2,"position":[580,300],"parameters":{"jsCode":"// ---------------------------------------------------------------\n// Normalise Meta / Google / TikTok reporting into ONE row shape.\n// Each platform names the same numbers differently, so we map them\n// onto: { platform, campaign, spend, platform_revenue, platform_roas,\n//         impressions, clicks, conversions }\n// ---------------------------------------------------------------\nconst round = (v, d = 2) => Math.round((Number(v) || 0) * 10 ** d) / 10 ** d;\n\n// Meta returns purchase_roas / action_values as arrays of {action_type, value}\nconst pickAction = (arr, type) => {\n  if (!Array.isArray(arr)) return 0;\n  const hit = arr.find(a => a.action_type === type) || arr[0];\n  return hit ? Number(hit.value) || 0 : 0;\n};\n\nconst rows = [];\n\nfor (const item of $input.all()) {\n  const j = item.json || {};\n\n  // ---- Meta Marketing API: { data: [ { spend, purchase_roas: [...] } ] }\n  if (Array.isArray(j.data) && j.data.length && j.data[0].campaign_name !== undefined) {\n    for (const d of j.data) {\n      const spend = Number(d.spend) || 0;\n      const roas = pickAction(d.purchase_roas, 'omni_purchase');\n      const revenue = pickAction(d.action_values, 'omni_purchase') || spend * roas;\n      rows.push({\n        platform: 'meta',\n        campaign: d.campaign_name,\n        spend: round(spend),\n        platform_revenue: round(revenue),\n        platform_roas: round(spend ? revenue / spend : 0),\n        impressions: Number(d.impressions) || 0,\n        clicks: Number(d.clicks) || 0,\n        conversions: pickAction(d.actions, 'omni_purchase'),\n        frequency: round(Number(d.frequency) || 0),\n      });\n    }\n    continue;\n  }\n\n  // ---- Google Ads searchStream: cost_micros + conversions_value\n  if (Array.isArray(j.results)) {\n    for (const r of j.results) {\n      const m = r.metrics || {};\n      const spend = (Number(m.cost_micros) || 0) / 1e6;\n      const revenue = Number(m.conversions_value) || 0;\n      rows.push({\n        platform: 'google',\n        campaign: (r.campaign && r.campaign.name) || 'unknown',\n        spend: round(spend),\n        platform_revenue: round(revenue),\n        platform_roas: round(spend ? revenue / spend : 0),\n        impressions: Number(m.impressions) || 0,\n        clicks: Number(m.clicks) || 0,\n        conversions: round(Number(m.conversions) || 0),\n        frequency: 0,\n      });\n    }\n    continue;\n  }\n\n  // ---- TikTok Ads: { data: { list: [ { metrics: {...} } ] } }\n  if (j.data && Array.isArray(j.data.list)) {\n    for (const r of j.data.list) {\n      const m = r.metrics || {};\n      const spend = Number(m.spend) || 0;\n      // TikTok reports value directly; fall back to spend x ROAS when absent.\n      const revenue = Number(m.total_purchase_value) || spend * (Number(m.complete_payment_roas) || 0);\n      rows.push({\n        platform: 'tiktok',\n        campaign: m.campaign_name || (r.dimensions && r.dimensions.campaign_id) || 'unknown',\n        spend: round(spend),\n        platform_revenue: round(revenue),\n        platform_roas: round(spend ? revenue / spend : 0),\n        impressions: Number(m.impressions) || 0,\n        clicks: Number(m.clicks) || 0,\n        conversions: Number(m.complete_payment) || 0,\n        frequency: round(Number(m.frequency) || 0),\n      });\n    }\n    continue;\n  }\n}\n\n// Drop zero-spend rows - they only dilute the blended maths.\nconst live = rows.filter(r => r.spend > 0.5);\n\n// Roll campaigns up per platform so downstream stays small.\nconst byPlatform = {};\nfor (const r of live) {\n  const p = (byPlatform[r.platform] = byPlatform[r.platform] || {\n    platform: r.platform, campaigns: 0, spend: 0, platform_revenue: 0,\n    impressions: 0, clicks: 0, conversions: 0,\n  });\n  p.campaigns += 1;\n  p.spend += r.spend;\n  p.platform_revenue += r.platform_revenue;\n  p.impressions += r.impressions;\n  p.clicks += r.clicks;\n  p.conversions += r.conversions;\n}\n\nreturn Object.values(byPlatform).map(p => ({\n  json: {\n    ...p,\n    spend: round(p.spend),\n    platform_revenue: round(p.platform_revenue),\n    platform_roas: round(p.spend ? p.platform_revenue / p.spend : 0),\n    cpa: round(p.conversions ? p.spend / p.conversions : 0),\n    ctr: round(p.impressions ? (p.clicks / p.impressions) * 100 : 0, 3),\n    date: $('Set Reporting Window').first().json.report_date,\n  },\n}));"}},{"id":"9c1f0008-47a0-4b21-8f10-000000000008","name":"Fetch Shopify Order Revenue","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[800,300],"parameters":{"url":"=https://{{ $('Set Reporting Window').first().json.shop_domain }}/admin/api/2024-10/orders.json","sendQuery":true,"queryParameters":{"parameters":[{"name":"status","value":"any"},{"name":"created_at_min","value":"={{ $('Set Reporting Window').first().json.report_date }}T00:00:00Z"},{"name":"created_at_max","value":"={{ $('Set Reporting Window').first().json.report_date }}T23:59:59Z"},{"name":"fields","value":"id,created_at,total_price,current_total_price,total_shipping_price_set,financial_status,cancelled_at,test"},{"name":"limit","value":"250"}]},"options":{}}},{"id":"9c1f0009-47a0-4b21-8f10-000000000009","name":"Compute Blended ROAS And Gap","type":"n8n-nodes-base.code","typeVersion":2,"position":[1020,300],"parameters":{"jsCode":"// ---------------------------------------------------------------\n// Blend the store's REAL revenue against total ad spend, then\n// measure how much of the platforms' self-reported revenue is\n// double counted (the attribution gap).\n// ---------------------------------------------------------------\nconst round = (v, d = 2) => Math.round((Number(v) || 0) * 10 ** d) / 10 ** d;\n\nconst platforms = $('Normalize Platform Spend').all().map(i => i.json);\n\n// --- Shopify orders: dedupe by id, drop test / cancelled / refunded\nconst seen = new Set();\nlet storeRevenue = 0;\nlet orderCount = 0;\nlet refunded = 0;\n\nfor (const item of $input.all()) {\n  const orders = (item.json && item.json.orders) || [];\n  for (const o of orders) {\n    if (seen.has(o.id)) continue;      // paginated pages can overlap\n    seen.add(o.id);\n    if (o.test) continue;\n    if (o.cancelled_at) continue;\n    if (o.financial_status === 'refunded' || o.financial_status === 'voided') {\n      refunded += Number(o.total_price) || 0;\n      continue;\n    }\n    const net = Number(o.current_total_price ?? o.total_price) || 0;\n    const shipping = Number(\n      ((o.total_shipping_price_set || {}).shop_money || {}).amount || 0\n    );\n    storeRevenue += net - shipping;    // ROAS on product revenue, not postage\n    orderCount += 1;\n  }\n}\n\nstoreRevenue = round(storeRevenue);\n\nconst totalSpend = round(platforms.reduce((s, p) => s + p.spend, 0));\nconst reportedRevenue = round(platforms.reduce((s, p) => s + p.platform_revenue, 0));\n\nconst blendedRoas = round(totalSpend ? storeRevenue / totalSpend : 0);\nconst reportedRoas = round(totalSpend ? reportedRevenue / totalSpend : 0);\n\n// Attribution gap = how much MORE the platforms claim than the store saw.\nconst gapValue = round(reportedRevenue - storeRevenue);\nconst gapPct = round(storeRevenue ? (gapValue / storeRevenue) * 100 : 0, 1);\n\n// Per-platform share of spend and of claimed revenue - a platform\n// claiming far more revenue share than spend share is the likely\n// over-claimer once the gap gets big.\nconst breakdown = platforms.map(p => {\n  const spendShare = totalSpend ? p.spend / totalSpend : 0;\n  const claimShare = reportedRevenue ? p.platform_revenue / reportedRevenue : 0;\n  return {\n    platform: p.platform,\n    spend: p.spend,\n    platform_roas: p.platform_roas,\n    spend_share_pct: round(spendShare * 100, 1),\n    claim_share_pct: round(claimShare * 100, 1),\n    over_claim_index: round(spendShare ? claimShare / spendShare : 0),\n  };\n});\n\nconst suspect = breakdown\n  .slice()\n  .sort((a, b) => b.over_claim_index - a.over_claim_index)[0];\n\n// MER (marketing efficiency ratio) is just blended ROAS stated as a %\nconst mer = round(storeRevenue ? (totalSpend / storeRevenue) * 100 : 0, 1);\n\nreturn [{\n  json: {\n    report_date: $('Set Reporting Window').first().json.report_date,\n    total_spend: totalSpend,\n    store_revenue: storeRevenue,\n    refunded_revenue: round(refunded),\n    orders: orderCount,\n    aov: round(orderCount ? storeRevenue / orderCount : 0),\n    reported_revenue: reportedRevenue,\n    reported_roas: reportedRoas,\n    blended_roas: blendedRoas,\n    attribution_gap_value: gapValue,\n    attribution_gap_pct: gapPct,\n    mer_pct: mer,\n    breakdown,\n    likely_over_claimer: suspect ? suspect.platform : 'n/a',\n    over_claim_index: suspect ? suspect.over_claim_index : 0,\n  },\n}];"}},{"id":"9c1f0010-47a0-4b21-8f10-000000000010","name":"Read Last 7 Snapshots","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[1240,300],"parameters":{"operation":"read","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"roas_snapshots"},"options":{}}},{"id":"9c1f0011-47a0-4b21-8f10-000000000011","name":"Score Drop Against Baseline","type":"n8n-nodes-base.code","typeVersion":2,"position":[1460,300],"parameters":{"jsCode":"// ---------------------------------------------------------------\n// Compare today's blended ROAS to the trailing 7-day baseline held\n// in the snapshot sheet, score the drop, and project month pacing.\n// ---------------------------------------------------------------\nconst round = (v, d = 2) => Math.round((Number(v) || 0) * 10 ** d) / 10 ** d;\n\nconst today = $('Compute Blended ROAS And Gap').first().json;\nconst TARGET_ROAS = 2.2;          // account break-even + margin\nconst DROP_PCT_ALERT = 15;        // % below baseline that matters\nconst Z_ALERT = 1.5;              // volatility-adjusted trigger\nconst GAP_ALERT_PCT = 30;         // platforms over-claiming by 30%+\n\n// Sheet rows come back oldest-first; keep the last 7 complete days\n// and never let today's own row (a re-run) into its own baseline.\nconst history = $input.all()\n  .map(i => i.json)\n  .filter(r => r && r.report_date && r.report_date !== today.report_date)\n  .map(r => ({\n    report_date: r.report_date,\n    blended_roas: Number(r.blended_roas) || 0,\n    total_spend: Number(r.total_spend) || 0,\n    store_revenue: Number(r.store_revenue) || 0,\n  }))\n  .filter(r => r.total_spend > 0)\n  .sort((a, b) => (a.report_date < b.report_date ? -1 : 1));\n\nconst window = history.slice(-7);\nconst vals = window.map(r => r.blended_roas);\n\nconst mean = vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : today.blended_roas;\nconst variance = vals.length > 1\n  ? vals.reduce((s, v) => s + (v - mean) ** 2, 0) / (vals.length - 1)\n  : 0;\nconst sd = Math.sqrt(variance);\n\nconst deltaPct = mean ? round(((today.blended_roas - mean) / mean) * 100, 1) : 0;\nconst z = sd > 0.01 ? round((today.blended_roas - mean) / sd, 2) : 0;\n\n// Severity: worst condition wins.\nlet severity = 'ok';\nif (today.blended_roas < TARGET_ROAS * 0.7) severity = 'critical';\nelse if (deltaPct <= -DROP_PCT_ALERT && z <= -Z_ALERT) severity = 'critical';\nelse if (deltaPct <= -DROP_PCT_ALERT) severity = 'warning';\nelse if (today.blended_roas < TARGET_ROAS) severity = 'warning';\nelse if (deltaPct >= 15) severity = 'improving';\n\n// Month pacing: extrapolate spend + revenue at the current daily rate.\nconst day = Number(String(today.report_date).slice(8, 10)) || 1;\nconst y = Number(String(today.report_date).slice(0, 4));\nconst m = Number(String(today.report_date).slice(5, 7));\nconst daysInMonth = new Date(y, m, 0).getDate();\nconst mtdSpend = round(window.reduce((s, r) => s + r.total_spend, 0) + today.total_spend);\nconst mtdRevenue = round(window.reduce((s, r) => s + r.store_revenue, 0) + today.store_revenue);\nconst dailySpend = round(mtdSpend / Math.max(1, Math.min(day, window.length + 1)));\nconst projectedSpend = round(dailySpend * daysInMonth);\nconst projectedRevenue = round(projectedSpend * today.blended_roas);\n\nconst gapWidening = today.attribution_gap_pct >= GAP_ALERT_PCT;\n\nreturn [{\n  json: {\n    ...today,\n    baseline_roas: round(mean),\n    baseline_sd: round(sd),\n    baseline_days: vals.length,\n    delta_vs_baseline_pct: deltaPct,\n    z_score: z,\n    target_roas: TARGET_ROAS,\n    severity,\n    alert: severity === 'critical' || severity === 'warning' || gapWidening,\n    gap_widening: gapWidening,\n    mtd_spend: mtdSpend,\n    mtd_revenue: mtdRevenue,\n    projected_month_spend: projectedSpend,\n    projected_month_revenue: projectedRevenue,\n    headline: severity === 'ok' || severity === 'improving'\n      ? `Blended ROAS ${today.blended_roas} (${deltaPct >= 0 ? '+' : ''}${deltaPct}% vs 7d)`\n      : `Blended ROAS ${today.blended_roas} vs baseline ${round(mean)} (${deltaPct}%)`,\n  },\n}];"}},{"id":"9c1f0012-47a0-4b21-8f10-000000000012","name":"Append Daily Snapshot","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[1680,300],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"roas_snapshots"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"9c1f0013-47a0-4b21-8f10-000000000013","name":"ROAS Drop Detected?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1900,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"c1","leftValue":"={{ $json.alert }}","rightValue":"","operator":{"type":"boolean","operation":"true","singleValue":true}}]},"options":{}}},{"id":"9c1f0014-47a0-4b21-8f10-000000000014","name":"Build Alert Payload","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[2120,140],"parameters":{"assignments":{"assignments":[{"id":"a1","name":"severity","value":"={{ $json.severity }}","type":"string"},{"id":"a2","name":"blended_roas","value":"={{ $json.blended_roas }}","type":"number"},{"id":"a3","name":"reported_roas","value":"={{ $json.reported_roas }}","type":"number"},{"id":"a4","name":"attribution_gap_pct","value":"={{ $json.attribution_gap_pct }}","type":"number"},{"id":"a5","name":"gap_widening","value":"={{ $json.gap_widening }}","type":"boolean"},{"id":"a6","name":"summary","value":"={{ $json.headline }} | spend {{ $json.total_spend }} | store rev {{ $json.store_revenue }} | gap {{ $json.attribution_gap_pct }}% ({{ $json.likely_over_claimer }})","type":"string"}]},"options":{}}},{"id":"9c1f0015-47a0-4b21-8f10-000000000015","name":"Log Healthy Day To Warehouse","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[2120,480],"parameters":{"operation":"executeQuery","query":"insert into roas_daily (report_date, total_spend, store_revenue, blended_roas, reported_roas, attribution_gap_pct, severity) values ($1,$2,$3,$4,$5,$6,$7) on conflict (report_date) do update set total_spend = excluded.total_spend, store_revenue = excluded.store_revenue, blended_roas = excluded.blended_roas","options":{"queryReplacement":"={{ $json.report_date }},{{ $json.total_spend }},{{ $json.store_revenue }},{{ $json.blended_roas }},{{ $json.reported_roas }},{{ $json.attribution_gap_pct }},{{ $json.severity }}"}}},{"id":"9c1f0016-47a0-4b21-8f10-000000000016","name":"Attribution Gap Driven?","type":"n8n-nodes-base.if","typeVersion":2,"position":[2340,140],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"g1","leftValue":"={{ $json.attribution_gap_pct }}","rightValue":30,"operator":{"type":"number","operation":"gte"}}]},"options":{}}},{"id":"9c1f0017-47a0-4b21-8f10-000000000017","name":"Email Attribution Review","type":"n8n-nodes-base.gmail","typeVersion":2.1,"position":[2560,20],"parameters":{"sendTo":"growth@brand.com","subject":"=Attribution gap {{ $json.attribution_gap_pct }}% - {{ $now.format('yyyy-LL-dd') }}","message":"=<h3>Platforms are claiming more than the store banked</h3><p>{{ $json.summary }}</p><p>Blended ROAS <b>{{ $json.blended_roas }}</b> vs platform-reported <b>{{ $json.reported_roas }}</b>. Treat platform ROAS as directional until the gap closes; scale on blended only.</p>","options":{}}},{"id":"9c1f0018-47a0-4b21-8f10-000000000018","name":"Post ROAS Drop Alert","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[2560,260],"parameters":{"select":"channel","channelId":{"__rl":true,"value":"#ads-alerts","mode":"name"},"text":"=🚨 {{ $json.severity.toUpperCase() }} ROAS drop — {{ $json.summary }}","otherOptions":{}}},{"id":"9c1f0019-47a0-4b21-8f10-000000000019","name":"Section: Collect","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[-360,-140],"parameters":{"content":"## 1. COLLECT\nPull yesterday's spend + platform-reported revenue from Meta, Google Ads and TikTok, then the store's real order revenue from Shopify.","height":340,"width":620,"color":4}},{"id":"9c1f0020-47a0-4b21-8f10-000000000020","name":"Section: Blend","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[560,-140],"parameters":{"content":"## 2. BLEND & SCORE\nNormalise every platform to one row shape, dedupe orders, then compute blended ROAS, platform-reported ROAS and the attribution gap. Scored against the trailing 7-day baseline (delta % + z-score) and logged to the snapshot sheet.","height":340,"width":860,"color":5}},{"id":"9c1f0021-47a0-4b21-8f10-000000000021","name":"Section: Act","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[1860,-140],"parameters":{"content":"## 3. ACT\nHealthy days go straight to the warehouse. Alert days split: a real efficiency drop pings Slack, a gap-driven drop emails growth to re-check attribution before anyone cuts budget.","height":340,"width":620,"color":3}}],"connections":{"Daily ROAS Snapshot Trigger":{"main":[[{"node":"Set Reporting Window","type":"main","index":0}]]},"Set Reporting Window":{"main":[[{"node":"Fetch Meta Spend And ROAS","type":"main","index":0},{"node":"Fetch Google Ads Cost","type":"main","index":0},{"node":"Fetch TikTok Ads Spend","type":"main","index":0}]]},"Fetch Meta Spend And ROAS":{"main":[[{"node":"Merge Ad Platform Data","type":"main","index":0}]]},"Fetch Google Ads Cost":{"main":[[{"node":"Merge Ad Platform Data","type":"main","index":1}]]},"Fetch TikTok Ads Spend":{"main":[[{"node":"Merge Ad Platform Data","type":"main","index":2}]]},"Merge Ad Platform Data":{"main":[[{"node":"Normalize Platform Spend","type":"main","index":0}]]},"Normalize Platform Spend":{"main":[[{"node":"Fetch Shopify Order Revenue","type":"main","index":0}]]},"Fetch Shopify Order Revenue":{"main":[[{"node":"Compute Blended ROAS And Gap","type":"main","index":0}]]},"Compute Blended ROAS And Gap":{"main":[[{"node":"Read Last 7 Snapshots","type":"main","index":0}]]},"Read Last 7 Snapshots":{"main":[[{"node":"Score Drop Against Baseline","type":"main","index":0}]]},"Score Drop Against Baseline":{"main":[[{"node":"Append Daily Snapshot","type":"main","index":0}]]},"Append Daily Snapshot":{"main":[[{"node":"ROAS Drop Detected?","type":"main","index":0}]]},"ROAS Drop Detected?":{"main":[[{"node":"Build Alert Payload","type":"main","index":0}],[{"node":"Log Healthy Day To Warehouse","type":"main","index":0}]]},"Build Alert Payload":{"main":[[{"node":"Attribution Gap Driven?","type":"main","index":0}]]},"Attribution Gap Driven?":{"main":[[{"node":"Email Attribution Review","type":"main","index":0}],[{"node":"Post ROAS Drop Alert","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"},"pinData":{}}