{
  "name": "Seller Follow-Up Engine (Enhanced)",
  "nodes": [
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/template-send",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ lead_id: $('Store Message Data').item.json.leadId, phone: $('Store Message Data').item.json.phone, template_id: $('Store Message Data').item.json.templateId }) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          },
          "timeout": 30000
        }
      },
      "id": "2814c3b9-c69f-4bfc-807e-8369f243ff63",
      "name": "Record to Convex (Follow-Up)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2176,
        10128
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "C0ABWBR67JL",
          "mode": "list",
          "cachedResultName": "test"
        },
        "text": "=\u2705 Follow-Up Engine completed batch\n\u2022 Sent: {{ $json.totalSent || 0 }} SMS\n\u2022 Errors: {{ $json.errors || 0 }}\n\u2022 Time: {{ new Date().toLocaleString() }}",
        "otherOptions": {}
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.4,
      "position": [
        1088,
        10128
      ],
      "id": "f406d877-0f35-4157-b016-ca2861afff39",
      "name": "Slack Success1",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9,10,11,12,14,15,16,17,18 * * 1-6"
            }
          ]
        }
      },
      "id": "df1a332f-0577-48af-b54d-c62f1836b13c",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        96,
        10864
      ]
    },
    {
      "parameters": {
        "jsCode": "// ============================================\n// SELLER FOLLOW-UP ENGINE - SPEC COMPLIANT v4.0\n// From: Follow_Up_Engine_Dev_Spec_v2.pdf\n// \n// HARD RULES (Section 9):\n// - NEVER send before 8 AM or after 8 PM EST\n// - NEVER send on Sunday\n// - Saturday: 10 AM - 6 PM only\n// \n// SEND WINDOWS (randomized):\n// - Morning: 8:30 AM - 10:30 AM\n// - Midday: 11:30 AM - 1:30 PM\n// - Afternoon: 2:30 PM - 4:30 PM\n// - Evening: 5:30 PM - 7:30 PM\n// ============================================\n\nconst now = new Date();\n\n// Get EST time properly\nconst estString = now.toLocaleString('en-US', { timeZone: 'America/New_York' });\nconst estDate = new Date(estString);\nconst currentHourEST = estDate.getHours();\nconst currentMinuteEST = estDate.getMinutes();\nconst dayOfWeekEST = estDate.getDay(); // 0=Sun, 6=Sat\n\n// Phone Number Configuration\nconst PHONE_CONFIG = {\n  '330': [\n    { id: 'phon_jIteklsdr2tUmnb09NjGuQYiPY1WylLCYAeIAyrZMe0', number: '+15555550100' },\n    { id: 'phon_uFcxzUFdhi1ZQl1BHaqoANc2fNacsViMtUIvYpMAV7j', number: '+15555550100' },\n    { id: 'phon_v8P5GhHOKwzCSR0hgdUHrnF4DUFmyby45uVtFouI3qe', number: '+15555550100' }\n  ],\n  '440': [\n    { id: 'phon_Hcd8Gnd5UVhOs6sd8RaS15krqSRFTKI3qrVmBNhpWd8', number: '+15555550100' },\n    { id: 'phon_mGR9Sverf72aPeVovjGChbNtfH6g2gmS2hEAlgzbLjq', number: '+15555550100' }\n  ],\n  '216': [\n    { id: 'phon_GPHgzIPuuBsCBtw5u2K0rVreNa5LWcqYjZZ2JgAEQz3', number: '+15555550100' }\n  ],\n  'default': { id: 'phon_jIteklsdr2tUmnb09NjGuQYiPY1WylLCYAeIAyrZMe0', number: '+15555550100' }\n};\n\n// ZIP to Area Code Mapping\nconst ZIP_TO_AREA_CODE = {\n  // BRRRR (Akron) - 330\n  '44302': '330', '44303': '330', '44306': '330', '44310': '330',\n  '44312': '330', '44313': '330', '44314': '330', '44319': '330', '44320': '330',\n  // BRRRR (Canton) - 330\n  '44703': '330', '44705': '330', '44708': '330', '44709': '330',\n  '44710': '330', '44714': '330',\n  // BRRRR (Other) - 330\n  '44646': '330', '44647': '330', '44203': '330', '44224': '330',\n  '44232': '330', '44685': '330',\n  // High ARV (West) - 440\n  '44136': '440', '44149': '440', '44212': '440', '44256': '440',\n  '44141': '440', '44281': '440', '44147': '440', '44133': '440',\n  // High ARV (Canton) - 330\n  '44718': '330', '44720': '330', '44721': '330'\n};\n\n// HARD RULES CHECK\nlet shouldRun = true;\nlet skipReason = '';\n\n// Rule 1: NEVER on Sunday\nif (dayOfWeekEST === 0) {\n  shouldRun = false;\n  skipReason = 'Sunday - no sends allowed';\n}\n\n// Rule 2: Saturday restricted hours (10 AM - 6 PM EST only)\nelse if (dayOfWeekEST === 6) {\n  if (currentHourEST < 10 || currentHourEST >= 18) {\n    shouldRun = false;\n    skipReason = 'Saturday outside 10AM-6PM EST window';\n  }\n}\n\n// Rule 3: Weekdays - 8 AM to 8 PM EST only\nelse {\n  if (currentHourEST < 8 || currentHourEST >= 20) {\n    shouldRun = false;\n    skipReason = 'Outside 8AM-8PM EST window';\n  }\n}\n\n// Determine current window for randomization\nlet currentWindow = 'none';\nif (currentHourEST >= 8 && currentHourEST < 11) currentWindow = 'morning';\nelse if (currentHourEST >= 11 && currentHourEST < 14) currentWindow = 'midday';\nelse if (currentHourEST >= 14 && currentHourEST < 17) currentWindow = 'afternoon';\nelse if (currentHourEST >= 17 && currentHourEST < 20) currentWindow = 'evening';\n\nreturn [{\n  json: {\n    shouldRun,\n    skipReason,\n    currentWindow,\n    currentTime: now.toISOString(),\n    currentTimeEST: estString,\n    currentHourEST,\n    currentMinuteEST,\n    dayOfWeekEST,\n    dayName: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][dayOfWeekEST],\n    phoneConfig: PHONE_CONFIG,\n    zipToAreaCode: ZIP_TO_AREA_CODE\n  }\n}];"
      },
      "id": "4eb6a2e1-8c5e-49a1-9466-20bfd4cce693",
      "name": "Initialize Config",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        96,
        10608
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "should-run",
              "leftValue": "={{ $json.shouldRun }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0d4507ff-e632-4906-8d3a-3536dbd1d913",
      "name": "Should Run?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        272,
        10608
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "options": {
          "reset": false
        }
      },
      "id": "94d74b38-7506-4225-898e-390c1d779c4d",
      "name": "Rate Limiter (1 per sec)",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1072,
        10592
      ]
    },
    {
      "parameters": {
        "amount": 1
      },
      "id": "8c6dcc89-d864-4e54-b9bd-40a496e2adde",
      "name": "1s Delay",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1808,
        10352
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.close.com/api/v1/activity/sms/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"lead_id\": \"{{ $json.leadId }}\",\n  \"contact_id\": \"{{ $json.contactId }}\",\n  \"local_phone\": \"{{ $json.assignedPhone }}\",\n  \"remote_phone\": \"{{ $json.phone }}\",\n  \"text\": {{ JSON.stringify($json.messageToSend) }},\n  \"direction\": \"outbound\",\n  \"status\": \"outbox\"\n}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "json"
            }
          },
          "timeout": 30000
        }
      },
      "id": "cda2be08-0072-4a6c-8917-f5049a82707b",
      "name": "Send SMS via Close",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1968,
        10352
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "sms-success",
              "leftValue": "={{ $json.statusCode }}",
              "rightValue": 200,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "019844f0-5a3d-4232-9527-05741419480a",
      "name": "SMS Sent OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2144,
        10336
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "// CALCULATE NEXT FOLLOW-UP v7.3 - Fixed data reference\n// After SMS send, $input.item.json is Close API response\n// Must reference Store Message Data node for original lead data\n\nconst inputItem = $('Store Message Data').first().json;\n\nif (!inputItem) {\n  console.log('No data from Store Message Data node');\n  return [{ json: { error: true, message: 'Missing upstream data' } }];\n}\n\n// Get sequence position from the stored data\nconst textBeingSentNow = inputItem.sequencePosition || inputItem._queue?.sequence_position || 1;\n\nif (!textBeingSentNow || textBeingSentNow < 1 || textBeingSentNow > 8) {\n  console.log('Invalid sequence position:', textBeingSentNow);\n  return [{ json: { error: true, message: 'Invalid sequence position' } }];\n}\n\nconsole.log('Processing Text #' + textBeingSentNow + ' for lead: ' + inputItem.leadId);\n\nconst currentPosition = textBeingSentNow;\nconst nextTextNumber = currentPosition + 1;\n\nconst SEQUENCE_HOURS = {\n  2: 6, 3: 36, 4: 144, 5: 204, 6: 288, 7: 420, 8: 612\n};\n\nconst hoursUntilNext = SEQUENCE_HOURS[nextTextNumber] || 168;\nconst now = new Date();\nconst nextSendDate = new Date(now.getTime() + (hoursUntilNext * 60 * 60 * 1000));\nconst sequenceComplete = nextTextNumber > 8;\n\nlet newPipelineStage;\nif (currentPosition <= 3) newPipelineStage = 'Blitz';\nelse if (currentPosition <= 6) newPipelineStage = 'Extended';\nelse newPipelineStage = 'Nurture';\n\nreturn [{\n  json: {\n    leadId: inputItem.leadId || inputItem._queue?.lead_id,\n    leadName: inputItem.firstName || inputItem._queue?.first_name || '',\n    phone: inputItem.phone || inputItem._queue?.phone || '',\n    address: inputItem.address || inputItem._queue?.address || '',\n    city: inputItem.city || inputItem._queue?.city || '',\n    zip: inputItem.zip || inputItem._queue?.zip || '',\n    newPosition: currentPosition,\n    nextTextNumber: nextTextNumber,\n    nextSendDate: sequenceComplete ? null : nextSendDate.toISOString(),\n    lastContacted: now.toISOString(),\n    newPipelineStage: newPipelineStage,\n    sequenceComplete: sequenceComplete,\n    listingType: inputItem.listingType || inputItem._queue?.listing_type || 'BRRRR',\n    tier: inputItem.tier || inputItem._queue?.tier || '',\n    _queue: inputItem._queue || {},\n    _debug: { textBeingSentNow, currentPosition, nextTextNumber, hoursUntilNext, version: '7.3', dataSource: 'Store Message Data' }\n  }\n}];"
      },
      "id": "7b54b9cb-6b26-4ef6-bea2-5c15d3f3bdd8",
      "name": "Calculate Next Follow-Up",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2368,
        10320
      ]
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.close.com/api/v1/lead/{{ $json.leadId }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"custom.cf_4k0rrTQ6pQUytyLp3T0lAthZ7AEFUKyRbndn59Hsu8U\": {{ $json.newPosition }},\n  \"custom.cf_d8OZm5MmRSchNbeJF4KXmrAm3JpjVscpyMKrAQ6ek9r\": \"{{ $json.nextSendDate || '' }}\",\n  \"custom.cf_QgUEmGIweiQG45RF5GD09JJNCyhcIZf2uNqOL8zT0sl\": \"{{ $json.newPipelineStage }}\",\n  \"custom.cf_mLLAy4ctJLqn2zhjECML593u8vs4lsIDtMjTQpHudtf\": \"{{ $json.lastContacted }}\",\n  \"custom.cf_vrVajMz5zvcWRSxoflOq1AbfxWT12lVCvQZD04hIPvG\": \"outbound\"\n}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          },
          "timeout": 30000
        }
      },
      "id": "0a7d13cc-7e74-4a7f-946e-8b91e29465fe",
      "name": "Update Lead (Success)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2544,
        10320
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// LOG SMS FAILURE v7.3 - Fixed data reference\n// Get error response from current input\nconst errorResponse = $input.item.json;\n\n// Get lead info from Store Message Data\nlet leadInfo = {};\ntry {\n  leadInfo = $('Store Message Data').first().json || {};\n} catch(e) {\n  console.log('Could not get lead info from Store Message Data');\n}\n\nlet failStats;\ntry {\n  const raw = $execution.customData.get('failureStats');\n  failStats = raw ? JSON.parse(raw) : { total: 0, failures: [] };\n} catch(e) {\n  failStats = { total: 0, failures: [] };\n}\n\nfailStats.total++;\nif (failStats.failures.length < 10) {\n  failStats.failures.push({ \n    leadId: leadInfo.leadId || 'unknown',\n    phone: leadInfo.phone || 'unknown',\n    error: JSON.stringify(errorResponse).slice(0, 200) \n  });\n}\n\n$execution.customData.set('failureStats', JSON.stringify(failStats));\n\nconsole.log('SMS Failure for lead:', leadInfo.leadId || 'unknown');\n\nreturn [{\n  json: {\n    status: 'failed',\n    leadId: leadInfo.leadId,\n    phone: leadInfo.phone,\n    error: errorResponse,\n    timestamp: new Date().toISOString()\n  }\n}];"
      },
      "id": "b6d34068-124e-481f-9f68-2e6068fe5bd6",
      "name": "Log SMS Failure",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2448,
        10608
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "url": "https://api.close.com/api/v1/lead/?query=custom.cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW:\"Active Conversation\" AND custom.cf_vrVajMz5zvcWRSxoflOq1AbfxWT12lVCvQZD04hIPvG:\"outbound\"&_fields=id,display_name,contacts,custom&_limit=100",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          },
          "timeout": 30000
        }
      },
      "id": "50c41a6c-451f-4dbe-a9f5-d41646cf1ef6",
      "name": "Get Stalled Conversations",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        336,
        10160
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// ============================================\n// STALL DETECTION\n// Check for conversations that went cold\n// ============================================\n\nconst response = $input.first().json;\nconst leads = response.data || [];\nconst now = new Date();\n\nconst stalledLeads = [];\n\nfor (const lead of leads) {\n  const custom = lead.custom || {};\n  const pipelineStage = custom.cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW;\n  const lastMsgDirection = custom.cf_vrVajMz5zvcWRSxoflOq1AbfxWT12lVCvQZD04hIPvG;\n  const lastMsgAt = custom.cf_mLLAy4ctJLqn2zhjECML593u8vs4lsIDtMjTQpHudtf;\n  const responseType = custom.cf_response_type;\n  \n  // Skip if not in Active Conversation\n  if (pipelineStage !== 'Active Conversation') continue;\n  \n  // Skip if they sent last message\n  if (lastMsgDirection !== 'outbound') continue;\n  \n  // Calculate hours since last message\n  if (!lastMsgAt) continue;\n  const hoursSince = (now.getTime() - new Date(lastMsgAt).getTime()) / (1000 * 60 * 60);\n  \n  // Stall if > 24 hours and previous response was positive/question\n  if (hoursSince > 24 && ['positive', 'question'].includes(responseType)) {\n    stalledLeads.push({\n      json: {\n        leadId: lead.id,\n        leadName: lead.display_name,\n        hoursSinceLastMsg: Math.round(hoursSince),\n        previousResponseType: responseType,\n        action: 'mark_stalled'\n      }\n    });\n  }\n}\n\nif (stalledLeads.length === 0) {\n  return [{ json: { noStalledLeads: true } }];\n}\n\nreturn stalledLeads;"
      },
      "id": "bf6ff907-3b1d-442d-bcc9-3ddf647fede9",
      "name": "Detect Stalled Conversations",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        528,
        10160
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "has-stalled",
              "leftValue": "={{ $json.leadId }}",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2393a189-17b0-40d6-82a6-67e7f5c3cabf",
      "name": "Has Stalled?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        704,
        10144
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.close.com/api/v1/lead/{{ $json.leadId }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"custom.cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW\": \"Stalled\",\n  \"custom.cf_stall_detected_at\": \"{{ $now.toISOString() }}\",\n  \"custom.cf_QgUEmGIweiQG45RF5GD09JJNCyhcIZf2uNqOL8zT0sl\": \"Re-engagement\",\n  \"custom.cf_4k0rrTQ6pQUytyLp3T0lAthZ7AEFUKyRbndn59Hsu8U\": 1,\n  \"custom.cf_d8OZm5MmRSchNbeJF4KXmrAm3JpjVscpyMKrAQ6ek9r\": \"{{ new Date(Date.now() + 24*60*60*1000).toISOString() }}\"\n}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          },
          "timeout": 30000
        }
      },
      "id": "4aa4f903-0bd0-4b7e-a4b3-d3ae3eaa3e32",
      "name": "Mark as Stalled",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        944,
        10128
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "phase-transition-check",
              "leftValue": "={{ $('Calculate Next Follow-Up').item.json.newPosition }}",
              "rightValue": 4,
              "operator": {
                "type": "number",
                "operation": "gte"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "28850260-2219-4870-81cd-1687b6fc2522",
      "name": "Phase Transition?1",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2896,
        10560
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.close.com/api/v1/lead/{{ $('Calculate Next Follow-Up').item.json.leadId }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"status_id\": \"stat_bmHeqaDzp8iU2XGVNMfHS1frlQTG2LaQvymODA7GtoK\"\n}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          },
          "timeout": 30000
        }
      },
      "id": "d6fc1695-d101-480f-92e4-0dec07575ff8",
      "name": "Update Status to Extended1",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2896,
        10752
      ]
    },
    {
      "parameters": {
        "jsCode": "// ============================================================================\n// ENHANCED SLACK NOTIFICATION WITH POSITION MONITORING\n// ============================================================================\n\nconst items = $('Rate Limiter (1 per sec)').all();\n\n// Collect statistics\nlet successCount = 0;\nlet failureCount = 0;\nlet positionMismatches = 0;\nconst leads = [];\nconst mismatches = [];\n\nitems.forEach(item => {\n  const data = item.json;\n\n  // Check if SMS was sent successfully\n  const success = data._smsSuccess || false;\n\n  if (success) {\n    successCount++;\n\n    leads.push({\n      name: data.leadName || data._queue?.first_name || 'Unknown',\n      phone: data.phone || data._queue?.phone || 'Unknown',\n      sentPosition: data._queue?.sequence_position,\n      newPosition: data.newPosition,\n      nextPosition: data.nextTextNumber,\n      stage: data.newPipelineStage\n    });\n\n    // CHECK FOR MISMATCH: If we have debug info\n    if (data._debug) {\n      const sentPos = data._debug.textBeingSentNow;\n      const updatedPos = data.newPosition;\n\n      if (sentPos !== updatedPos) {\n        positionMismatches++;\n        mismatches.push({\n          lead: data.leadName || 'Unknown',\n          sentPosition: sentPos,\n          updatedPosition: updatedPos\n        });\n      }\n    }\n\n  } else {\n    failureCount++;\n  }\n});\n\n// Calculate success rate\nconst totalProcessed = successCount + failureCount;\nconst successRate = totalProcessed > 0 ? ((successCount / totalProcessed) * 100).toFixed(1) : 0;\n\n// Build detailed message\nconst leadsList = leads.slice(0, 10).map(l =>\n  `  \u2022 ${l.name}: Text #${l.sentPosition} \u2192 Position ${l.newPosition} (next: ${l.nextPosition || 'complete'}) [${l.stage}]`\n).join('\\n');\n\nconst mismatchList = mismatches.length > 0 ? mismatches.map(m =>\n  `  \u26a0\ufe0f ${m.lead}: Sent #${m.sentPosition} but updated to ${m.updatedPosition}`\n).join('\\n') : '  \u2705 No mismatches detected';\n\nconst timestamp = new Date().toISOString();\n\nconst message = `\n\ud83d\udd14 *Follow-Up Engine Completed*\n\n*Summary:*\n\u2705 Sent: ${successCount}\n\u274c Failed: ${failureCount}\n\ud83d\udcca Success Rate: ${successRate}%\n\u23f0 Time: ${timestamp}\n\n*Position Tracking:*\n${positionMismatches > 0 ? '\u26a0\ufe0f' : '\u2705'} Mismatches: ${positionMismatches}\n${mismatchList}\n\n*Recent Sends:*\n${leadsList}\n${leads.length > 10 ? `... and ${leads.length - 10} more` : ''}\n\n${positionMismatches > 0 ? '\\n\u26a0\ufe0f *ACTION REQUIRED:* Position mismatches detected! Check logs.' : ''}\n`;\n\nreturn [{\n  json: {\n    message: message,\n    successCount,\n    failureCount,\n    successRate,\n    positionMismatches,\n    totalProcessed,\n    leads,\n    mismatches,\n    timestamp\n  }\n}];\n"
      },
      "id": "4b7df2ec-efb7-4ad0-bfb1-504c6fd99c79",
      "name": "Collect Success Stats1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        528,
        10432
      ]
    },
    {
      "parameters": {
        "jsCode": "// TRACK SUCCESS v7.4 - Removed $execution.customData (sandbox blocked)\n// Just pass through data from Calculate Next Follow-Up\n\nconst calcItem = $('Calculate Next Follow-Up').first().json;\n\nif (!calcItem || typeof calcItem !== 'object') {\n  console.log('No data from Calculate Next Follow-Up');\n  return [{ json: { status: 'error', message: 'Missing upstream data' } }];\n}\n\nconsole.log('Track Success - lead:', calcItem.leadId, '| Position:', calcItem.newPosition);\n\nreturn [{\n  json: {\n    status: 'success',\n    leadId: calcItem.leadId,\n    leadName: calcItem.leadName,\n    phone: calcItem.phone,\n    address: calcItem.address,\n    city: calcItem.city,\n    zip: calcItem.zip,\n    newPosition: calcItem.newPosition,\n    nextTextNumber: calcItem.nextTextNumber,\n    nextSendDate: calcItem.nextSendDate,\n    sequenceComplete: calcItem.sequenceComplete,\n    newPipelineStage: calcItem.newPipelineStage,\n    listingType: calcItem.listingType,\n    tier: calcItem.tier,\n    _queue: calcItem._queue,\n    _smsSuccess: true,\n    _debug: calcItem._debug\n  }\n}];"
      },
      "id": "f9a76bd8-63e5-46f8-b95b-ba9b17657fdf",
      "name": "Track Success1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3120,
        10576
      ]
    },
    {
      "parameters": {
        "jsCode": "// Query Convex for follow-ups due NOW\nconst CONVEX_URL = 'https://YOUR_CONVEX_DEPLOYMENT.convex.site';\nconst now = Date.now();\n\ntry {\n  const data = await this.helpers.httpRequest({\n    method: 'POST',\n    url: `${CONVEX_URL}/api/follow-up-queue-due`,\n    headers: { 'Content-Type': 'application/json' },\n    body: {\n      due_before: now,\n      status: 'pending',\n      limit: 1000\n    },\n    json: true\n  });\n  \n  const dueLeads = data.leads || [];\n  \n  if (dueLeads.length === 0) {\n    return [{ \n      json: { \n        noLeads: true, \n        totalDue: 0,\n        leads: [], \n        message: 'No follow-ups due', \n        serverTime: new Date().toISOString() \n      } \n    }];\n  }\n  \n  console.log(`Found ${dueLeads.length} follow-ups due from Convex queue`);\n  \n  // Return with totalDue at top level for easy visibility\n  return [{\n    json: {\n      totalDue: dueLeads.length,\n      message: `${dueLeads.length} follow-ups due`,\n      leads: dueLeads,\n      serverTime: new Date().toISOString()\n    }\n  }];\n} catch (error) {\n  console.log('Convex query error:', error.message);\n  return [{ json: { error: true, totalDue: 0, leads: [], message: error.message } }];\n}"
      },
      "id": "2ba524dd-6fef-4332-a7fc-4c072e1e2f30",
      "name": "Fetch Due from Convex",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        496,
        10592
      ]
    },
    {
      "parameters": {
        "jsCode": "// UPDATE CONVEX QUEUE v7.3 - Fixed data reference\nconst CONVEX_URL = 'https://YOUR_CONVEX_DEPLOYMENT.convex.site';\n\n// Reference Track Success1 for proper data\nconst item = $('Track Success1').first().json;\n\nif (!item || typeof item !== 'object') {\n  console.log('No data from Track Success1');\n  return [{ json: { status: 'error', message: 'Missing upstream data' } }];\n}\n\nconst queueId = item._queue?._id;\nconst leadId = item.leadId || item._queue?.lead_id;\nconst phone = item.phone || item._queue?.phone;\n\nconsole.log('Processing queue update for lead:', leadId);\n\nif (!queueId) {\n  console.log('Missing queue ID - skipping');\n  return [{ json: { status: 'skipped', reason: 'no_queue_id', leadId } }];\n}\n\nconst results = { queueId, leadId, markedSent: false, addedNext: false, errors: [] };\n\n// STEP 1: Mark current as SENT\ntry {\n  await this.helpers.httpRequest({\n    method: 'POST',\n    url: `${CONVEX_URL}/api/follow-up-queue-complete`,\n    headers: { 'Content-Type': 'application/json' },\n    body: { queue_id: queueId, status: 'sent', sent_at: Date.now() },\n    json: true\n  });\n  results.markedSent = true;\n} catch (error) {\n  results.errors.push(error.message);\n  console.log('Failed to mark sent:', error.message);\n}\n\n// STEP 2: Add next follow-up\nif (!item.sequenceComplete && item.nextTextNumber && item.nextTextNumber <= 8) {\n  try {\n    const allQueue = await this.helpers.httpRequest({\n      method: 'GET',\n      url: `${CONVEX_URL}/api/follow-up-queue-list?status=pending&limit=100`,\n      json: true\n    });\n    const duplicate = allQueue.find(q => q.lead_id === leadId && q.sequence_position === item.nextTextNumber && q.status === 'pending');\n    if (duplicate) {\n      results.skippedReason = 'duplicate';\n      return [{ json: results }];\n    }\n  } catch (e) {}\n\n  const nextSendTimestamp = new Date(item.nextSendDate).getTime();\n  try {\n    await this.helpers.httpRequest({\n      method: 'POST',\n      url: `${CONVEX_URL}/api/follow-up-queue-add`,\n      headers: { 'Content-Type': 'application/json' },\n      body: {\n        lead_id: leadId,\n        phone: phone,\n        first_name: item._queue?.first_name || item.leadName || '',\n        address: item._queue?.address || item.address || '',\n        city: item._queue?.city || item.city || '',\n        zip: item._queue?.zip || item.zip || '',\n        next_send_date: nextSendTimestamp,\n        sequence_position: item.nextTextNumber,\n        lead_type: 'Seller',\n        listing_type: item.listingType || item._queue?.listing_type || 'BRRRR',\n        tier: item.tier || item._queue?.tier || '',\n        status: 'pending'\n      },\n      json: true\n    });\n    results.addedNext = true;\n  } catch (error) {\n    results.errors.push(error.message);\n  }\n} else {\n  results.skippedReason = 'sequence_complete';\n}\n\nreturn [{ json: results }];"
      },
      "id": "update-convex-queue",
      "name": "Update Convex Queue",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3296,
        10576
      ]
    },
    {
      "parameters": {
        "jsCode": "// Split Convex leads array into individual n8n items\nconst input = $input.first().json;\nconst leads = input.leads || [];\n\n// If no leads, return EMPTY array - nothing enters the loop\nif (!leads || leads.length === 0) {\n  console.log('No follow-ups due - returning empty array');\n  return [];  // Empty = Rate Limiter goes straight to \"done\" output\n}\n\nconsole.log(`Splitting ${leads.length} leads into individual items`);\n\n// Output each lead as a separate item\nreturn leads.map(queueItem => ({\n  json: {\n    lead_id: queueItem.lead_id,\n    _queue: queueItem\n  }\n}));"
      },
      "id": "fetch-close-details",
      "name": "Split Lead Items",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        672,
        10592
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "check-id-exists",
              "leftValue": "={{ $json.contacts[0].lead_id }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "check-lead-exists",
      "name": "Lead Exists in Close?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1648,
        10576
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/follow-up-queue-delete",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ lead_id: $('Split Lead Items').item.json.lead_id }) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "delete-stale-from-convex",
      "name": "Delete Stale Lead from Convex",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1872,
        10592
      ]
    },
    {
      "parameters": {
        "jsCode": "// LOG DELETED STALE LEAD v7.3 - Fixed input validation\nconst rawInput = $input.item.json;\n\n// Validate input is an object\nif (!rawInput || typeof rawInput !== 'object' || Array.isArray(rawInput)) {\n  console.log('Invalid input for deleted lead log');\n  return [{ json: { status: 'deleted', data: null, timestamp: new Date().toISOString() } }];\n}\n\nconsole.log('Deleted stale lead:', rawInput.lead_id || 'unknown');\nreturn [{ json: { status: 'deleted', data: rawInput, timestamp: new Date().toISOString() } }];"
      },
      "id": "log-deleted-lead",
      "name": "Log Deleted Stale Lead",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2032,
        10592
      ]
    },
    {
      "parameters": {
        "jsCode": "// Build Message v9.3 - Full Safety Filters (matching fls workflow)\nconst CONVEX_URL = 'https://YOUR_CONVEX_DEPLOYMENT.convex.site';\n\n// Helper: Safely extract string from array or string (Close CRM returns arrays for multi-select)\nconst safeStr = (val) => {\n  if (Array.isArray(val)) return (val[0] || '').toString();\n  return (val || '').toString();\n};\n\n// Helper: Calculate hours since a date\nconst hoursSince = (dateStr) => {\n  if (!dateStr) return Infinity;\n  const date = new Date(dateStr);\n  return (Date.now() - date.getTime()) / (1000 * 60 * 60);\n};\n\n// Get original Convex queue data (with _id)\nlet originalQueue = null;\ntry {\n  originalQueue = $('Rate Limiter (1 per sec)').first().json._queue;\n} catch(e) {\n  console.log('Could not get original queue data');\n}\n\n// Input validation for Close data\nconst rawInput = $input.item.json;\nlet closeData;\n\nif (!rawInput || typeof rawInput !== 'object' || Array.isArray(rawInput)) {\n  console.log('Invalid input - not an object:', typeof rawInput);\n  return [{ json: { action: 'SKIP', reason: 'Invalid input data', skipType: 'invalid_input' } }];\n}\n\ncloseData = rawInput;\n\nconsole.log('=== BUILD MESSAGE v9.3 ===');\nconsole.log('Close ID:', closeData.id);\nconsole.log('Queue ID:', originalQueue?._id);\n\nif (!closeData.id) {\n  return [{ json: { action: 'SKIP', reason: 'No Close ID', skipType: 'missing_data' } }];\n}\n\n// Extract data from Close CRM\nconst leadId = closeData.id;\nconst displayName = closeData.display_name || '';\nconst firstName = displayName.split(' ')[0] || 'there';\n\n// Get contact info\nconst contact = closeData.contacts && closeData.contacts[0] ? closeData.contacts[0] : {};\nconst contactId = contact.id || null;\nconst phones = contact.phones || [];\nconst phone = phones.length > 0 ? phones[0].phone : '';\n\nif (!phone) {\n  return [{ json: { action: 'SKIP', reason: 'No phone', lead_id: leadId, skipType: 'missing_phone' } }];\n}\n\n// Get custom fields from Close (using safeStr for array fields)\nconst custom = closeData.custom || {};\nconst address = custom.address || custom.property_address || safeStr(custom['Property Address']) || safeStr(custom.cf_oYpMjF2AbqjJ9TTnn3BZgCNlductY17okbScR0MvrhD) || originalQueue?.address || 'your property';\nconst city = custom.city || custom.property_city || safeStr(custom['Property City']) || safeStr(custom.cf_V5lJPTCII754eYehZUytaQVlT2AhErHmoNfCRTLrBYp) || originalQueue?.city || 'your area';\nconst zip = custom.zip || custom.property_zip || originalQueue?.zip || '';\nconst sequencePosition = Math.floor(originalQueue?.sequence_position || custom.sequence_position || custom['Sequence Position'] || 1);\nconst listingTypeRaw = safeStr(originalQueue?.listing_type || custom.listing_type || custom['Listing Type'] || 'BRRRR');\nconst tier = originalQueue?.tier || custom.tier || '';\n\nconsole.log('Lead:', firstName, '| Phone:', phone, '| Position:', sequencePosition);\n\n// ========== SAFETY CHECKS (CRITICAL!) ==========\nconst statusLabel = (closeData.status_label || '').toLowerCase();\nconst rawPipeline = custom.cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || custom['Pipeline Stage'] || custom.pipeline_stage || '';\nconst pipelineStage = safeStr(rawPipeline).toLowerCase();\n\n// Get timing fields for safety checks\nconst lastMsgDirection = safeStr(custom.cf_vrVajMz5zvcWRSxoflOq1AbfxWT12lVCvQZD04hIPvG || custom['Last Message Direction'] || '');\nconst lastContactedAt = safeStr(custom.cf_mLLAy4ctJLqn2zhjECML593u8vs4lsIDtMjTQpHudtf || custom['Last Contacted'] || '');\nconst vaLastMsgAt = safeStr(custom.cf_kePjoaKjgduHWIlz5sVnilaTT5SuURgLAsl1D2NhLl9 || custom['VA Last Message At'] || '');\n\nconsole.log('Status:', statusLabel, '| Pipeline:', pipelineStage);\nconsole.log('Last Direction:', lastMsgDirection, '| Last Contact:', lastContactedAt);\n\n// ========== 1. RECENT INBOUND CHECK (48hrs) ==========\nif (lastMsgDirection.toLowerCase() === 'inbound' && hoursSince(lastContactedAt) < 48) {\n  console.log('SKIP: Recent inbound -', Math.round(hoursSince(lastContactedAt)), 'hrs ago');\n  return [{ json: { action: 'SKIP', reason: `Recent inbound (${Math.round(hoursSince(lastContactedAt))}hrs)`, lead_id: leadId, phone, skipType: 'recent_inbound' } }];\n}\n\n// ========== 2. DOUBLE TAP CHECK (outbound <12hrs) ==========\nif (lastMsgDirection.toLowerCase() === 'outbound' && hoursSince(lastContactedAt) < 12) {\n  console.log('SKIP: Double tap -', Math.round(hoursSince(lastContactedAt)), 'hrs ago');\n  return [{ json: { action: 'SKIP', reason: `Double tap (${Math.round(hoursSince(lastContactedAt))}hrs)`, lead_id: leadId, phone, skipType: 'double_tap' } }];\n}\n\n// ========== 3. VA ACTIVE CHECK (<48hrs) ==========\nif (vaLastMsgAt && hoursSince(vaLastMsgAt) < 48) {\n  console.log('SKIP: VA active -', Math.round(hoursSince(vaLastMsgAt)), 'hrs ago');\n  return [{ json: { action: 'SKIP', reason: `VA active (${Math.round(hoursSince(vaLastMsgAt))}hrs)`, lead_id: leadId, phone, skipType: 'va_active' } }];\n}\n\n// ========== 4. STATUS LABEL EXCLUSIONS ==========\nconst EXCLUDED_STATUSES = ['dnc', 'human hand', 'hostile', 'paused'];\nfor (const excluded of EXCLUDED_STATUSES) {\n  if (statusLabel.includes(excluded)) {\n    console.log('SKIP: Excluded status -', statusLabel);\n    return [{ json: { action: 'SKIP', reason: `Status: ${closeData.status_label}`, lead_id: leadId, phone, skipType: 'excluded_status' } }];\n  }\n}\n\n// ========== 5. PIPELINE STAGE EXCLUSIONS ==========\nconst EXCLUDED_STAGES = [\n  'dnc', 'hot', 'warm', 'cold', 'human hand', 'hostile', 'paused',\n  'appointment', 'callback', 'wrong contact', 'creative finance',\n  'under contract', 'hostile - paused'\n];\nfor (const excluded of EXCLUDED_STAGES) {\n  if (pipelineStage.includes(excluded)) {\n    console.log('SKIP: Excluded stage -', pipelineStage);\n    return [{ json: { action: 'SKIP', reason: `Stage: ${pipelineStage}`, lead_id: leadId, phone, skipType: 'excluded_stage' } }];\n  }\n}\n\n// ========== 6. DNC CHECK (Convex) ==========\ntry {\n  const dncResp = await this.helpers.httpRequest({\n    method: 'GET',\n    url: `${CONVEX_URL}/api/dnc-check?phone=${encodeURIComponent(phone)}`,\n    json: true\n  });\n  if (dncResp && dncResp.isDnc === true) {\n    return [{ json: { action: 'SKIP', reason: 'DNC (Convex)', lead_id: leadId, phone, skipType: 'dnc' } }];\n  }\n} catch (err) {\n  console.log('DNC check error:', err.message);\n}\n\n// ========== 7. TCPA CHECK ==========\ntry {\n  const cleanPhone = phone.replace(/\\D/g, '');\n  const areaCode = cleanPhone.length >= 10 ? cleanPhone.substring(cleanPhone.length - 10, cleanPhone.length - 7) : '216';\n  const TIMEZONE_MAP = { '216': 'America/New_York', '440': 'America/New_York', '330': 'America/New_York' };\n  const tz = TIMEZONE_MAP[areaCode] || 'America/New_York';\n  const now = new Date();\n  const opts = { timeZone: tz, hour: 'numeric', hour12: false, weekday: 'short' };\n  const localTimeStr = now.toLocaleString('en-US', opts);\n  const parts = localTimeStr.split(' ');\n  const weekday = parts[0];\n  const hour = parseInt(parts[1], 10);\n  \n  if (weekday === 'Sun') {\n    return [{ json: { action: 'SKIP', reason: 'Sunday', lead_id: leadId, phone, skipType: 'tcpa_sunday' } }];\n  }\n  if (hour < 9 || hour >= 20) {\n    return [{ json: { action: 'SKIP', reason: 'Outside hours', lead_id: leadId, phone, skipType: 'tcpa_hours' } }];\n  }\n} catch (err) {\n  console.log('TCPA check error:', err.message);\n}\n\n// ========== 8. RECENT REPLY CHECK ==========\ntry {\n  const resp = await this.helpers.httpRequest({\n    method: 'GET',\n    url: `${CONVEX_URL}/api/last-inbound?lead_id=${encodeURIComponent(leadId)}`,\n    json: true\n  });\n  if (resp && resp.timestamp) {\n    const hrs = (Date.now() - new Date(resp.timestamp).getTime()) / 3600000;\n    if (hrs < 24) {\n      return [{ json: { action: 'SKIP', reason: 'Recent reply', lead_id: leadId, phone, skipType: 'recent_reply' } }];\n    }\n  }\n} catch (err) {}\n\n// ========== 9. HOSTILE CHECK (Convex) ==========\ntry {\n  const resp = await this.helpers.httpRequest({\n    method: 'GET',\n    url: `${CONVEX_URL}/api/hostile-check?lead_id=${encodeURIComponent(leadId)}`,\n    json: true\n  });\n  if (resp && resp.inCoolingPeriod) {\n    return [{ json: { action: 'SKIP', reason: 'Hostile cooling', lead_id: leadId, phone, skipType: 'hostile' } }];\n  }\n} catch (err) {}\n\nconsole.log('All safety checks passed!');\n\n// ========== GET ASSIGNED PHONE ==========\nlet assignedPhone = null;\nif (custom['Assigned Phone']) {\n  assignedPhone = String(custom['Assigned Phone']);\n  if (!assignedPhone.startsWith('+')) assignedPhone = '+' + assignedPhone;\n}\nif (!assignedPhone) {\n  try {\n    const resp = await this.helpers.httpRequest({ method: 'GET', url: `${CONVEX_URL}/api/phone-select`, json: true });\n    if (resp && resp.phone) assignedPhone = resp.phone;\n  } catch (e) {}\n}\nif (!assignedPhone) assignedPhone = '+15555550100';\n\n// ========== BUILD MESSAGE ==========\nconst streetName = address.replace(/^\\d+\\s+/, '').split(',')[0].trim();\nlet listingType;\nif (listingTypeRaw === 'HIGH_ARV' || listingTypeRaw === 'High_ARV' || listingTypeRaw === 'ARV') {\n  listingType = 'High_ARV';\n} else if (listingTypeRaw && listingTypeRaw.indexOf('Expired') === 0) {\n  listingType = listingTypeRaw; // Preserve Expired_Hot, Expired_Warm, Expired_Cold\n} else {\n  listingType = 'BRRRR';\n}\n\nlet templateBody = null, templateId = null, templateVariant = null;\nconst FALLBACK = { 1: 'Hey, following up on {street_name}', 2: '{street_name} - still interested?', 3: 'Quick question about {street_name}', 4: 'Hey, {street_name}?', 5: 'Checking in on {street_name}', 6: 'Final note on {street_name}', 7: 'Market update for {street_name}', 8: 'Last message about {street_name}' };\n\ntry {\n  const apiUrl = `${CONVEX_URL}/api/template-select-thompson?position=${sequencePosition}&listing_type=${encodeURIComponent(listingType)}`;\n  const parsed = await this.helpers.httpRequest({ method: 'GET', url: apiUrl, json: true });\n  if (parsed && parsed.selected && parsed.selected.body) {\n    templateBody = parsed.selected.body;\n    templateId = parsed.selected._id;\n    templateVariant = parsed.selected.variant_name || 'A';\n  }\n} catch (e) {\n  console.log('Template API error:', e.message);\n}\n\nif (!templateBody) {\n  templateBody = FALLBACK[sequencePosition] || FALLBACK[1];\n  templateId = 'fallback_' + sequencePosition;\n  templateVariant = 'fallback';\n}\n\nconst messageToSend = templateBody\n  .replace(/\\{street_name\\}/gi, streetName)\n  .replace(/\\{address\\}/gi, address)\n  .replace(/\\{city\\}/gi, city)\n  .replace(/\\{first_name\\}/gi, firstName);\n\nconsole.log('Message ready:', messageToSend.substring(0, 50));\n\n// Build queue data WITH _id from original Convex data\nconst queueData = {\n  _id: originalQueue?._id,\n  lead_id: leadId,\n  phone: phone,\n  first_name: firstName,\n  address: address,\n  city: city,\n  zip: zip,\n  sequence_position: sequencePosition,\n  listing_type: listingType,\n  tier: tier\n};\n\nreturn [{\n  json: {\n    action: 'SEND',\n    leadId: leadId,\n    contactId: contactId,\n    assignedPhone: assignedPhone,\n    phone: phone,\n    firstName: firstName,\n    address: address,\n    city: city,\n    zip: zip,\n    streetName: streetName,\n    sequencePosition: sequencePosition,\n    listingType: listingType,\n    tier: tier,\n    messageToSend: messageToSend,\n    templateId: templateId,\n    templateVariant: templateVariant,\n    templateBody: templateBody,\n    _queue: queueData,\n    _closeData: closeData,\n    _debug: { version: '9.3', statusLabel, pipelineStage, lastMsgDirection, hasQueueId: !!originalQueue?._id }\n  }\n}];"
      },
      "id": "build-message",
      "name": "Build Message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1296,
        10384
      ]
    },
    {
      "parameters": {
        "maxItems": 300
      },
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        832,
        10592
      ],
      "id": "ea41dc0d-b1bf-4d9c-a59e-4928f24d3935",
      "name": "Limit"
    },
    {
      "parameters": {
        "content": "## Seller Follow-Up Engine\n\n**8-Touch SMS Sequence**\n\n### Sequence Timing (from DEVELOPER-README):\n| Pos | Name | Day | Phase |\n|-----|------|-----|-------|\n| 1 | Initial Outreach | Day 0 | Blitz |\n| 2 | Follow-up 1 | Day 0.5 (12hrs) | Blitz |\n| 3 | Follow-up 2 | Day 1 | Blitz |\n| 4 | Follow-up 3 | Day 4 | Extended |\n| 5 | Mid-Sequence 1 | Day 7 | Extended |\n| 6 | Mid-Sequence 2 | Day 14 | Extended |\n| 7 | Re-engagement 1 | Day 30 | Long-term |\n| 8 | Final Push | Day 60 | Long-term |\n\n### Key Endpoints:\n- Convex: `https://YOUR_CONVEX_DEPLOYMENT.convex.site`\n- Templates: `/api/templates?position={N}&listing_type={type}`\n- Queue: `/api/follow-up-queue-due`\n\n### Notes:\n- Code_Violation \u2192 mapped to BRRRR templates\n- Templates use {firstName} and {address} placeholders",
        "height": 864
      },
      "id": "sticky-note-1",
      "name": "Workflow Info",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -688,
        10272
      ]
    },
    {
      "parameters": {
        "content": "## Seller Follow-Up Engine (Enhanced)\n\n**Purpose:** Automated follow-up SMS sequence for seller leads (Text 2-8). Runs on schedule to send timed follow-ups to leads who haven't responded.\n\n**Trigger:** Schedule Trigger (every X minutes)\n\n**Key Features:**\n- Fetches due follow-ups from Convex queue\n- Validates lead still exists in Close CRM\n- Detects stalled conversations\n- 1-second rate limiting between SMS\n- Phase transition tracking (Blitz \u2192 Extended)",
        "height": 496
      },
      "id": "sticky-overview-new",
      "name": "\ud83d\udccb Follow-Up Engine Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -368,
        10464
      ]
    },
    {
      "parameters": {
        "content": "## Data Flow\n\n1\ufe0f\u20e3 **Initialize Config** \u2192 Set up run parameters\n2\ufe0f\u20e3 **Should Run?** \u2192 Check if within operating hours\n3\ufe0f\u20e3 **Fetch Due from Convex** \u2192 Get leads needing follow-up\n4\ufe0f\u20e3 **Get Close Lead** \u2192 Verify lead exists in CRM\n5\ufe0f\u20e3 **Build Message** \u2192 Create personalized SMS\n6\ufe0f\u20e3 **Send SMS via Close** \u2192 Deliver follow-up text\n7\ufe0f\u20e3 **Calculate Next** \u2192 Determine next follow-up time\n8\ufe0f\u20e3 **Update Lead** \u2192 Update status in Close CRM\n9\ufe0f\u20e3 **Update Convex Queue** \u2192 Schedule next follow-up\n\n**Parallel:** Detect stalled conversations",
        "height": 464
      },
      "id": "sticky-flow-new",
      "name": "\ud83d\udd04 Data Flow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -352,
        9904
      ]
    },
    {
      "parameters": {
        "content": "## Integrations\n\n**Queue Management:**\n- Convex (follow-up queue & scheduling)\n\n**CRM:**\n- Close CRM (lead data & SMS sending)\n\n**Notifications:**\n- Slack (success reports)\n\n**Rate Limiting:**\n- 1 SMS per second to prevent throttling\n\n**Cleanup:**\n- Auto-removes stale leads from queue",
        "height": 496
      },
      "id": "sticky-integrations-new",
      "name": "\ud83d\udd0c Integrations",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        16,
        10048
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/follow-up-queue-cancel",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"lead_id\": \"{{ $('Build Message').first().json.leadId }}\",\n  \"reason\": \"sms_send_failed\"\n}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "delete-failed-from-queue",
      "name": "Delete Failed Lead from Queue",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2640,
        10608
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "check-not-skip",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.action }}",
              "rightValue": "SKIP"
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "check-should-send",
      "name": "Should Send SMS?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1456,
        10368
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/queue-skip",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ lead_id: $json.lead_id, phone: $json.phone, skip_reason: $json.reason, skip_type: $json.skipType }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "handle-skip",
      "name": "Handle Skip (Update Queue)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2048,
        10784
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "name": "leadId",
              "value": "={{ $json.leadId }}",
              "type": "string"
            },
            {
              "name": "phone",
              "value": "={{ $json.phone }}",
              "type": "string"
            },
            {
              "name": "templateId",
              "value": "={{ $json.templateId }}",
              "type": "string"
            },
            {
              "name": "templateVariant",
              "value": "={{ $json.templateVariant }}",
              "type": "string"
            },
            {
              "name": "messageToSend",
              "value": "={{ $json.messageToSend }}",
              "type": "string"
            },
            {
              "name": "sequencePosition",
              "value": "={{ $json.sequencePosition }}",
              "type": "number"
            },
            {
              "name": "assignedPhone",
              "value": "={{ $json.assignedPhone.phone || $json.assignedPhone }}",
              "type": "string"
            },
            {
              "name": "contactId",
              "value": "={{ $json.contactId }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "store-message-data",
      "name": "Store Message Data",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1664,
        10352
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/phone-update",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ phone: $('Store Message Data').item.json.assignedPhone, event_type: 'send' }) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          },
          "timeout": 15000
        }
      },
      "id": "update-phone-stats",
      "name": "Update Phone Stats",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2176,
        9968
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "not-dnc-status",
              "leftValue": "={{ ($json.status_label || '').toLowerCase() }}",
              "rightValue": "dnc",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-human-hand-status",
              "leftValue": "={{ ($json.status_label || '').toLowerCase() }}",
              "rightValue": "human hand",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-hostile-status",
              "leftValue": "={{ ($json.status_label || '').toLowerCase() }}",
              "rightValue": "hostile",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-paused-status",
              "leftValue": "={{ ($json.status_label || '').toLowerCase() }}",
              "rightValue": "paused",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-dnc-pipeline",
              "leftValue": "={{ String(($json.custom || {}).cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || ($json.custom || {})['Pipeline Stage'] || '').toLowerCase() }}",
              "rightValue": "dnc",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-hot-pipeline",
              "leftValue": "={{ String(($json.custom || {}).cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || ($json.custom || {})['Pipeline Stage'] || '').toLowerCase() }}",
              "rightValue": "hot",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-warm-pipeline",
              "leftValue": "={{ String(($json.custom || {}).cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || ($json.custom || {})['Pipeline Stage'] || '').toLowerCase() }}",
              "rightValue": "warm",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-cold-pipeline",
              "leftValue": "={{ String(($json.custom || {}).cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || ($json.custom || {})['Pipeline Stage'] || '').toLowerCase() }}",
              "rightValue": "cold",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-human-hand-pipeline",
              "leftValue": "={{ String(($json.custom || {}).cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || ($json.custom || {})['Pipeline Stage'] || '').toLowerCase() }}",
              "rightValue": "human hand",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-hostile-pipeline",
              "leftValue": "={{ String(($json.custom || {}).cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || ($json.custom || {})['Pipeline Stage'] || '').toLowerCase() }}",
              "rightValue": "hostile",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-wrong-contact",
              "leftValue": "={{ String(($json.custom || {}).cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || ($json.custom || {})['Pipeline Stage'] || '').toLowerCase() }}",
              "rightValue": "wrong contact",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-creative-finance",
              "leftValue": "={{ String(($json.custom || {}).cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || ($json.custom || {})['Pipeline Stage'] || '').toLowerCase() }}",
              "rightValue": "creative finance",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-under-contract",
              "leftValue": "={{ String(($json.custom || {}).cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || ($json.custom || {})['Pipeline Stage'] || '').toLowerCase() }}",
              "rightValue": "under contract",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            },
            {
              "id": "not-appointment",
              "leftValue": "={{ String(($json.custom || {}).cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || ($json.custom || {})['Pipeline Stage'] || '').toLowerCase() }}",
              "rightValue": "appointment",
              "operator": {
                "type": "string",
                "operation": "notContains"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "check-lead-status",
      "name": "Check Lead Status",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2224,
        10544
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "// SKIP EXCLUDED STATUS v7.4 - Now calls queue-skip API to cancel\nconst CONVEX_URL = 'https://YOUR_CONVEX_DEPLOYMENT.convex.site';\n\nconst rawInput = $input.item.json;\n\nif (!rawInput || typeof rawInput !== 'object' || Array.isArray(rawInput)) {\n  console.log('Invalid input - not an object');\n  return [{ json: { status: 'skipped', reason: 'invalid_input', data: null } }];\n}\n\nconst item = rawInput;\nconst leadId = item.id || '';\nconst contact = (item.contacts && item.contacts[0]) || {};\nconst phones = contact.phones || [];\nconst phone = phones.length > 0 ? phones[0].phone : '';\nconst statusId = item.status_id || '';\nconst custom = item.custom || {};\nconst pipeline = custom.cf_POWtXhMQwwrBsrlkocDvtA5Fs57DvdKjwomTTgTP5vW || '';\n\n// Determine skip reason\nvar skipType = 'excluded_status';\nvar reason = 'Close CRM status excluded';\n\nif (statusId === 'stat_AVA3WALf67nRg7DZXeNckA5RS0ifVRtcBJqeReNs9D6') {\n  skipType = 'human_hand';\n  reason = 'HUMAN HAND status - requires manual handling';\n} else if (pipeline === 'DNC') {\n  skipType = 'dnc';\n  reason = 'DNC pipeline - opted out';\n} else if (pipeline === 'Hot Lead' || pipeline === 'Warm Lead') {\n  skipType = 'hot_warm';\n  reason = pipeline + ' - active engagement, skip auto-sequence';\n}\n\nconsole.log('SKIP:', leadId, '| Reason:', reason, '| Type:', skipType);\n\n// Call queue-skip API to cancel/update the queue entry\nif (leadId && phone) {\n  try {\n    await this.helpers.httpRequest({\n      method: 'POST',\n      url: CONVEX_URL + '/api/queue-skip',\n      body: {\n        lead_id: leadId,\n        phone: phone,\n        skip_reason: reason,\n        skip_type: skipType\n      },\n      json: true\n    });\n    console.log('Queue updated for skipped lead');\n  } catch (err) {\n    console.log('Queue-skip API error:', err.message);\n  }\n}\n\nreturn [{ json: { status: 'skipped', reason: reason, skipType: skipType, lead_id: leadId, phone: phone } }];"
      },
      "id": "skip-excluded-status",
      "name": "Skip (Excluded Status)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1792,
        10784
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "has-data",
              "leftValue": "={{ $json.id }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "check-api-error",
      "name": "Check API Error",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1456,
        10608
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/follow-up-queue-delete",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"lead_id\": \"{{ $('Rate Limiter (1 per sec)').first().json._queue.lead_id }}\",\n  \"phone\": \"{{ $('Rate Limiter (1 per sec)').first().json._queue.phone }}\"\n}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "handle-api-error",
      "name": "Handle API Error",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1248,
        10768
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/log-event",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ event_type: 'sms_cost', source: 'follow_up_engine', amount: 0.02, lead_id: $json.leadId, phone: $json.phone, sequence_position: $json.sequencePosition, template_id: $json.templateId, tier: $json.tier || 'unknown', listing_type: $json.listingType || $json.listing_type || 'unknown' }) }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "log-sms-cost",
      "name": "Log SMS Cost",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2832,
        10320
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/api/log-sms-sent",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"workflow\": \"seller_follow_up_engine\",\n  \"text_position\": {{ $('Build Message').item.json.sequence_position || 2 }},\n  \"lead_id\": \"{{ $('Build Message').item.json.lead_id || '' }}\",\n  \"phone\": \"{{ $('Build Message').item.json.phone || '' }}\",\n  \"address\": \"{{ $('Build Message').item.json.address || '' }}\",\n  \"template_id\": \"{{ $('Build Message').item.json.template_id || '' }}\"\n}",
        "options": {}
      },
      "id": "log-sms-sent-followup",
      "name": "Log SMS Sent (Follow-Up)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2176,
        9728
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "url": "=https://api.close.com/api/v1/lead/{{ $json.lead_id }}/?_fields=id,display_name,contacts,custom,activities,status_id,status_label",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "options": {}
      },
      "id": "c0f958e3-794e-4d61-b26b-03a6708aa614",
      "name": "Get Lead data",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1296,
        10608
      ],
      "executeOnce": false,
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "notes": "Gets custom field IDs to map correctly"
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        []
      ]
    },
    "Initialize Config": {
      "main": [
        [
          {
            "node": "Should Run?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Should Run?": {
      "main": [
        [
          {
            "node": "Get Stalled Conversations",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Due from Convex",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch Due from Convex",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1s Delay": {
      "main": [
        [
          {
            "node": "Send SMS via Close",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send SMS via Close": {
      "main": [
        [
          {
            "node": "SMS Sent OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SMS Sent OK?": {
      "main": [
        [
          {
            "node": "Calculate Next Follow-Up",
            "type": "main",
            "index": 0
          },
          {
            "node": "Record to Convex (Follow-Up)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Update Phone Stats",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log SMS Cost",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log SMS Sent (Follow-Up)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log SMS Failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Next Follow-Up": {
      "main": [
        [
          {
            "node": "Update Lead (Success)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Lead (Success)": {
      "main": [
        [
          {
            "node": "Phase Transition?1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log SMS Failure": {
      "1": [
        [
          {
            "node": "Delete Failed Lead from Queue",
            "type": "0",
            "index": 0
          }
        ]
      ],
      "main": [
        [
          {
            "node": "Rate Limiter (1 per sec)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Delete Failed Lead from Queue",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Rate Limiter (1 per sec)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Stalled Conversations": {
      "main": [
        [
          {
            "node": "Detect Stalled Conversations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect Stalled Conversations": {
      "main": [
        [
          {
            "node": "Has Stalled?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Stalled?": {
      "main": [
        [
          {
            "node": "Mark as Stalled",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Phase Transition?1": {
      "main": [
        [
          {
            "node": "Update Status to Extended1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Track Success1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Status to Extended1": {
      "main": [
        [
          {
            "node": "Track Success1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Due from Convex": {
      "main": [
        [
          {
            "node": "Split Lead Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Track Success1": {
      "main": [
        [
          {
            "node": "Update Convex Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Convex Queue": {
      "main": [
        [
          {
            "node": "Rate Limiter (1 per sec)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lead Exists in Close?": {
      "main": [
        [
          {
            "node": "Check Lead Status",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Delete Stale Lead from Convex",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Delete Stale Lead from Convex": {
      "main": [
        [
          {
            "node": "Log Deleted Stale Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Lead Items": {
      "main": [
        [
          {
            "node": "Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rate Limiter (1 per sec)": {
      "main": [
        [
          {
            "node": "Collect Success Stats1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Lead data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Deleted Stale Lead": {
      "main": [
        [
          {
            "node": "Rate Limiter (1 per sec)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Limit": {
      "main": [
        [
          {
            "node": "Rate Limiter (1 per sec)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Delete Failed Lead from Queue": {
      "main": [
        [
          {
            "node": "Rate Limiter (1 per sec)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Message": {
      "main": [
        [
          {
            "node": "Should Send SMS?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handle Skip (Update Queue)": {
      "main": [
        [
          {
            "node": "Rate Limiter (1 per sec)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store Message Data": {
      "main": [
        [
          {
            "node": "1s Delay",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Should Send SMS?": {
      "main": [
        [
          {
            "node": "Store Message Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Skip (Update Queue)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark as Stalled": {
      "main": [
        [
          {
            "node": "Slack Success1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip (Excluded Status)": {
      "main": [
        [
          {
            "node": "Rate Limiter (1 per sec)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Lead Status": {
      "main": [
        [
          {
            "node": "Build Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skip (Excluded Status)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check API Error": {
      "main": [
        [
          {
            "node": "Lead Exists in Close?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle API Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handle API Error": {
      "main": [
        [
          {
            "node": "Rate Limiter (1 per sec)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Lead data": {
      "main": [
        [
          {
            "node": "Check API Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "availableInMCP": false,
    "callerPolicy": "workflowsFromSameOwner",
    "timeSavedMode": "fixed",
    "timezone": "America/New_York",
    "binaryMode": "separate"
  },
  "active": false
}