{
  "name": "Text back missed calls with Twilio for HVAC, plumbing and home service businesses",
  "tags": [],
  "nodes": [
    {
      "id": "2a5a3e82-2948-4068-823c-5363f4f542ff",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -768,
        -128
      ],
      "parameters": {
        "width": 480,
        "height": 864,
        "content": "## Text back missed calls with Twilio for HVAC, plumbing and home service businesses\n\n### How it works\n\nThis workflow helps home service businesses automatically respond to missed phone calls. It first answers Twilio's inbound call webhook with TwiML to forward the call, then listens for Twilio status callbacks to determine whether the call was missed. When a missed call is detected, it deduplicates repeat callers, builds a text-back message, sends the customer an SMS, and notifies the owner in Telegram.\n\n### Setup steps\n\n- Configure the Twilio incoming call webhook to point to the Incoming Call webhook URL.\n- Replace YOUR-N8N-HOST in the TwiML with your n8n domain so Twilio posts the dial outcome back to the Call Status webhook.\n- Set up Twilio credentials for sending SMS and confirm the sending phone number is SMS-enabled.\n- Set up Telegram bot credentials and the owner's chat ID for notifications.\n- Edit the TwiML forwarding destination so calls route to the correct business phone number.\n- Review the code nodes for the repeat-caller suppression window and message content.\n\n### Customization\n\nCustomize the text-back copy, business name, owner notification format, and deduplication time window in the code nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "cfa244b4-c668-43e9-8717-4e8f57242c87",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -208,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Forward incoming calls\n\nReceives Twilio's incoming call webhook and immediately returns TwiML to forward or dial the business phone line."
      },
      "typeVersion": 1
    },
    {
      "id": "135e4bb9-9478-436e-bcff-e905f218d38e",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -208,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 304,
        "content": "## Receive call status\n\nHandles Twilio call status callbacks, acknowledges the webhook, and checks whether the call outcome qualifies as missed."
      },
      "typeVersion": 1
    },
    {
      "id": "2b8e4e8e-1501-4f6c-89fc-81e7c04a4472",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Prepare missed-call text\n\nSuppresses repeat callers within the configured window, then builds the customer text-back message and owner notification payload."
      },
      "typeVersion": 1
    },
    {
      "id": "c27338a6-b4ff-42dd-aefa-2d89a62cc8ed",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        928,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 576,
        "content": "## Send follow-up alerts\n\nSends the SMS text-back to the missed caller through Twilio and notifies the business owner through Telegram."
      },
      "typeVersion": 1
    },
    {
      "id": "webhook-incoming-call",
      "name": "When Incoming Call Received",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -160,
        40
      ],
      "parameters": {
        "path": "incoming-call",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "respond-twiml-dial",
      "name": "Forward Call With TwiML",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        60,
        40
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/xml"
              }
            ]
          }
        },
        "respondWith": "text",
        "responseBody": "=<Response><Dial timeout=\"20\" callerId=\"{{ $json.body.To }}\" action=\"https://YOUR-N8N-HOST/webhook/call-status\" method=\"POST\"><Number>+10000000000</Number></Dial></Response>"
      },
      "typeVersion": 1.1
    },
    {
      "id": "webhook-call-status",
      "name": "When Call Status Updated",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -160,
        384
      ],
      "parameters": {
        "path": "call-status",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "respond-ack",
      "name": "Acknowledge Call Status",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        64,
        384
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/xml"
              }
            ]
          }
        },
        "respondWith": "text",
        "responseBody": "<Response/>"
      },
      "typeVersion": 1.1
    },
    {
      "id": "if-call-missed",
      "name": "If Call Was Missed",
      "type": "n8n-nodes-base.if",
      "position": [
        288,
        384
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "call-was-missed",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ ['no-answer', 'busy', 'failed', 'canceled'].includes($json.body.DialCallStatus) }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "code-dedupe",
      "name": "Filter Repeat Callers",
      "type": "n8n-nodes-base.code",
      "position": [
        528,
        176
      ],
      "parameters": {
        "jsCode": "// Only text a caller once per window, even if they redial several times.\nconst store = $getWorkflowStaticData('global');\nstore.recentCallers = store.recentCallers || {};\n\nconst WINDOW_MS = 2 * 60 * 60 * 1000; // 2 hours\nconst now = Date.now();\n\n// Forget anyone outside the window so the store cannot grow forever.\nfor (const [number, seenAt] of Object.entries(store.recentCallers)) {\n  if (now - seenAt > WINDOW_MS) delete store.recentCallers[number];\n}\n\nconst fresh = [];\nfor (const item of $input.all()) {\n  const from = item.json.body?.From;\n  if (!from) continue;\n  if (store.recentCallers[from]) continue;\n  store.recentCallers[from] = now;\n  fresh.push(item);\n}\n\nreturn fresh;"
      },
      "typeVersion": 2
    },
    {
      "id": "code-build-message",
      "name": "Create Text Message Content",
      "type": "n8n-nodes-base.code",
      "position": [
        752,
        176
      ],
      "parameters": {
        "jsCode": "// ---------- EDIT THIS BLOCK ----------\nconst BUSINESS_NAME = 'Your Company';\nconst TIMEZONE = 'America/Chicago';\nconst OPEN_HOUR = 8;   // 24h clock\nconst CLOSE_HOUR = 17; // 24h clock\nconst WORKDAYS = [1, 2, 3, 4, 5]; // 0 = Sunday\n// -------------------------------------\n\nconst local = new Date(new Date().toLocaleString('en-US', { timeZone: TIMEZONE }));\nconst isOpen =\n  WORKDAYS.includes(local.getDay()) &&\n  local.getHours() >= OPEN_HOUR &&\n  local.getHours() < CLOSE_HOUR;\n\nreturn $input.all().map((item) => {\n  const body = item.json.body || {};\n  const fromNumber = body.From;\n  const businessNumber = body.To;\n\n  const smsBody = isOpen\n    ? `Sorry we missed your call at ${BUSINESS_NAME} - we are on a job right now. Reply with your name, address and what is going on, and we will call you straight back. Reply STOP to opt out.`\n    : `Thanks for calling ${BUSINESS_NAME}. We are closed right now and open again at ${OPEN_HOUR}:00. Reply with your name, address and the issue and you will be first in line in the morning. Reply STOP to opt out.`;\n\n  const ownerAlert = `Missed call from ${fromNumber}\\nStatus: ${body.DialCallStatus}\\nHours: ${isOpen ? 'open' : 'after hours'}\\nText-back sent.`;\n\n  return { json: { fromNumber, businessNumber, smsBody, ownerAlert, isOpen } };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "twilio-send-sms",
      "name": "Send SMS Text-Back",
      "type": "n8n-nodes-base.twilio",
      "position": [
        976,
        80
      ],
      "parameters": {
        "to": "={{ $json.fromNumber }}",
        "from": "={{ $json.businessNumber }}",
        "message": "={{ $json.smsBody }}",
        "options": {},
        "resource": "sms",
        "operation": "send",
        "toWhatsapp": false
      },
      "typeVersion": 1
    },
    {
      "id": "telegram-notify-owner",
      "name": "Alert Business Owner via Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        976,
        304
      ],
      "parameters": {
        "text": "={{ $json.ownerAlert }}",
        "chatId": "REPLACE_WITH_YOUR_CHAT_ID",
        "resource": "message",
        "operation": "sendMessage",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "typeVersion": 1.2
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "If Call Was Missed": {
      "main": [
        [
          {
            "node": "Filter Repeat Callers",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Filter Repeat Callers": {
      "main": [
        [
          {
            "node": "Create Text Message Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Acknowledge Call Status": {
      "main": [
        [
          {
            "node": "If Call Was Missed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Call Status Updated": {
      "main": [
        [
          {
            "node": "Acknowledge Call Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Text Message Content": {
      "main": [
        [
          {
            "node": "Send SMS Text-Back",
            "type": "main",
            "index": 0
          },
          {
            "node": "Alert Business Owner via Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Incoming Call Received": {
      "main": [
        [
          {
            "node": "Forward Call With TwiML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}