{
  "name": "Missed Call Recovery — Text Back in 30 Seconds",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "missed-call",
        "options": {}
      },
      "id": "b2000000-0000-4000-8000-000000000001",
      "name": "Missed call event",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-640, 0],
      "webhookId": "b2000000-0000-4000-8000-000000000001",
      "notesInFlow": true,
      "notes": "Point your phone system's 'no answer' / 'voicemail left' webhook here. Expected fields: from, to, callStatus, durationSec."
    },
    {
      "parameters": {
        "jsCode": "// Decide whether this missed call deserves an automatic text back.\n// Rules are intentionally boring: no model, no guessing, fully auditable.\n\nconst call = $input.first().json;\nconst from = call.From || call.from;\nconst status = String(call.CallStatus || call.callStatus || '').toLowerCase();\nconst duration = Number(call.CallDuration || call.durationSec || 0);\n\nconst MISSED = ['no-answer', 'busy', 'failed', 'voicemail'];\n\n// Skip anything that is not a real missed inbound call from a mobile-looking number.\nconst isMissed = MISSED.includes(status) || (status === 'completed' && duration < 10);\nconst isValidNumber = /^\\+?[1-9]\\d{7,14}$/.test(String(from || '').replace(/[^+\\d]/g, ''));\n\n// Business hours in the clinic's own timezone.\nconst tz = $env.CLINIC_TIMEZONE || 'America/New_York';\nconst local = new Date(new Date().toLocaleString('en-US', { timeZone: tz }));\nconst hour = local.getHours();\nconst day = local.getDay();\nconst isOpen = day >= 1 && day <= 5 && hour >= 8 && hour < 17;\n\nreturn [{\n  json: {\n    from,\n    shouldText: isMissed && isValidNumber,\n    isOpen,\n  },\n}];"
      },
      "id": "b2000000-0000-4000-8000-000000000002",
      "name": "Should we text back?",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-420, 0]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "cond-should-text",
              "leftValue": "={{ $json.shouldText }}",
              "rightValue": true,
              "operator": { "type": "boolean", "operation": "true", "singleValue": true }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b2000000-0000-4000-8000-000000000003",
      "name": "Valid missed call?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [-200, 0]
    },
    {
      "parameters": {
        "resource": "sms",
        "operation": "send",
        "from": "={{ $env.CLINIC_SMS_NUMBER }}",
        "to": "={{ $json.from }}",
        "message": "=Hi, this is {{ $env.CLINIC_NAME }} — sorry we missed your call.{{ $json.isOpen ? ' We are with patients right now.' : ' We are currently closed.' }} Reply here and we will help: 1 for an appointment, 2 for a prescription refill, 3 for billing, 4 for something else. If this is a medical emergency, call 911.",
        "options": {}
      },
      "id": "b2000000-0000-4000-8000-000000000004",
      "name": "Text the caller back",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [40, -100],
      "notesInFlow": true,
      "notes": "Send within 30 seconds. Response rates fall off a cliff after about two minutes — the caller has already dialled the next clinic."
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": { "__rl": true, "value": "={{ $env.MISSED_CALL_SHEET_ID }}", "mode": "id" },
        "sheetName": { "__rl": true, "value": "Missed calls", "mode": "name" },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Time": "={{ $now.toISO() }}",
            "Caller": "={{ $json.from }}",
            "During hours": "={{ $json.isOpen ? 'Yes' : 'No' }}",
            "Texted back": "Yes",
            "Outcome": "Awaiting reply"
          }
        },
        "options": {}
      },
      "id": "b2000000-0000-4000-8000-000000000005",
      "name": "Log the missed call",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [40, 100],
      "notesInFlow": true,
      "notes": "Keeping the log is what lets you prove the recovered-revenue number later. Swap for your CRM if you have one."
    },
    {
      "parameters": {
        "amount": 15,
        "unit": "minutes"
      },
      "id": "b2000000-0000-4000-8000-000000000006",
      "name": "Wait 15 minutes",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [280, -100],
      "webhookId": "b2000000-0000-4000-8000-000000000006"
    },
    {
      "parameters": {
        "resource": "sms",
        "operation": "send",
        "from": "={{ $env.CLINIC_SMS_NUMBER }}",
        "to": "={{ $env.FRONT_DESK_NUMBER }}",
        "message": "=Missed call from {{ $json.from }} — texted back, no reply after 15 minutes. Worth a call back.",
        "options": {}
      },
      "id": "b2000000-0000-4000-8000-000000000007",
      "name": "Nudge the front desk",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [520, -100],
      "notesInFlow": true,
      "notes": "Automation handles the easy 70%. A human still closes the rest — this node is what stops callers falling through the gap."
    }
  ],
  "connections": {
    "Missed call event": {
      "main": [[{ "node": "Should we text back?", "type": "main", "index": 0 }]]
    },
    "Should we text back?": {
      "main": [[{ "node": "Valid missed call?", "type": "main", "index": 0 }]]
    },
    "Valid missed call?": {
      "main": [
        [
          { "node": "Text the caller back", "type": "main", "index": 0 },
          { "node": "Log the missed call", "type": "main", "index": 0 }
        ],
        []
      ]
    },
    "Text the caller back": {
      "main": [[{ "node": "Wait 15 minutes", "type": "main", "index": 0 }]]
    },
    "Wait 15 minutes": {
      "main": [[{ "node": "Nudge the front desk", "type": "main", "index": 0 }]]
    }
  },
  "pinData": {},
  "settings": { "executionOrder": "v1" },
  "tags": [{ "name": "healthcare" }, { "name": "front desk" }]
}
