{
  "name": "WF-NW-03, Followup SMS Only (touch 2-4)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9-20 * * *"
            }
          ]
        }
      },
      "id": "trg-9et",
      "name": "Every Hour 9 AM - 8 PM ET",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -208,
        -304
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 10
            }
          ]
        }
      },
      "id": "trg-10et",
      "name": "Send 10 AM ET (CT zone)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -208,
        -96
      ],
      "disabled": true
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 11
            }
          ]
        }
      },
      "id": "trg-11et",
      "name": "Send 11 AM ET (MT zone)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -208,
        112
      ],
      "disabled": true
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 12
            }
          ]
        }
      },
      "id": "trg-12et",
      "name": "Send 12 PM ET (PT/AZ zone)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -208,
        304
      ],
      "disabled": true
    },
    {
      "parameters": {
        "url": "https://YOUR_CONVEX_DEPLOYMENT/sms-batch?limit=100",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 60000
        }
      },
      "id": "http-get-sms-batch",
      "name": "GET Convex /sms-batch",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        112,
        -304
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000
    },
    {
      "parameters": {
        "jsCode": "// WF-NW-03 gate + STICKY sender. FOLLOWUPS ONLY (touch 2,3,4).\n// Original v1 templates (reverted from v2.1).\nconst SEND_REAL = true;\nconst MAX_TOUCHES = 4;\nconst BATCH_LIMIT = 5;\nconst RECENT_SEND_WINDOW_MS = 5 * 60 * 1000;\nconst GAP_DAYS = [0, 1, 3, 2];\nconst QUIET_START = 8, QUIET_END = 21;\nconst NPA_TZ = {\"405\":\"America/Chicago\",\"410\":\"America/New_York\",\"901\":\"America/Chicago\",\"520\":\"America/Phoenix\",\"502\":\"America/New_York\",\"915\":\"America/Chicago\",\"989\":\"America/New_York\",\"313\":\"America/New_York\",\"480\":\"America/Phoenix\",\"720\":\"America/Denver\",\"702\":\"America/Los_Angeles\",\"303\":\"America/Denver\",\"404\":\"America/New_York\",\"678\":\"America/New_York\",\"414\":\"America/Chicago\",\"612\":\"America/Chicago\",\"443\":\"America/New_York\",\"763\":\"America/Chicago\",\"812\":\"America/Chicago\",\"256\":\"America/Chicago\",\"617\":\"America/New_York\",\"623\":\"America/Phoenix\",\"539\":\"America/Chicago\",\"505\":\"America/Los_Angeles\",\"330\":\"America/New_York\",\"903\":\"America/New_York\",\"918\":\"America/Chicago\",\"205\":\"America/Chicago\",\"615\":\"America/Chicago\",\"704\":\"America/Denver\",\"983\":\"America/New_York\",\"770\":\"America/New_York\",\"618\":\"America/Chicago\",\"857\":\"America/New_York\",\"806\":\"America/Chicago\",\"508\":\"America/New_York\",\"239\":\"America/New_York\",\"952\":\"America/Chicago\",\"229\":\"America/Chicago\",\"301\":\"America/New_York\",\"402\":\"America/New_York\",\"725\":\"America/Los_Angeles\",\"765\":\"America/Chicago\",\"223\":\"America/Chicago\",\"662\":\"America/New_York\",\"706\":\"America/New_York\",\"240\":\"America/New_York\",\"347\":\"America/New_York\",\"212\":\"America/New_York\",\"646\":\"America/New_York\",\"516\":\"America/New_York\",\"332\":\"America/New_York\",\"929\":\"America/New_York\",\"917\":\"America/New_York\",\"718\":\"America/New_York\",\"252\":\"America/New_York\",\"507\":\"America/Chicago\",\"512\":\"America/Chicago\",\"501\":\"America/Chicago\",\"737\":\"America/Chicago\",\"517\":\"America/New_York\",\"614\":\"America/New_York\",\"406\":\"America/New_York\",\"979\":\"America/New_York\",\"828\":\"America/New_York\",\"920\":\"America/New_York\",\"609\":\"America/New_York\",\"380\":\"America/New_York\",\"323\":\"America/New_York\",\"972\":\"America/Chicago\",\"214\":\"America/Chicago\",\"570\":\"America/Chicago\",\"469\":\"America/Chicago\",\"503\":\"America/Chicago\",\"817\":\"America/Chicago\",\"945\":\"America/Chicago\",\"682\":\"America/Chicago\",\"317\":\"America/New_York\",\"424\":\"America/New_York\",\"215\":\"America/New_York\",\"315\":\"America/New_York\",\"267\":\"America/New_York\",\"610\":\"America/New_York\",\"272\":\"America/New_York\",\"602\":\"America/Phoenix\",\"760\":\"America/Phoenix\",\"970\":\"America/Phoenix\",\"210\":\"America/Chicago\",\"726\":\"America/Chicago\",\"346\":\"America/Chicago\",\"858\":\"America/Chicago\",\"281\":\"America/Chicago\",\"832\":\"America/Chicago\",\"713\":\"America/Chicago\",\"254\":\"America/Chicago\"};\nfunction npaTz(p){ const d=String(p||'').replace(/\\D/g,'').slice(-10); return NPA_TZ[d.slice(0,3)] || 'America/Los_Angeles'; }\nfunction localHour(tz){ try { return Number(new Intl.DateTimeFormat('en-US',{hour:'numeric',hour12:false,timeZone:tz||'America/New_York'}).format(new Date())); } catch(e){ return new Date().getHours(); } }\nfunction e164(p){ const d=String(p||'').replace(/\\D/g,''); return d.length===10?'+1'+d:(d.length===11&&d[0]==='1')?'+'+d:null; }\n\nconst FOLLOWUPS = {\n  1: \"Hey, still want me to send that site I made for {biz}? It's ready whenever.\",\n  2: \"last try I promise, want me to drop the link for {biz}? Takes 10 sec to glance at.\",\n  3: \"alright I'll assume you're booked solid, going with someone else. cya later alligator \ud83d\udc0a\"\n};\n\nconst now = Date.now();\nconst out = [];\nfor (const item of $input.all()) {\n  const l = item.json;\n  if (l.status && l.status !== 'in_close') continue;\n  const n = l.sms_count == null ? 0 : l.sms_count;\n  if (n < 1) continue;\n  if (n >= MAX_TOUCHES) continue;\n  const gap = GAP_DAYS[n] == null ? 2 : GAP_DAYS[n];\n  if (l.last_sms_at) {\n    const msSinceLast = now - l.last_sms_at;\n    if (msSinceLast < gap * 86400000) continue;\n    if (msSinceLast < RECENT_SEND_WINDOW_MS) continue;\n  }\n  const h = localHour(l.timezone || npaTz(l.phone));\n  if (h < QUIET_START || h >= QUIET_END) continue;\n  const remote = e164(l.phone);\n  if (!remote || !l.close_lead_id || !l.close_contact_id) continue;\n  const senderPhone = l.assigned_sender_phone || null;\n  const senderLabel = l.assigned_sender_label || null;\n  const text = (FOLLOWUPS[n] || '').replace(/\\{biz\\}/g, l.business_name || 'your shop');\n  if (!text) continue;\n  out.push({ json: {\n    id: l.id,\n    lead_id: l.close_lead_id,\n    contact_id: l.close_contact_id,\n    local_phone: senderPhone,\n    sender_label: senderLabel,\n    sender_phone: senderPhone,\n    needs_sender_backfill: !senderPhone,\n    remote_phone: remote,\n    text: text,\n    sms_count: n,\n    send_real: SEND_REAL,\n  }});\n  if (out.length >= BATCH_LIMIT) break;\n}\nreturn out;\n"
      },
      "id": "code-gate",
      "name": "Gate + build SMS",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        288,
        -304
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "cond-sendreal",
              "leftValue": "={{ $json.send_real }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-sendreal",
      "name": "SEND_REAL?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        672,
        0
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "split-batches",
      "name": "Loop Batches (10)",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        656,
        -288
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.close.com/api/v1/activity/sms/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ lead_id: $json.lead_id, contact_id: $json.contact_id, direction: 'outbound', status: 'outbox', local_phone: $json.sticky_sender_phone, remote_phone: $json.remote_phone, text: $json.text }) }}",
        "options": {}
      },
      "id": "http-close-sms",
      "name": "Close SMS, Followup",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1632,
        32
      ],
      "retryOnFail": false,
      "maxTries": 1,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT/record-sms",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ id: $('Gate + build SMS').item.json.id }) }}",
        "options": {}
      },
      "id": "http-record-sms",
      "name": "Convex /record-sms",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2208,
        -256
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.close.com/api/v1/lead/{{ $('Gate + build SMS').item.json.lead_id }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ 'custom.cf_PcdRHPUTgoPxgqczBOFs5eDrhVFAqYcez63lQgCJyJK': ($('Gate + build SMS').item.json.sms_count || 0) + 1 }) }}",
        "options": {}
      },
      "id": "http-close-smscount",
      "name": "Close PUT cf SMS Count",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2368,
        -256
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "unit": "seconds"
      },
      "id": "wait-throttle",
      "name": "Wait 1.5s (rate-limit)",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        1472,
        -288
      ]
    },
    {
      "parameters": {
        "jsCode": "const sent = $input.all().length;\nconst gate = $('Gate + build SMS').all();\nif (sent > 0) {\n  const lines = gate.slice(0, 15).map(g => '\u2022 ' + (g.json.remote_phone || '') + ' (touch ' + (g.json.sms_count + 1) + ')' + (g.json.text ? (', ' + String(g.json.text).slice(0, 50)) : '')).join('\\n');\n  await this.helpers.httpRequest({\n    method: 'POST',\n    url: 'YOUR_SLACK_WEBHOOK',\n    body: { text: '\ud83d\udce4 *WF-NW-03 Followup* sent ' + sent + ' SMS:\\n' + lines + (gate.length > 15 ? ('\\n\u2026and ' + (gate.length - 15) + ' more') : '') },\n    json: true\n  });\n}\nreturn $input.all();"
      },
      "id": "code-slack",
      "name": "Slack: send summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        608,
        -464
      ]
    },
    {
      "parameters": {
        "jsCode": "return $input.all().map(i => ({ json: { dry_run: true, would_send_to: i.json.remote_phone, touch: i.json.sms_count + 1, text: i.json.text, lead_id: i.json.lead_id } }));"
      },
      "id": "code-dryrun",
      "name": "No-Op (dry-run log)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        192
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT/sms-activities/log",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ (function(){ var lead = $('Gate + build SMS').item.json; var closeResp = $json; return JSON.stringify({ lead_id: lead.id, close_lead_id: lead.close_lead_id, stage: Number(lead.sms_count) || 0, direction: 'outbound', text: closeResp.text || '', sent_at: Date.now(), close_activity_id: closeResp.id, status: closeResp.status || 'outbox', sender_label: lead.sender_label, sender_phone: lead.sender_phone }); })() }}",
        "options": {}
      },
      "id": "http-log-sms-followup",
      "name": "Convex /sms-activities/log (followup)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2048,
        -256
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "url": "=https://api.close.com/api/v1/lead/{{ $json.lead_id }}/?_fields=id,status_id,status_label,display_name,custom.cf_CpsUzGm98lRUY7ah9LUhfFGGC6xwYE7ZYsBj1DcPgTf",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "options": {
          "timeout": 15000
        }
      },
      "id": "http-close-get-lead",
      "name": "Close GET lead status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        848,
        -272
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c-hh",
              "leftValue": "={{ $json.status_id }}",
              "rightValue": "stat_AVA3WALf67nRg7DZXeNckA5RS0ifVRtcBJqeReNs9D6",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "c-oo",
              "leftValue": "={{ $json.status_id }}",
              "rightValue": "stat_KztVZXjQ5BW8OzP9Mk4hSzvAvuLilm0ClxMlaOQwR9R",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "c-rp",
              "leftValue": "={{ $json.status_id }}",
              "rightValue": "stat_0QPQkN2ZspV6SMnlH4B0ySCbE10waV9tWBbdCXrQ43m",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "c-ds",
              "leftValue": "={{ $json.status_id }}",
              "rightValue": "stat_JkhhQYXcbMQXXixqSBlmq35po9YWwgD0cn2XnNXP8z2",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "c-landline",
              "leftValue": "={{ ($json['custom.cf_CpsUzGm98lRUY7ah9LUhfFGGC6xwYE7ZYsBj1DcPgTf'] || '').toLowerCase() }}",
              "rightValue": "landline",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "id": "if-status-skip",
      "name": "Status in skip list?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1024,
        -272
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_SLACK_WEBHOOK",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: '\ud83d\uded1 *FOLLOWUP SKIPPED* \u00b7 ' + ($json.display_name || $('Gate + build SMS').item.json.lead_id || '?') + ' \u00b7 status: ' + ($json.status_label || '?') + ' \u00b7 no more automated SMS' }) }}",
        "options": {}
      },
      "id": "slack-status-skip",
      "name": "Slack: Followup Skipped (Status)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1216,
        -288
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c-sent",
              "leftValue": "={{ !!$json.id }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-followup-sent",
      "name": "Followup Sent OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1840,
        -240
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_SLACK_WEBHOOK",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: '\u274c *FOLLOWUP SMS FAILED* \u00b7 lead ' + ($('Gate + build SMS').item.json.lead_id || '?') + ' \u00b7 touch ' + (($('Gate + build SMS').item.json.sms_count || 0) + 1) + ' \u00b7 error: ' + (($json.error && $json.error.message) || 'unknown') }) }}",
        "options": {}
      },
      "id": "slack-followup-fail",
      "name": "Slack: Followup Failed",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1680,
        -288
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "url": "=https://api.close.com/api/v1/activity/sms/?lead_id={{ $('Gate + build SMS').item.json.lead_id }}&_order_by=date_created&_limit=50&_fields=id,direction,status,text,date_created,local_phone",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "options": {
          "timeout": 15000
        }
      },
      "id": "http-close-sms-hist",
      "name": "Close GET SMS history (sticky fallback)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1168,
        32
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Derive sticky sender AND correct next-touch from Close's real SMS history.\n// Close is source of truth: sticky sender = OLDEST outbound local_phone (opener's phone).\n// This ensures leads always see the SAME number for every follow-up.\n\nconst FOLLOWUPS = {\n  2: \"Are you interested in seeing The Website I made your business?  If you have one and I missed it let me know\",\n  3: \"I'll keep following up until I get a No. Can I send it over?\",\n  4: \"I'm going to keep following up lol just say cya later alligator \ud83d\udc0a and I'll STOP over just say send Website\"\n};\n\nfunction touchOf(text) {\n  const t = (text || '').toLowerCase();\n  if (t.indexOf('cya later alligator') >= 0) return 4;\n  if (t.indexOf(\"i'll keep following up until i get a no\") >= 0) return 3;\n  if (t.indexOf('are you interested in seeing the website') >= 0) return 2;\n  return 1;\n}\n\nconst out = [];\nfor (const item of $input.all()) {\n  const gate = $('Gate + build SMS').item.json;\n  const smsList = (item.json && item.json.data) ? item.json.data : [];\n\n  // Sticky sender: Close history WINS (opener's phone is what lead saw).\n  // Convex assigned_sender_phone is only used if no history exists (shouldn't happen for followups).\n  const outbound = smsList.filter(m => m && m.direction === 'outbound' && m.local_phone);\n  outbound.sort((a, b) => new Date(a.date_created || 0) - new Date(b.date_created || 0));\n  const openerPhone = (outbound[0] && outbound[0].local_phone) || null;\n  const sticky_sender_phone = openerPhone || gate.sender_phone || '+15555550100';\n  const sender_source = openerPhone ? 'close_opener' : (gate.sender_phone ? 'convex_assigned' : 'hardcoded_fallback');\n\n  // Determine correct next touch from Close's actual delivered outbound\n  const delivered = smsList.filter(m => m && m.direction === 'outbound' && m.status === 'sent');\n  let maxTouch = 0;\n  for (const m of delivered) {\n    const t = touchOf(m.text);\n    if (t > maxTouch) maxTouch = t;\n  }\n\n  // Already at Touch 4 \u2192 skip\n  if (maxTouch >= 4) {\n    console.log(`Skipping ${gate.lead_id} \u2014 already delivered Touch 4`);\n    continue;\n  }\n\n  // No delivered SMS at all \u2192 skip (opener should come from WF-NW-01)\n  if (maxTouch < 1) {\n    console.log(`Skipping ${gate.lead_id} \u2014 no delivered SMS yet`);\n    continue;\n  }\n\n  const nextTouch = maxTouch + 1;\n  const correctText = FOLLOWUPS[nextTouch];\n\n  out.push({\n    json: Object.assign({}, gate, {\n      sticky_sender_phone,\n      sender_source,\n      close_history_size: outbound.length,\n      close_max_touch_delivered: maxTouch,\n      correct_next_touch: nextTouch,\n      text: correctText,\n      sms_count: maxTouch,\n    })\n  });\n}\nreturn out;"
      },
      "id": "code-derive-sender",
      "name": "Derive Sticky Sender",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1392,
        32
      ]
    },
    {
      "parameters": {
        "url": "=https://api.close.com/api/v1/activity/sms/?lead_id={{ $('Gate + build SMS').item.json.lead_id }}&_limit=50&_fields=id,direction",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "options": {
          "timeout": 15000
        }
      },
      "id": "http-close-check-inbound",
      "name": "Close GET inbound check",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        992,
        -32
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "c-inbound-exists",
              "leftValue": "={{ ($json.data || []).filter(x => x.direction === 'inbound').length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-has-inbound",
      "name": "Has inbound?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1264,
        16
      ]
    }
  ],
  "connections": {
    "Every Hour 9 AM - 8 PM ET": {
      "main": [
        [
          {
            "node": "GET Convex /sms-batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send 10 AM ET (CT zone)": {
      "main": [
        [
          {
            "node": "GET Convex /sms-batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send 11 AM ET (MT zone)": {
      "main": [
        [
          {
            "node": "GET Convex /sms-batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send 12 PM ET (PT/AZ zone)": {
      "main": [
        [
          {
            "node": "GET Convex /sms-batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GET Convex /sms-batch": {
      "main": [
        [
          {
            "node": "Gate + build SMS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gate + build SMS": {
      "main": [
        [
          {
            "node": "SEND_REAL?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SEND_REAL?": {
      "main": [
        [
          {
            "node": "Loop Batches (10)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No-Op (dry-run log)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Batches (10)": {
      "main": [
        [
          {
            "node": "Slack: send summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Close GET lead status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Close GET lead status": {
      "main": [
        [
          {
            "node": "Status in skip list?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Status in skip list?": {
      "main": [
        [
          {
            "node": "Slack: Followup Skipped (Status)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Close GET inbound check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack: Followup Skipped (Status)": {
      "main": [
        [
          {
            "node": "Wait 1.5s (rate-limit)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Close GET SMS history (sticky fallback)": {
      "main": [
        [
          {
            "node": "Derive Sticky Sender",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Derive Sticky Sender": {
      "main": [
        [
          {
            "node": "Close SMS, Followup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Close SMS, Followup": {
      "main": [
        [
          {
            "node": "Followup Sent OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Followup Sent OK?": {
      "main": [
        [
          {
            "node": "Convex /sms-activities/log (followup)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: Followup Failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack: Followup Failed": {
      "main": [
        [
          {
            "node": "Wait 1.5s (rate-limit)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convex /sms-activities/log (followup)": {
      "main": [
        [
          {
            "node": "Convex /record-sms",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convex /record-sms": {
      "main": [
        [
          {
            "node": "Close PUT cf SMS Count",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Close PUT cf SMS Count": {
      "main": [
        [
          {
            "node": "Wait 1.5s (rate-limit)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 1.5s (rate-limit)": {
      "main": [
        [
          {
            "node": "Loop Batches (10)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Close GET inbound check": {
      "main": [
        [
          {
            "node": "Has inbound?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has inbound?": {
      "main": [
        [
          {
            "node": "Wait 1.5s (rate-limit)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Close GET SMS history (sticky fallback)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "America/New_York",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false,
    "binaryMode": "separate",
    "saveDataSuccessExecution": "all",
    "saveDataErrorExecution": "all",
    "saveManualExecutions": true
  },
  "active": false
}