{
  "name": "LDS - Missed-Call Speed-to-Lead (DEMO)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "lds-missed-call",
        "responseMode": "onReceived",
        "options": {}
      },
      "id": "w2-webhook",
      "name": "Missed call received",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -520,
        300
      ],
      "notes": "Point your phone system / VoIP at this URL. Swap for Twilio voice webhook on a live client."
    },
    {
      "parameters": {
        "jsCode": "\n// Triages a missed inbound call. Demo mode: if the webhook payload is empty the\n// sample below is used, so the node runs and is testable without a live telephony feed.\nconst SAMPLE = { caller_number: '+14035550142', business_name: 'Example Plumbing',\n                 called_at: '2026-10-05T14:32:00-06:00', call_status: 'no-answer' };\nconst raw = ($json && $json.body && Object.keys($json.body).length) ? $json.body : SAMPLE;\n\n// Normalise North American numbers to E.164. A bare 10-digit local number is valid,\n// NOT an error - rejecting it was a bug that would have dropped real leads.\nconst digits = String(raw.caller_number || '').replace(/[^0-9]/g, '');\nlet e164 = '';\nif (digits.length === 10) e164 = '+1' + digits;\nelse if (digits.length === 11 && digits.charAt(0) === '1') e164 = '+' + digits;\nelse if (digits.length > 11) e164 = '+' + digits;\n\nconst calledAt = new Date(raw.called_at || Date.now());\n// America/Edmonton is UTC-6 (MDT) in October\nconst localHour = (calledAt.getUTCHours() - 6 + 24) % 24;\nconst isMissedCall = (raw.call_status || 'no-answer') !== 'answered';\n// After hours is when a text-back matters MOST, so it never blocks one.\nconst isAfterHours = !(localHour >= 8 && localHour < 18);\n\nlet skip = null;\nif (!e164) skip = 'unusable caller ID';\nelse if (!isMissedCall) skip = 'call was answered - no text-back needed';\n\nreturn [{\n  caller_number: e164 || null,\n  caller_number_raw: raw.caller_number || '',\n  business_name: raw.business_name || 'Unknown business',\n  called_at_local: calledAt.toISOString(),\n  local_hour: localHour,\n  is_missed_call: isMissedCall,\n  is_after_hours: isAfterHours,\n  text_back: skip ? 'no' : 'yes',\n  skip_reason: skip,\n  followup_minutes: [0, 60, 1440],\n  followup_touches: 3,\n}];\n"
      },
      "id": "w2-normalize",
      "name": "Normalize + triage",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -290,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $json.text_back }}",
              "rightValue": "yes",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "w2-if",
      "name": "Send text-back?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -60,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "\n// Builds the text-back body plus the owner alert. No side effects: pure string work.\nconst d = $json;\nconst firstTouch  = `Hi, sorry we missed your call to ${d.business_name}. What can we help with? Reply here and we'll get right back to you.`;\nconst secondTouch = `Just following up on our missed call earlier - still need a hand? We can book you in today.`;\nconst ownerAlert  = `MISSED CALL: ${d.caller_number} at ${d.local_hour}:00. Automatic text-back sent; ` +\n                    `${d.followup_touches} follow-up touches scheduled at ${d.followup_minutes.join(', ')} minutes.`;\nreturn [{ ...d, text_back_body: firstTouch, followup_body: secondTouch,\n          owner_alert_body: ownerAlert }];\n"
      },
      "id": "w2-compose",
      "name": "Compose text-back",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        180,
        180
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://REPLACE-WITH-TWILIO-OR-GHL-ENDPOINT",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ to: $json.caller_number, body: $json.text_back_body }) }}",
        "options": {}
      },
      "id": "w2-sms",
      "name": "Text back the caller (SMS)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        420,
        120
      ],
      "notes": "Swap for the Twilio node, or GoHighLevel if the client already runs it. Needs credentials."
    },
    {
      "parameters": {
        "sendTo": "={{ $env.OWNER_ALERT_EMAIL || 'owner@example.com' }}",
        "subject": "=Missed call from {{ $json.caller_number }} - text-back sent",
        "message": "={{ $json.owner_alert_body }}",
        "options": {}
      },
      "id": "w2-owner",
      "name": "Alert the owner",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        420,
        300
      ]
    },
    {
      "parameters": {},
      "id": "w2-log",
      "name": "No text-back",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        180,
        460
      ]
    },
    {
      "parameters": {},
      "id": "w2-skip",
      "name": "No usable caller ID",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        -60,
        460
      ]
    }
  ],
  "connections": {
    "Missed call received": {
      "main": [
        [
          {
            "node": "Normalize + triage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize + triage": {
      "main": [
        [
          {
            "node": "Send text-back?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send text-back?": {
      "main": [
        [
          {
            "node": "Compose text-back",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No usable caller ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compose text-back": {
      "main": [
        [
          {
            "node": "Text back the caller (SMS)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Alert the owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "tags": []
}