{
  "name": "Missed-call text-back (EGYRICAN sample)",
  "nodes": [
    {
      "parameters": {
        "content": "## Missed-call text-back (EGYRICAN sample)\nFictional business: Bright Smile Dental, Astoria NY.\n\n1. In Twilio, point the number's **Dial action URL** at the *Call ended* webhook.\n2. Point the number's **Messaging webhook** at the *SMS reply* webhook.\n3. Fill in the Twilio, Google Sheets and SMTP credentials, and the settings in *Decide*.",
        "height": 220,
        "width": 520
      },
      "id": "ec1015df-242c-563c-8fb1-970a6b5a04d9",
      "name": "Sticky note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -260
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "missed-call",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "e3cb76f0-3b3b-5ddb-9911-725d921946ec",
      "name": "Call ended",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "webhookId": "2845d7db-22dc-5045-881b-f96fb74dc861"
    },
    {
      "parameters": {
        "jsCode": "// Settings for the (fictional) practice. Change these for a real client.\nconst cfg = {\n  businessName: 'Bright Smile Dental',\n  bookingLink: 'https://example.com/book',\n  opensAt: 8,          // 8am\n  closesAt: 18,        // 6pm\n  timezone: 'America/New_York',\n  cooldownHours: 12,   // text a number at most once in this window\n};\n\n// Twilio posts the call as form fields. DialCallStatus is set when the call was\n// forwarded to the front desk and nobody picked up.\nconst body = $json.body || {};\nconst caller = body.From || '';\nconst status = String(body.DialCallStatus || body.CallStatus || '').toLowerCase();\nconst missed = ['no-answer', 'busy', 'failed', 'canceled'].includes(status);\n\nconst hour = Number(new Date().toLocaleString('en-US', { hour: 'numeric', hourCycle: 'h23', timeZone: cfg.timezone }));\nconst afterHours = hour < cfg.opensAt || hour >= cfg.closesAt;\n\n// Remember who we texted so a caller who rings three times gets one text, not three.\n// (Static data is saved for production runs of an active workflow.)\nconst memory = $getWorkflowStaticData('global');\nmemory.lastText = memory.lastText || {};\nconst now = Date.now();\nconst textedRecently = memory.lastText[caller] && now - memory.lastText[caller] < cfg.cooldownHours * 3600 * 1000;\nconst validMobile = /^\\+1\\d{10}$/.test(caller);\n\nconst shouldText = missed && validMobile && !textedRecently;\nif (shouldText) memory.lastText[caller] = now;\n\nconst smsText = afterHours\n  ? `Hi, this is ${cfg.businessName}. Sorry we missed your call, we're closed right now. Reply 1 to book online, 2 and we'll call you first thing tomorrow, or just text us what you need.`\n  : `Hi, this is ${cfg.businessName}. Sorry we missed your call, we're with a patient. Reply 1 to book online, 2 for a call back within the hour, or text us your question.`;\n\n// What the caller hears when Twilio asks what to do after the forwarded call ends.\nconst say = (t) => `<Say voice=\"Polly.Joanna\">${t}</Say>`;\nconst twiml = shouldText\n  ? say('Sorry we missed you. We just sent you a text message so you can book or ask for a call back.')\n  : missed\n    ? say('Sorry we missed you. Please leave your name and number after the tone and we will call you back.') + '<Record maxLength=\"120\"/>'\n    : '';\n\nreturn [{\n  json: {\n    receivedAt: new Date().toISOString(),\n    caller,\n    businessNumber: body.To || '',\n    callSid: body.CallSid || '',\n    status,\n    missed,\n    afterHours,\n    skipped: !shouldText ? (!missed ? 'answered' : !validMobile ? 'not a US mobile number' : 'already texted') : '',\n    shouldText,\n    smsText,\n    twiml,\n  },\n}];\n"
      },
      "id": "a1627ddc-0de5-5371-bfb8-cbfbec9ddef0",
      "name": "Decide",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "=<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response>{{ $json.twiml }}</Response>",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/xml"
              }
            ]
          }
        }
      },
      "id": "ffbba930-d670-558e-94ad-d6bb34a6fc28",
      "name": "Answer Twilio",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        440,
        -140
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "00cad27d-325b-5f02-9e6f-ecc2d15e05d6",
              "leftValue": "={{ $json.shouldText }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "f7d528ea-175a-50ab-a125-0dbaeb098451",
      "name": "Missed and new?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        440,
        60
      ]
    },
    {
      "parameters": {
        "from": "={{ $json.businessNumber }}",
        "to": "={{ $json.caller }}",
        "message": "={{ $json.smsText }}",
        "options": {}
      },
      "id": "a1a096e5-fdcb-52b1-88bd-cca655f8244b",
      "name": "Text the caller",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        680,
        0
      ],
      "credentials": {
        "twilioApi": {
          "id": "REPLACE",
          "name": "Twilio account"
        }
      }
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/REPLACE_WITH_YOUR_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Missed calls"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "id": "11b17362-70e9-5aa1-b595-c9511598b2a4",
      "name": "Log missed call",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        900,
        0
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "sms-reply",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "0388555e-b6ce-591d-a373-c2efe9cbec94",
      "name": "SMS reply",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        360
      ],
      "webhookId": "e4efac86-6a9d-5ba6-9576-47793816c457"
    },
    {
      "parameters": {
        "jsCode": "const bookingLink = 'https://example.com/book';\nconst body = $json.body || {};\nconst from = body.From || '';\nconst text = String(body.Body || '').trim();\nconst word = text.toLowerCase();\n\nlet intent = 'question';\nif (/^(stop|unsubscribe|cancel|end|quit)$/.test(word)) intent = 'opt-out';\nelse if (/^1\\b|book|appointment|schedule/.test(word)) intent = 'book';\nelse if (/^2\\b|call me|call back|callback/.test(word)) intent = 'call back';\nelse if (/pain|swollen|bleeding|emergency|urgent|broke/.test(word)) intent = 'urgent';\n\nconst replies = {\n  'book': `Great! Pick any open time here: ${bookingLink}`,\n  'call back': \"Thanks! Someone from our front desk will call you soon.\",\n  'urgent': \"We're sorry you're in pain. We've flagged this as urgent and someone will call you right away. If it's a medical emergency, call 911.\",\n  'question': \"Thanks, we got your message. Someone from our front desk will reply shortly.\",\n  // Twilio sends its own confirmation for STOP, so we reply with nothing.\n  'opt-out': '',\n};\n\nconst xml = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n\nreturn [{\n  json: {\n    receivedAt: new Date().toISOString(),\n    from,\n    message: text,\n    intent,\n    autoReply: replies[intent],\n    twiml: replies[intent] ? `<Message>${xml(replies[intent])}</Message>` : '',\n    subject: `${intent === 'urgent' ? 'URGENT: ' : ''}Text from ${from}: ${intent}`,\n    staffNote: `${from} replied to the missed-call text.\\n\\nTheir message: \"${text}\"\\nWhat they want: ${intent}\\nWe auto-replied: \"${replies[intent] || '(nothing, they opted out)'}\"`,\n  },\n}];\n"
      },
      "id": "79a87151-b670-5233-ba7d-eaaa1061b146",
      "name": "Read reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        360
      ]
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "=<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response>{{ $json.twiml }}</Response>",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/xml"
              }
            ]
          }
        }
      },
      "id": "90c093c8-b730-5f26-8cfd-86846a74e040",
      "name": "Answer the text",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        440,
        220
      ]
    },
    {
      "parameters": {
        "fromEmail": "alerts@example.com",
        "toEmail": "frontdesk@example.com",
        "subject": "={{ $json.subject }}",
        "emailFormat": "text",
        "text": "={{ $json.staffNote }}",
        "options": {}
      },
      "id": "95c2c178-9a31-5d39-b880-af42827fe585",
      "name": "Tell the front desk",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        440,
        420
      ],
      "credentials": {
        "smtp": {
          "id": "REPLACE",
          "name": "Front desk SMTP"
        }
      }
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/REPLACE_WITH_YOUR_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Replies"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "id": "89a9b272-6ae1-5255-a9af-2e42cd463ec2",
      "name": "Log reply",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        680,
        420
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE",
          "name": "Google Sheets account"
        }
      }
    }
  ],
  "connections": {
    "Call ended": {
      "main": [
        [
          {
            "node": "Decide",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decide": {
      "main": [
        [
          {
            "node": "Answer Twilio",
            "type": "main",
            "index": 0
          },
          {
            "node": "Missed and new?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Missed and new?": {
      "main": [
        [
          {
            "node": "Text the caller",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Text the caller": {
      "main": [
        [
          {
            "node": "Log missed call",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SMS reply": {
      "main": [
        [
          {
            "node": "Read reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read reply": {
      "main": [
        [
          {
            "node": "Answer the text",
            "type": "main",
            "index": 0
          },
          {
            "node": "Tell the front desk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tell the front desk": {
      "main": [
        [
          {
            "node": "Log reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "timezone": "America/New_York"
  },
  "pinData": {},
  "tags": [],
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
