Skip to content
New: a free website for your business, live in 48 to 72 hoursWorkflow automation on n8n and Claude, built for your businessFixed price, agreed before we start · Live in 5 to 10 business days30-day money-back guarantee on every planProfessional firms · Agencies · E-commerceYou own every workflow, in your own n8nClaude API: your data is not used for trainingHuman approval on every sensitive stepFree automation audit, no obligation
PURIST
30 days
Money-back window
14 h/wk
Modeled time saved
99.97%
Uptime SLA
< 7 days
Deploy time
PURIST AI
Claude Opus 4.7 · n8n v1.71 · <80ms
What type of business are you running? I'll show you exactly which processes we'd automate first and your estimated ROI.
Powered by n8n + Claude Opus 4.7 Get my free automation plan →
How to Build a Missed-Call SMS Recovery Automation With n8n (2026)
Guides 13 min read · 2,989 words

How to Build a Missed-Call SMS Recovery Automation With n8n (2026)

A working, step-by-step build of a missed-call text-back with n8n and Twilio: the Dial pattern that actually detects missed calls, safe SQL, deduplication, business hours, error alerts and the reply flow, with a live interactive figure.

P

Purist Team

September 8, 2026 · Updated October 24, 2026

In short. A missed-call text-back sends the caller a short, personal SMS the moment a call goes unanswered, so they stay with you instead of calling the next business. In n8n you need your phone provider's call webhook, an SMS provider such as Twilio, a lead table and a log, then you measure missed calls, send success and reply rate.

Why Missed Calls Are the Most Recoverable Lead Source Most Businesses Ignore

Service businesses miss calls for a good reason: the person who would answer is busy doing the work. The plumber is under a sink, the stylist has scissors in hand, the dentist is with a patient. The problem is what happens next. A caller who reaches voicemail and hangs up without a message rarely waits; they call the next business in their search results, and the job goes to whoever answers first.

A missed-call text-back closes that gap with a simple mechanism. The moment a call goes unanswered, the caller receives a short, personal SMS from the same number they dialled, inviting them to reply with what they need or to book a slot online. The caller is still holding their phone, still looking for help, and now has a conversation open with you instead of a voicemail beep. It is one of the highest return-per-hour builds in small business automation, which is why this tutorial covers the complete, working implementation rather than the concept.

Fig 1. The whole automation in one machine: the desk phone rings into a counter; if nobody answers by the fourth ring, a message card rides to the caller's phone and the booking comes back. Click the figure to answer in time and nothing is sent. Open full screen

How It Works in Five Steps

The figure above maps one to one onto the workflow you are about to build:

  1. 1 A customer calls your business number while you are busy or after hours.
  2. 2 Your phone rings for a fixed time (in North America a ring cycle is 2 seconds on, 4 seconds off, so four rings take about 24 seconds). Nobody picks up, so the call is marked as missed.
  3. 3 The missed-call event fires an n8n workflow. No human is involved.
  4. 4 A few seconds later, the caller receives a personal text with a booking link.
  5. 5 They reply or pick a slot, and the lead lands in your calendar and your CRM with its full history.

If you answer before the timeout, nothing is sent. That single rule is what keeps the automation invisible to the customers you do reach.

The Architecture That Actually Detects a Missed Call

Many tutorials point Twilio's "Call Status Changes" webhook at n8n and filter on `CallStatus = no-answer`. That does not work for inbound calls to a Twilio number: Twilio itself answers the inbound leg to run your instructions, so that leg almost always ends as `completed`. The reliable pattern is to let Twilio forward the call to your real phone with the TwiML `` verb, and read the result of that forwarded leg.

Here is the flow:

  • The customer calls your Twilio number.
  • Twilio runs a small TwiML document that dials your mobile or desk phone, with a timeout.
  • When the dial ends, Twilio posts `DialCallStatus` to the `action` URL, which is your n8n webhook.
  • `completed` means someone answered. `no-answer`, `busy` or `failed` means the call was missed, and the text-back runs.

What You'll Need Before Starting

An n8n instance (self-hosted or n8n Cloud) with a public HTTPS URL, a Twilio account with a local number that supports voice and SMS, and a database or CRM with an API. This tutorial uses Postgres for the logs and lead table; the same pattern applies to HubSpot, Pipedrive, GoHighLevel or Airtable. Twilio is used because voice forwarding and SMS live on one number, which matters: the text must come from the number the customer just called. Comparing providers? Our Twilio vs Plivo vs Vonage vs MessageBird comparison covers cost per message and deliverability.

