{"name":"Shopify marketing workflow","nodes":[{"id":"9f1a2b3c-0001-4a01-8b01-000000000001","name":"Daily 06:00 Segment Run","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[-260,300],"parameters":{"rule":{"interval":[{"field":"days","daysInterval":1,"triggerAtHour":6}]}}},{"id":"9f1a2b3c-0002-4a01-8b01-000000000002","name":"Fetch Shopify Orders","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,300],"parameters":{"url":"https://SHOP_NAME.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: 540 }).toISO() }}"},{"name":"fields","value":"id,created_at,cancelled_at,financial_status,total_price,total_discounts,currency,customer,shipping_address,line_items,refunds"},{"name":"limit","value":"250"}]},"options":{}}},{"id":"9f1a2b3c-0003-4a01-8b01-000000000003","name":"Load Last Run Segments","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[0,60],"parameters":{"operation":"read","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"segment_history"},"options":{}}},{"id":"9f1a2b3c-0004-4a01-8b01-000000000004","name":"Tag Prior Snapshot","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[260,60],"parameters":{"assignments":{"assignments":[{"id":"p1","name":"customer_id","value":"={{ $json.customer_id }}","type":"string"},{"id":"p2","name":"track","value":"={{ $json.track }}","type":"string"},{"id":"p3","name":"value_score","value":"={{ $json.value_score }}","type":"number"},{"id":"p4","name":"scored_at","value":"={{ $json.scored_at }}","type":"string"},{"id":"p5","name":"source","value":"prior","type":"string"}]},"options":{}}},{"id":"9f1a2b3c-0005-4a01-8b01-000000000005","name":"Build Customer RFM Facts","type":"n8n-nodes-base.code","typeVersion":2,"position":[260,300],"parameters":{"jsCode":"// ---- Build per-customer RFM facts from raw Shopify order lines ----\n// Input: Shopify Admin API orders (financial_status, customer, total_price, line_items...)\nconst NOW = Date.now();\nconst DAY = 86400000;\nconst WINDOW_DAYS = 540; // 18 months of history is our modelling window\n\nconst orders = $input.all().map(i => i.json)\n  .filter(o => o && o.customer && o.customer.id)\n  // only money that actually landed: paid / partially_refunded count, everything else is noise\n  .filter(o => ['paid', 'partially_refunded'].includes(o.financial_status))\n  .filter(o => o.cancelled_at == null)\n  .filter(o => (NOW - new Date(o.created_at).getTime()) / DAY <= WINDOW_DAYS);\n\n// dedupe: Shopify pagination overlaps, the same order id can arrive twice\nconst seenOrders = new Set();\nconst byCustomer = new Map();\n\nfor (const o of orders) {\n  if (seenOrders.has(o.id)) continue;\n  seenOrders.add(o.id);\n\n  const cid = String(o.customer.id);\n  const gross = Number(o.total_price || 0);\n  const refunded = (o.refunds || []).reduce((s, r) =>\n    s + (r.transactions || []).reduce((t, x) => t + Number(x.amount || 0), 0), 0);\n  const net = Math.max(0, gross - refunded);\n  const ts = new Date(o.created_at).getTime();\n\n  let c = byCustomer.get(cid);\n  if (!c) {\n    c = {\n      customer_id: cid,\n      email: (o.customer.email || '').trim().toLowerCase(),\n      accepts_marketing: o.customer.accepts_marketing === true,\n      currency: o.currency || 'USD',\n      country: (o.shipping_address && o.shipping_address.country_code) || 'US',\n      orders: 0,\n      net_revenue: 0,\n      refunded_revenue: 0,\n      first_order_ts: ts,\n      last_order_ts: ts,\n      discount_orders: 0,\n      product_types: {},\n    };\n    byCustomer.set(cid, c);\n  }\n\n  c.orders += 1;\n  c.net_revenue += net;\n  c.refunded_revenue += refunded;\n  c.first_order_ts = Math.min(c.first_order_ts, ts);\n  c.last_order_ts = Math.max(c.last_order_ts, ts);\n  if (Number(o.total_discounts || 0) > 0) c.discount_orders += 1;\n\n  for (const li of (o.line_items || [])) {\n    const t = li.product_type || 'unknown';\n    c.product_types[t] = (c.product_types[t] || 0) + Number(li.quantity || 1);\n  }\n}\n\nreturn [...byCustomer.values()].map(c => {\n  const tenureDays = Math.max(1, Math.round((c.last_order_ts - c.first_order_ts) / DAY));\n  const recencyDays = Math.round((NOW - c.last_order_ts) / DAY);\n  // average gap between orders — the basis of the \"is this customer overdue?\" test\n  const avgGapDays = c.orders > 1 ? Math.round(tenureDays / (c.orders - 1)) : null;\n  const topType = Object.entries(c.product_types).sort((a, b) => b[1] - a[1])[0];\n\n  return {\n    json: {\n      ...c,\n      product_types: undefined,\n      top_product_type: topType ? topType[0] : 'unknown',\n      recency_days: recencyDays,\n      tenure_days: tenureDays,\n      avg_order_value: Number((c.net_revenue / c.orders).toFixed(2)),\n      avg_gap_days: avgGapDays,\n      discount_rate: Number((c.discount_orders / c.orders).toFixed(2)),\n      refund_rate: Number((c.refunded_revenue / Math.max(1, c.net_revenue + c.refunded_revenue)).toFixed(3)),\n      net_revenue: Number(c.net_revenue.toFixed(2)),\n      source: 'current',\n    },\n  };\n});"}},{"id":"9f1a2b3c-0006-4a01-8b01-000000000006","name":"Score RFM & Assign Track","type":"n8n-nodes-base.code","typeVersion":2,"position":[520,300],"parameters":{"jsCode":"// ---- Score R/F/M into 1-5 quintiles, then map the RFM cell to a marketing track ----\nconst rows = $input.all().map(i => i.json);\nif (!rows.length) return [];\n\n// quintile cut points computed off THIS run's population, not hardcoded — so the\n// segmentation self-calibrates as the store grows.\nfunction quantile(sorted, p) {\n  if (!sorted.length) return 0;\n  const idx = (sorted.length - 1) * p;\n  const lo = Math.floor(idx), hi = Math.ceil(idx);\n  return lo === hi ? sorted[lo] : sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);\n}\nfunction scoreAgainst(value, cuts, invert) {\n  // cuts = [p20, p40, p60, p80]; invert=true for recency (lower days = better)\n  let s = 1;\n  for (const c of cuts) if (value > c) s++;\n  return invert ? 6 - s : s;\n}\n\nconst recSorted = rows.map(r => r.recency_days).sort((a, b) => a - b);\nconst freqSorted = rows.map(r => r.orders).sort((a, b) => a - b);\nconst monSorted = rows.map(r => r.net_revenue).sort((a, b) => a - b);\nconst cuts = arr => [0.2, 0.4, 0.6, 0.8].map(p => quantile(arr, p));\nconst rCuts = cuts(recSorted), fCuts = cuts(freqSorted), mCuts = cuts(monSorted);\n\nconst AOV_ALL = monSorted.reduce((a, b) => a + b, 0) / rows.length;\n\nreturn rows.map(r => {\n  const R = scoreAgainst(r.recency_days, rCuts, true);\n  const F = scoreAgainst(r.orders, fCuts, false);\n  const M = scoreAgainst(r.net_revenue, mCuts, false);\n\n  // weighted composite: money matters most, recency next, raw frequency least\n  const value_score = Number((M * 0.5 + R * 0.3 + F * 0.2).toFixed(2));\n\n  // \"overdue\" = past 1.5x their own personal buying cadence\n  const overdueRatio = r.avg_gap_days ? r.recency_days / (r.avg_gap_days * 1.5) : null;\n  const is_overdue = overdueRatio !== null && overdueRatio >= 1;\n\n  // predicted 90-day value: personal order rate x AOV, decayed by how stale they are\n  const ordersPerDay = r.orders / Math.max(30, r.tenure_days);\n  const decay = Math.exp(-r.recency_days / 120);\n  const projected_90d_value = Number((ordersPerDay * 90 * r.avg_order_value * decay).toFixed(2));\n\n  let track, priority;\n  if (R >= 4 && F >= 4 && M >= 4)            { track = 'vip_loyalty';      priority = 1; }\n  else if (M >= 4 && R <= 2)                 { track = 'winback_high';     priority = 1; }\n  else if (F >= 4 && R <= 2)                 { track = 'winback_high';     priority = 2; }\n  else if (R >= 4 && F <= 2)                 { track = 'new_onboarding';   priority = 2; }\n  else if (R >= 3 && F >= 3)                 { track = 'grow_crosssell';   priority = 3; }\n  else if (is_overdue && M >= 3)             { track = 'at_risk_nurture';  priority = 2; }\n  else if (R <= 1 && F <= 2)                 { track = 'dormant_lowcost';  priority = 5; }\n  else                                       { track = 'standard_nurture'; priority = 4; }\n\n  // heavy discount-only, high-refund buyers are not worth paid reach\n  const margin_flag = (r.discount_rate >= 0.8 || r.refund_rate >= 0.25) ? 'low_margin' : 'ok';\n  if (margin_flag === 'low_margin' && track === 'vip_loyalty') track = 'grow_crosssell';\n\n  return {\n    json: {\n      customer_id: r.customer_id,\n      email: r.email,\n      accepts_marketing: r.accepts_marketing,\n      country: r.country,\n      r_score: R, f_score: F, m_score: M,\n      rfm_cell: `${R}${F}${M}`,\n      value_score,\n      track,\n      priority,\n      is_overdue,\n      margin_flag,\n      orders: r.orders,\n      net_revenue: r.net_revenue,\n      avg_order_value: r.avg_order_value,\n      avg_gap_days: r.avg_gap_days,\n      recency_days: r.recency_days,\n      top_product_type: r.top_product_type,\n      projected_90d_value,\n      value_vs_store_avg: Number((r.net_revenue / (AOV_ALL || 1)).toFixed(2)),\n      scored_at: new Date().toISOString(),\n      source: 'current',\n    },\n  };\n});"}},{"id":"9f1a2b3c-0007-4a01-8b01-000000000007","name":"Merge Current + Prior","type":"n8n-nodes-base.merge","typeVersion":3,"position":[780,300],"parameters":{"mode":"append","options":{}}},{"id":"9f1a2b3c-0008-4a01-8b01-000000000008","name":"Detect Segment Movement","type":"n8n-nodes-base.code","typeVersion":2,"position":[1040,300],"parameters":{"jsCode":"// ---- Diff today's tracks against the last run to detect segment movement ----\nconst all = $input.all().map(i => i.json);\nconst current = all.filter(r => r.source === 'current');\nconst prior = all.filter(r => r.source === 'prior');\n\nconst priorByCustomer = new Map();\nfor (const p of prior) {\n  const key = String(p.customer_id || '');\n  if (!key) continue;\n  // sheet may hold several historical rows per customer — keep the newest\n  const existing = priorByCustomer.get(key);\n  if (!existing || new Date(p.scored_at || 0) > new Date(existing.scored_at || 0)) {\n    priorByCustomer.set(key, p);\n  }\n}\n\n// track ladder: higher index = healthier. Used to label an upgrade vs a downgrade.\nconst LADDER = ['dormant_lowcost', 'winback_high', 'at_risk_nurture', 'standard_nurture',\n  'new_onboarding', 'grow_crosssell', 'vip_loyalty'];\n\nreturn current.map(c => {\n  const p = priorByCustomer.get(String(c.customer_id));\n  const prevTrack = p ? p.track : null;\n  const prevScore = p ? Number(p.value_score || 0) : null;\n\n  let movement = 'new_entry';\n  if (prevTrack === c.track) movement = 'held';\n  else if (prevTrack) {\n    movement = LADDER.indexOf(c.track) > LADDER.indexOf(prevTrack) ? 'upgraded' : 'downgraded';\n  }\n\n  const delta_value_score = prevScore === null ? null : Number((c.value_score - prevScore).toFixed(2));\n\n  return {\n    json: {\n      ...c,\n      previous_track: prevTrack,\n      movement,\n      delta_value_score,\n      // only push to paid/email destinations when something actually changed,\n      // otherwise we burn API quota re-uploading a static list every day\n      needs_sync: movement !== 'held',\n      is_high_value: c.track === 'vip_loyalty' || c.track === 'winback_high' || c.value_score >= 4,\n    },\n  };\n});"}},{"id":"9f1a2b3c-0009-4a01-8b01-000000000009","name":"Only Marketable & Changed","type":"n8n-nodes-base.filter","typeVersion":2,"position":[1300,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"f1","leftValue":"={{ $json.accepts_marketing }}","rightValue":"","operator":{"type":"boolean","operation":"true"}},{"id":"f2","leftValue":"={{ $json.needs_sync }}","rightValue":"","operator":{"type":"boolean","operation":"true"}},{"id":"f3","leftValue":"={{ $json.email }}","rightValue":"","operator":{"type":"string","operation":"notEmpty"}}]},"options":{}}},{"id":"9f1a2b3c-000a-4a01-8b01-00000000000a","name":"High Value Customer?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1560,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"c1","leftValue":"={{ $json.is_high_value }}","rightValue":"","operator":{"type":"boolean","operation":"true"}}]},"options":{}}},{"id":"9f1a2b3c-000b-4a01-8b01-00000000000b","name":"Sync VIP List to Klaviyo","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1820,100],"parameters":{"method":"POST","url":"https://a.klaviyo.com/api/lists/LIST_ID_VIP/relationships/profiles/","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ data: [ { type: 'profile', id: $json.customer_id, attributes: { email: $json.email, properties: { rfm_cell: $json.rfm_cell, track: $json.track, value_score: $json.value_score, projected_90d_value: $json.projected_90d_value } } } ] }) }}","options":{}}},{"id":"9f1a2b3c-000c-4a01-8b01-00000000000c","name":"Sync VIP Audience to Meta","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2080,100],"parameters":{"method":"POST","url":"https://graph.facebook.com/v21.0/CUSTOM_AUDIENCE_ID_VIP/users","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ payload: { schema: ['EMAIL','COUNTRY'], data: [[ $json.email, $json.country ]] }, session: { session_id: 8123, batch_seq: 1, last_batch_flag: true } } ) }}","options":{}}},{"id":"9f1a2b3c-000d-4a01-8b01-00000000000d","name":"Churn Risk Track?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1820,520],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"or","conditions":[{"id":"c2","leftValue":"={{ $json.track }}","rightValue":"at_risk_nurture","operator":{"type":"string","operation":"equals"}},{"id":"c3","leftValue":"={{ $json.movement }}","rightValue":"downgraded","operator":{"type":"string","operation":"equals"}}]},"options":{}}},{"id":"9f1a2b3c-000e-4a01-8b01-00000000000e","name":"Trigger Winback Flow in Klaviyo","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2080,440],"parameters":{"method":"POST","url":"https://a.klaviyo.com/api/events/","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ data: { type: 'event', attributes: { metric: { data: { type: 'metric', attributes: { name: 'Segment Downgraded' } } }, profile: { data: { type: 'profile', attributes: { email: $json.email } } }, properties: { track: $json.track, previous_track: $json.previous_track, recency_days: $json.recency_days, avg_gap_days: $json.avg_gap_days, top_product_type: $json.top_product_type } } } }) }}","options":{}}},{"id":"9f1a2b3c-000f-4a01-8b01-00000000000f","name":"Add to Standard Nurture List","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2080,660],"parameters":{"method":"POST","url":"https://a.klaviyo.com/api/lists/LIST_ID_NURTURE/relationships/profiles/","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ data: [ { type: 'profile', id: $json.customer_id, attributes: { email: $json.email, properties: { track: $json.track, movement: $json.movement, value_score: $json.value_score } } } ] }) }}","options":{}}},{"id":"9f1a2b3c-0010-4a01-8b01-000000000010","name":"Log Segment Assignments","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[2340,300],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"segment_history"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"9f1a2b3c-0011-4a01-8b01-000000000011","name":"Build Movement Report","type":"n8n-nodes-base.code","typeVersion":2,"position":[2600,300],"parameters":{"jsCode":"// ---- Roll the per-customer rows up into a segment-movement report ----\nconst rows = $input.all().map(i => i.json);\n\nconst byTrack = {};\nlet totalProjected = 0;\nfor (const r of rows) {\n  const t = r.track || 'unknown';\n  byTrack[t] = byTrack[t] || { track: t, customers: 0, revenue: 0, projected_90d: 0, upgraded: 0, downgraded: 0, new_entry: 0, held: 0 };\n  const b = byTrack[t];\n  b.customers += 1;\n  b.revenue += Number(r.net_revenue || 0);\n  b.projected_90d += Number(r.projected_90d_value || 0);\n  if (b[r.movement] !== undefined) b[r.movement] += 1;\n  totalProjected += Number(r.projected_90d_value || 0);\n}\n\nconst segments = Object.values(byTrack)\n  .map(b => ({\n    ...b,\n    revenue: Number(b.revenue.toFixed(2)),\n    projected_90d: Number(b.projected_90d.toFixed(2)),\n    avg_ltv: Number((b.revenue / b.customers).toFixed(2)),\n    net_movement: b.upgraded - b.downgraded,\n  }))\n  .sort((a, b) => b.projected_90d - a.projected_90d);\n\nconst churnRisk = rows.filter(r => r.movement === 'downgraded').length;\nconst rescued = rows.filter(r => r.movement === 'upgraded').length;\nconst revenueAtRisk = rows.filter(r => r.movement === 'downgraded')\n  .reduce((s, r) => s + Number(r.projected_90d_value || 0), 0);\n\nconst line = s => `${s.track.padEnd(18)} ${String(s.customers).padStart(6)}  $${s.revenue.toFixed(0).padStart(9)}  ${s.net_movement > 0 ? '+' : ''}${s.net_movement}`;\n\nconst summary = [\n  `Segments: ${segments.length} | Customers: ${rows.length}`,\n  `Upgraded: ${rescued} | Downgraded: ${churnRisk} | Net: ${rescued - churnRisk > 0 ? '+' : ''}${rescued - churnRisk}`,\n  `Projected 90d value: $${totalProjected.toFixed(0)} (at risk: $${revenueAtRisk.toFixed(0)})`,\n  '',\n  'TRACK              CUSTOMERS      REVENUE  NET MOVE',\n  ...segments.map(line),\n].join('\\n');\n\nconst html = `<h2>Shopify segment movement — ${new Date().toISOString().slice(0, 10)}</h2>\n<p><b>${rows.length}</b> customers across <b>${segments.length}</b> tracks. Projected 90-day value <b>$${totalProjected.toFixed(0)}</b>, of which <b>$${revenueAtRisk.toFixed(0)}</b> sits in downgraded customers.</p>\n<ul>${segments.map(s => `<li><b>${s.track}</b> — ${s.customers} customers, $${s.revenue.toFixed(0)} lifetime, avg LTV $${s.avg_ltv}, net movement ${s.net_movement > 0 ? '+' : ''}${s.net_movement}</li>`).join('')}</ul>`;\n\nreturn [{ json: { summary, html, segments, total_customers: rows.length, upgraded: rescued, downgraded: churnRisk, revenue_at_risk: Number(revenueAtRisk.toFixed(2)) } }];"}},{"id":"9f1a2b3c-0012-4a01-8b01-000000000012","name":"Post Segment Digest to Slack","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[2860,160],"parameters":{"select":"channel","channelId":{"__rl":true,"value":"#retention","mode":"name"},"text":"=📊 Shopify segment movement\n```{{ $json.summary }}```","otherOptions":{}}},{"id":"9f1a2b3c-0013-4a01-8b01-000000000013","name":"Email Segment Report","type":"n8n-nodes-base.gmail","typeVersion":2.1,"position":[2860,440],"parameters":{"sendTo":"retention@brand.com","subject":"=Segment movement {{ $now.format('yyyy-LL-dd') }} — {{ $json.upgraded }} up / {{ $json.downgraded }} down","message":"={{ $json.html }}","options":{}}},{"id":"9f1a2b3c-0021-4a01-8b01-000000000021","name":"Note Ingest","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[-300,-180],"parameters":{"content":"## 1. INGEST\nPull 18 months of paid Shopify orders and last night's segment snapshot from the history sheet. Orders are deduped by id and net of refunds before any scoring happens.","height":220,"width":520,"color":4}},{"id":"9f1a2b3c-0022-4a01-8b01-000000000022","name":"Note Score","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[240,-180],"parameters":{"content":"## 2. SCORE + DIFF\nR/F/M scored into 1-5 quintiles calculated from this run's own population, blended into value_score (M 50% / R 30% / F 20%), mapped to a marketing track, then diffed against yesterday to label upgraded / downgraded / held.","height":220,"width":740,"color":5}},{"id":"9f1a2b3c-0023-4a01-8b01-000000000023","name":"Note Activate","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[1540,-180],"parameters":{"content":"## 3. ACTIVATE + REPORT\nOnly marketable customers whose track actually changed get synced (saves API quota). VIPs go to a Klaviyo list + Meta custom audience; downgraders fire a winback event; everyone else lands in nurture. All rows are logged back to the history sheet, then rolled up into the Slack + email digest.","height":240,"width":900,"color":3}}],"connections":{"Daily 06:00 Segment Run":{"main":[[{"node":"Fetch Shopify Orders","type":"main","index":0},{"node":"Load Last Run Segments","type":"main","index":0}]]},"Fetch Shopify Orders":{"main":[[{"node":"Build Customer RFM Facts","type":"main","index":0}]]},"Build Customer RFM Facts":{"main":[[{"node":"Score RFM & Assign Track","type":"main","index":0}]]},"Score RFM & Assign Track":{"main":[[{"node":"Merge Current + Prior","type":"main","index":0}]]},"Load Last Run Segments":{"main":[[{"node":"Tag Prior Snapshot","type":"main","index":0}]]},"Tag Prior Snapshot":{"main":[[{"node":"Merge Current + Prior","type":"main","index":1}]]},"Merge Current + Prior":{"main":[[{"node":"Detect Segment Movement","type":"main","index":0}]]},"Detect Segment Movement":{"main":[[{"node":"Only Marketable & Changed","type":"main","index":0}]]},"Only Marketable & Changed":{"main":[[{"node":"High Value Customer?","type":"main","index":0}]]},"High Value Customer?":{"main":[[{"node":"Sync VIP List to Klaviyo","type":"main","index":0}],[{"node":"Churn Risk Track?","type":"main","index":0}]]},"Sync VIP List to Klaviyo":{"main":[[{"node":"Sync VIP Audience to Meta","type":"main","index":0}]]},"Sync VIP Audience to Meta":{"main":[[{"node":"Log Segment Assignments","type":"main","index":0}]]},"Churn Risk Track?":{"main":[[{"node":"Trigger Winback Flow in Klaviyo","type":"main","index":0}],[{"node":"Add to Standard Nurture List","type":"main","index":0}]]},"Trigger Winback Flow in Klaviyo":{"main":[[{"node":"Log Segment Assignments","type":"main","index":0}]]},"Add to Standard Nurture List":{"main":[[{"node":"Log Segment Assignments","type":"main","index":0}]]},"Log Segment Assignments":{"main":[[{"node":"Build Movement Report","type":"main","index":0}]]},"Build Movement Report":{"main":[[{"node":"Post Segment Digest to Slack","type":"main","index":0},{"node":"Email Segment Report","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"},"pinData":{}}