In the United States, texting customers from a local 10-digit number requires A2P 10DLC registration (a brand and a campaign) in the Twilio console. Unregistered traffic is filtered or blocked by carriers, so register first: approval can take several days, and it is the most common reason a finished build sends nothing.

Step 1: Create the Tables

Two small tables keep the workflow honest: one logs every text you send (for deduplication and reporting), one holds the leads.

sql
CREATE TABLE sms_log (
  id           bigserial PRIMARY KEY,
  phone_number text NOT NULL,
  message_type text NOT NULL,
  status       text NOT NULL DEFAULT 'sent',
  error        text,
  sent_at      timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX sms_log_phone_time ON sms_log (phone_number, sent_at DESC);
-- leads, one row per phone number
CREATE TABLE leads (
  phone_number text PRIMARY KEY,
  source       text NOT NULL,
  status       text NOT NULL,
  last_message text,
  created_at   timestamptz NOT NULL DEFAULT now(),
  updated_at   timestamptz NOT NULL DEFAULT now()
);

Step 2: Forward the Call With a Timeout (TwiML)

In the Twilio console, create a TwiML Bin and set it as the number's "A call comes in" handler. It forwards the call to your phone and tells Twilio where to report the result.

xml
<Response>
  <Dial timeout="20" action="https://your-n8n-instance.com/webhook/dial-result" method="POST">
    <Number>+15557654321</Number>
  </Dial>
</Response>

The figure above uses four rings, about 24 seconds; the right value for you is whatever fits under your voicemail delay. The `timeout` is the most important number in the build. Set it a few seconds shorter than the time your own mobile takes to send calls to voicemail (often 20 to 25 seconds). If your voicemail picks up first, Twilio sees the forwarded leg as answered, reports `completed`, and the text-back never fires.

Step 3: Receive the Dial Result in n8n

Add a Webhook node on the path `dial-result`, method POST, with "Respond" set to "Using Respond to Webhook node". Twilio waits for TwiML from this URL, so the workflow must always answer it, even when nothing else happens.

Then route on the status. A Switch or IF node keeps it readable:

json
{
  "name": "Was the call missed?",
  "type": "n8n-nodes-base.if",
  "parameters": {
    "conditions": {
      "combinator": "and",
      "conditions": [
        {
          "leftValue": "={{ $json.body.DialCallStatus }}",
          "rightValue": "completed",
          "operator": { "type": "string", "operation": "notEquals" }
        }
      ]
    }
  }
}

On both branches, reply to Twilio with a Respond to Webhook node, Content-Type `text/xml`. On the missed branch you can tell the caller what is about to happen, which noticeably reduces callbacks to a busy line:

xml
<Response>
  <Say voice="Polly.Joanna">Sorry we could not pick up. We are sending you a text right now so you can book in seconds.</Say>
  <Hangup/>
</Response>

On the answered branch, reply with ``.

Step 4: Skip Repeat Callers (Deduplication)

People who cannot get through often call two or three times in a row. They should receive one text, not three. Check the log for a text sent to this number in the last 30 minutes, using a query parameter rather than pasting the phone number into the SQL string:

json
{
  "name": "Texted in the last 30 min?",
  "type": "n8n-nodes-base.postgres",
  "parameters": {
    "operation": "executeQuery",
    "query": "SELECT count(*)::int AS recent FROM sms_log WHERE phone_number = $1 AND sent_at > now() - interval '30 minutes'",
    "options": { "queryReplacement": "={{ $('Webhook').item.json.body.From }}" }
  }
}

Follow it with an IF node that continues only when `recent` equals 0. Because the query always returns one row, the branch logic stays explicit and you avoid the classic n8n trap where an empty result silently stops the workflow.

Step 5: Check Business Hours and Send the Text

A text at 11 pm can feel intrusive. Add a Code node that sets an `afterHours` flag from your opening hours and your time zone, then pick the message accordingly. Both versions are short, signed, and give two easy ways forward:

json
{
  "name": "Send text-back",
  "type": "n8n-nodes-base.twilio",
  "parameters": {
    "from": "={{ $env.TWILIO_PHONE_NUMBER }}",
    "to": "={{ $('Webhook').item.json.body.From }}",
    "message": "={{ $json.afterHours ? 'Hi, it is ' + $env.BUSINESS_NAME + '. We are closed right now but saw your call. Book the first slot tomorrow: ' + $env.BOOKING_URL + ' or reply here and we will call you back.' : 'Hi, it is ' + $env.BUSINESS_NAME + '. Sorry we missed you, we are with a customer. What do you need? Reply here or book a time: ' + $env.BOOKING_URL }}"
  },
  "onError": "continueErrorOutput"
}

The message copy matters more than it looks. Name the business in the first words so the text is not mistaken for spam, say why you missed the call, and ask one question. "What do you need?" gets replies; "Thank you for contacting us" does not.

Step 6: Log the Text and Create the Lead

Every missed call should create or update a lead, so a reply arrives on a record with context instead of as an orphaned text, and so you can count missed calls as a business metric.

json
{
  "name": "Upsert lead",
  "type": "n8n-nodes-base.postgres",
  "parameters": {
    "operation": "executeQuery",
    "query": "INSERT INTO leads (phone_number, source, status) VALUES ($1, 'missed_call', 'text_back_sent') ON CONFLICT (phone_number) DO UPDATE SET status = 'text_back_sent', updated_at = now()",
    "options": { "queryReplacement": "={{ $('Webhook').item.json.body.From }}" }
  }
}

A second Postgres node inserts a row into `sms_log` with `message_type = 'missed_call_textback'`. Keep both writes after the send, so the log only ever records texts that actually left.

Step 7: Handle the Reply

A second workflow handles the conversation. Set the number's "A message comes in" webhook to another n8n Webhook node (`sms-reply`). Twilio expects TwiML here too, so answer with an empty ``, then forward the reply to whoever handles bookings:

json
{
  "name": "Forward reply to the team",
  "type": "n8n-nodes-base.twilio",
  "parameters": {
    "from": "={{ $env.TWILIO_PHONE_NUMBER }}",
    "to": "={{ $env.STAFF_ALERT_PHONE }}",
    "message": "={{ 'Reply from missed call ' + $json.body.From + ': \"' + $json.body.Body + '\". Call or text them back from the business line.' }}"
  }
}

Update the lead to `replied` with the message text (again with `$1` and `$2` query parameters). For most small businesses, a fast human reply converts better than a keyword bot, because reply volume is low enough to handle personally. Once you know which questions come up every week (opening hours, prices, service area), automate those answers and keep forwarding the rest. If you want the replies to land in a shared inbox instead of a phone, send them to Slack, WhatsApp or your CRM's conversation view instead.

Twilio handles STOP, UNSUBSCRIBE and similar opt-out keywords automatically on long-code numbers, so a caller who does not want texts can stop them without any extra logic in your workflow.

Step 8: Never Let a Failed Text Disappear

Sends do fail: the caller is on a landline, the number is invalid, Twilio rate-limits a burst of calls, or a credential expires. Because the send node uses `continueErrorOutput`, failures flow to a separate branch instead of stopping the run. On that branch, write the error to `sms_log` with `status = 'failed'` and alert a person:

json
{
  "name": "Alert: text-back failed",
  "type": "n8n-nodes-base.twilio",
  "parameters": {
    "from": "={{ $env.TWILIO_PHONE_NUMBER }}",
    "to": "={{ $env.STAFF_ALERT_PHONE }}",
    "message": "={{ 'Text-back failed for missed call from ' + $('Webhook').item.json.body.From + '. Please call them back.' }}"
  }
}

Landlines are the most common failure. If many of your callers use them (older customers, other businesses), add a Twilio Lookup call with line type intelligence before the send and route landline callers straight to a callback task instead of an SMS.

Step 9: Test the Full Flow Before Going Live

Run four tests from a personal phone before pointing real traffic at the number:

  1. 1 Call and let it ring out. Confirm the execution in n8n, the spoken message, and the text within a few seconds.
  2. 2 Call and answer on your phone. Confirm nothing is sent.
  3. 3 Call twice within a few minutes. Confirm only one text arrives.
  4. 4 Reply to the text. Confirm it reaches the staff phone with the caller's number, and that the lead status changed.

Then force a failure (temporarily set an invalid `from` number) and check that the alert fires and the failure is logged. A text-back that silently fails during a busy week is worse than none at all, because the owner believes missed calls are handled.

The Complete Flow at a Glance

text
Customer calls the Twilio number
  -> TwiML <Dial timeout="20"> forwards to your phone
     -> answered: DialCallStatus = completed -> reply <Hangup/>, stop
     -> not answered: no-answer | busy | failed
        -> reply TwiML: "We are texting you now"
        -> texted this number in the last 30 min? -> yes: stop
        -> business hours? -> pick day or after-hours message
        -> send SMS from the same number
           -> success: upsert lead + log the text
           -> failure: log the error + alert a person
[Second workflow] Customer replies
  -> answer Twilio with <Response/>
  -> forward to the team + mark the lead "replied"

What to Measure Once It Is Live

Results depend heavily on your trade, your call volume and how fast someone follows up on replies, so measure your own numbers rather than trusting averages. Everything below comes straight from the two tables you created:

MetricHow to compute itWhat it tells you
Missed calls per weekLeads created with source `missed_call`How much demand you were losing before
Send success rateSent rows divided by all rows in `sms_log`Technical health: landlines, invalid numbers, expired credentials
Reply rateLeads with status `replied` divided by texts sentWhether the message copy works
Booked rateBookings that trace back to a missed-call leadThe revenue the automation recovers
Time to first human replyReply received to staff responseThe step most likely to leak leads

Review them weekly for the first month, then monthly. A falling send rate points to a technical problem; a falling reply rate points to the message.

Common Build Mistakes

Listening to the wrong status

Filtering on the inbound `CallStatus` instead of the forwarded leg's `DialCallStatus` produces a workflow that never fires. Use the `` action pattern from Step 2.

Letting your mobile voicemail answer first

If the dial timeout is longer than your voicemail delay, every missed call looks answered. Keep the timeout below it, or turn voicemail off on the forwarding target.

Not answering Twilio's webhook

Twilio expects TwiML from the action URL. If the workflow never responds, the caller hears an application error message. Always end each branch with a Respond to Webhook node.

Texting existing customers like new prospects

A client calling about tomorrow's appointment does not need "book a time". Look the number up in your client list first and send a shorter "we saw your call, we will call you back shortly" instead.

Building SQL with string templates

Pasting `{{ $json.body.From }}` into a query works until a payload contains a quote. Use `$1` query parameters, as in every example above.

Hardcoding the business name and booking link

Keep them in environment variables or a settings table. The day the booking URL changes, or a second location opens, you change one value instead of editing every node.

A Note on Compliance

A single text sent in direct response to a call the customer initiated is generally treated as a transactional reply rather than marketing, because the caller acted first. That is not a blanket exemption: rules such as the TCPA in the United States and CASL in Canada, plus carrier rules for 10DLC traffic, still apply, and follow-up marketing texts need explicit consent. Keep the text-back strictly about the call, honour opt-outs, and confirm your obligations for your jurisdiction before sending anything beyond that first reply.

Frequently Asked Questions

Can I keep my existing business number?

Yes. Either port the number to Twilio, or keep your current provider and set "forward on no answer" to a Twilio number that runs this workflow. Many VoIP systems (RingCentral, Aircall, Grasshopper and others) can also post call events to a webhook directly; the n8n logic stays the same, only the field names change.

How do I avoid texting robocalls and wrong numbers?

Skip caller IDs that are anonymous or withheld, add Twilio Lookup or a spam-score API before the send, and keep the deduplication window. You will occasionally text a wrong number; the cost of that is far lower than ignoring a real customer.

Does this work for several locations?

Yes. Give each location its own Twilio number and map the called number (`To` in the webhook) to that location's name, booking link and staff phone in a small settings table.

How long does it take to build?

For one location with a standard CRM, plan for an afternoon to a day including testing and Twilio configuration, plus the A2P 10DLC approval time in the United States, which you should start first.

Do I need n8n Cloud, or is self-hosted enough?

Self-hosted n8n handles this volume easily. What matters is uptime and a stable public HTTPS URL: if the instance is down when a call is missed, the text does not go out. A small managed VPS with automatic restarts is enough.

Can it answer with AI instead of forwarding replies?

It can, and it works best for the predictable questions (hours, prices, service area, availability). Keep a human in the loop for quotes and anything unusual, and always hand over when the customer asks for a person.

Want it built and connected to your phone line and CRM? Book a free audit and we will map the exact setup for your business.

Tags

n8n tutorialmissed call text backn8n twilio integrationautomation workflow codesmall business phone automation
P

The PURIST editorial team covers automation, AI agents, and operations strategy for businesses scaling with n8n, Make, and Claude AI.

Keep reading

More from the blog.

All articles

From audit to deployment

Experience the automation
these articles are about.

Get my free automation plan →