Arsalan Faysal – Automation & RevOps Blog

Automating Patient Follow-Up for Dental Practices: A Case Study

Written by Arsalan Faysal | May 24, 2026 4:37:55 AM
6 Follow-Up Workflows
0 Code Changes to Add Workflow
100% Opt-Out Permanence
2-Way SMS with Inbound Webhook

The Problem Most Dental Practices Refuse to Quantify

A consultant led a consultation. The patient sat across the desk, received a full treatment plan, heard the numbers — and left without booking. Maybe they needed to think. Maybe the cost surprised them. Maybe life just got in the way.

Three weeks later, that patient is still on a printed spreadsheet somewhere. Nobody has followed up. The practice management software exported a CSV on Monday. It is sitting in someone's inbox. There is no system, no trigger, no automation. The revenue from that consultation leaked the moment the patient walked out the door.

This is not a one-off problem. For the boutique Cambridgeshire dental practice that came to us, it was the defining operational gap. They had strong clinical outcomes, high consultation volume, and a serious percentage of open treatment plans that were simply never activated. Patients who had already raised their hand — sat through a consultation, received a plan — were falling into silence because no follow-up infrastructure existed.

The brief was specific: build a complete patient follow-up automation system from zero. No existing CRM to inherit. No legacy database to migrate. Full architectural freedom. A detailed nine-page functional specification waiting to be executed.

"This is not a beginner role. We need someone who has shipped complex, revenue-generating funnels. You must be comfortable with conditional branching, webhooks, and API integrations."

The hard part was not writing code. The hard part was engineering a system that a non-technical practice team could operate forever — adding new workflows without touching configuration, editing message templates through a UI, and waking up every morning to a dashboard that showed them exactly what needed attention.

Architecture Overview: How We Thought About This Before Writing a Single Line

Most automation projects fail at the design phase — not because the tools are wrong, but because the state model is wrong. Before selecting any platform, we mapped the system as a state machine. A patient is always in one of a defined set of states, and every event transitions them from one to the next. That constraint keeps the system predictable, auditable, and impossible to put into a loop.

PATIENT STATE MACHINE
══════════════════════════════════════════════════════════════

  [NEW ENTRY]
      │
      ├─── Manual Form ──────────────────────────────────────┐
      │                                                      │
      └─── CSV Import (Weekly Bulk) ──────────────────────► │
                                                         [DEDUP ENGINE]
                                                             │
                           ┌───────────────┬─────────────────┴──┬────────────────┐
                           │               │                     │                 │
                     [OPT-OUT?]     [DUPLICATE?]          [NEW PATIENT]   [WORKFLOW ACTIVE?]
                     → Block         → Skip                → Enrol         → Skip
                     Permanently                           → Workflow
                           │
                    ┌──────┴──────────────────────────────────────────────────────┐
                    │               ACTIVE WORKFLOW ENGINE                        │
                    │   WF_1 (30–45d)  WF_2 (30–45d)  WF_3  ...  WF_N           │
                    │       [TIMED DISPATCH: SMS + EMAIL SEQUENCES]               │
                    └──────────────────┬──────────────────────────────────────────┘
                                       │
                        ┌──────────────┴────────────────┐
                        │                               │
                  [PATIENT REPLIES?]           [SEQUENCE COMPLETES]
                  → Halt sequence              → Mark complete
                  → Flag for human             → Audit log updated
                  → Log reply                  → Dashboard updated
                        │
                  [STOP REPLY?]
                  → Permanent opt-out
                  → Block ALL workflows
                  → No override, ever

With the state machine clear, the tool selection became obvious rather than opinionated.

System Layer Tool Selected Rationale
Orchestration n8n (self-hosted) Full control over async execution, custom JavaScript nodes, and webhook receivers with zero per-execution cost at this volume.
Database / CRM Airtable Non-technical staff can view and edit records without touching configuration. Solid API for programmatic reads and writes.
Two-Way SMS (UK) Twilio Messaging UK long code numbers, inbound webhook support, delivery status callbacks, and carrier-level STOP keyword handling built in.
Email Delivery Resend Developer-first API, high deliverability, clean webhook events for open and bounce tracking.
AI Personalisation Claude API (Anthropic) Deterministic, low-hallucination output. Excellent at rewriting short-form clinical communications in a warm but professional register without introducing clinical claims.
Dashboard Airtable Interface Designer Zero additional infrastructure. The same Airtable base surfaces a filtered, role-scoped interface for the daily morning review. Works on mobile.

Entry Layer: Two Paths Into the System

The system accepts patients through two distinct entry mechanisms. One handles real-time edge cases. The other handles operational scale.

Manual Entry Form

The practice team occasionally needs to add a single patient immediately — a walk-in who almost committed, a referral who needs follow-up before the weekly export runs. For this, we built a lightweight form backed directly by an n8n webhook.

The form collects the minimum data required to enrol a patient: full name, normalised phone number (E.164 formatted), email, treatment type, consultation date, and the workflow to enter. On submit, the webhook fires synchronously. The dedup check, opt-out verification, and enrolment happen in under two seconds. The form lives as an Airtable form view. The practice coordinator bookmarks it on their phone.

CSV Bulk Import with AI Column Mapping

The practice management software exports patient data as a CSV every Monday. The problem: these exports are inconsistent. Column names change between software versions. Staff sometimes rearrange columns. The order is never guaranteed.

The traditional solution is a rigid column-mapping configuration that breaks the moment the export format shifts. We rejected that entirely. Instead, we built an AI-assisted column mapper that runs on every upload:

  1. The CSV is uploaded to a secure n8n webhook endpoint.
  2. n8n reads the header row and the first five data rows of the file.
  3. This sample is passed to the Claude API with a structured prompt that maps headers to our canonical field schema.
  4. Claude returns a JSON mapping object — e.g. {"Patient Name": "full_name", "Mobile": "phone", "Email Address": "email"}
  5. n8n applies the mapping to process every row in the CSV using the dynamically resolved column names.
// n8n Function Node — Claude Column Mapping Prompt

const headerRow = $input.first().json.headers;
// e.g. ["Patient Name","Mobile","Email Address","Consult Date","Tx Type"]

const prompt = `
You are a data normalisation assistant for a dental practice CRM.

Map each of these CSV headers to one canonical field name (or null if no match):
Canonical fields: full_name | phone | email | consultation_date | treatment_type | notes

Headers: ${JSON.stringify(headerRow)}

Return ONLY a valid JSON object. No explanation. No markdown fences.
Example: {"Patient Name": "full_name", "Mobile No": "phone", "Email": "email"}
`.trim();

const response = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "x-api-key": process.env.ANTHROPIC_API_KEY,
    "anthropic-version": "2023-06-01",
    "content-type": "application/json"
  },
  body: JSON.stringify({
    model: "claude-sonnet-4-20250514",
    max_tokens: 400,
    messages: [{ role: "user", content: prompt }]
  })
});

const data = await response.json();
const mapping = JSON.parse(data.content[0].text.trim());

return [{ json: { mapping, headers: headerRow } }];

The result is a system that handles column reordering, renamed headers, and even column additions without any manual reconfiguration. Staff upload the CSV. The system figures out the rest.

Deduplication and the Opt-Out Enforcement Layer

This is the most critical compliance component of the entire system. Get it wrong and you are exposing the practice to ICO enforcement, patient complaints, and potential fines under UK GDPR. We built it to be mechanically impossible to bypass.

Phone Number Normalisation

Before any deduplication check, every phone number entering the system is normalised to E.164 format: international dialling code, no spaces, no dashes, no parentheses. 07700 900123 becomes +447700900123. This is the dedup key. Not name. Not email. The phone number, normalised.

Why phone number as the primary key? Because the same patient may be entered as "Sarah J. Mitchell" and "S Mitchell" in different exports. The email may differ between personal and work accounts. The normalised phone number is the single source of identity truth for SMS-based follow-up.

// Phone normalisation — runs on every patient entry (n8n Function Node)

function normalisePhone(raw) {
  let cleaned = raw.replace(/\D/g, ''); // Strip all non-digit characters

  if (cleaned.startsWith('07') && cleaned.length === 11) {
    return '+44' + cleaned.substring(1); // UK mobile: 07xxx → +447xxx
  }
  if (cleaned.startsWith('44') && cleaned.length === 12) {
    return '+' + cleaned;               // Already has country code, add +
  }
  if (cleaned.startsWith('447') && cleaned.length === 12) {
    return '+' + cleaned;               // E.164 without the +
  }
  return '+' + cleaned;                 // Fallback
}

const phone = normalisePhone($input.first().json.raw_phone);
return [{ json: { normalised_phone: phone } }];

The Three-Gate Deduplication Check

Every patient entering the system passes through three sequential gates before enrolment:

Gate Logic — Sequential, All-Or-Nothing
Gate 1
Opt-Out Check Query Airtable OptOuts table by normalised phone. If found: permanent block. Skip remaining gates. Log skipped reason. End.
Gate 2
Active Workflow Check Query Enrolments table: does this phone + workflow_id combination already have a status of active? If yes: skip. The same patient cannot run the same sequence twice simultaneously.
Gate 3
Enrol Patient is new to this workflow. Create record in Patients and Enrolments. Set next_send_at to the first step timestamp. Write to audit log.

The Opt-Out Is Permanent. That Is Not Negotiable.

When a patient replies STOP — to any message, in any workflow, at any time — two things happen simultaneously:

  1. Their normalised phone number is written to the OptOuts table with a UTC timestamp and source workflow reference.
  2. All active enrolment records for that phone number have their status set to opted_out. No future sequences will trigger.

There is no override. There is no admin toggle. There is no "bypass for re-consent". The system does not allow it architecturally — the opt-out check is the first gate, and a positive result terminates the flow completely before any other logic runs.

GDPR Article 21 gives individuals the right to object to processing for direct marketing purposes, and that objection must be respected permanently and immediately. This is not a compliance checkbox. It is enforced at the data layer. Every outbound SMS includes STOP instructions. Every email includes an unsubscribe link. Both channels share a single opt-out state.

The Six Follow-Up Workflows: Architecture and Sequencing Logic

The practice serves patients across multiple treatment categories. Patients who did not commit to cosmetic treatment need a different conversation than patients who have an open orthodontic plan. Each workflow is its own self-contained sequence — same dispatch engine, different templates, different cadence, different logic.

The key architectural decision: workflows are stored as configuration in Airtable, not hardcoded logic in n8n. The orchestrator reads the workflow definition at runtime. This is what allows new workflows to be added without any code change.

WORKFLOW CONFIG SCHEMA (Airtable)
══════════════════════════════════════════════════════════════

TABLE: Workflows
  workflow_id     → "WF_COSMETIC_01"
  name            → "Cosmetic Consultation No-Commit"
  duration_days   → 42
  active          → TRUE

TABLE: WorkflowSteps  (one workflow → many steps)
  step_id         → "WF_COSMETIC_01_STEP_1"
  workflow_id     → "WF_COSMETIC_01"
  step_number     → 1
  send_after_days → 3        ← days from enrolment date
  channel         → "sms"    ← or "email"
  template_id     → "TPL_COSMETIC_SMS_01"

  step_id         → "WF_COSMETIC_01_STEP_2"
  workflow_id     → "WF_COSMETIC_01"
  step_number     → 2
  send_after_days → 10
  channel         → "email"
  template_id     → "TPL_COSMETIC_EMAIL_01"

  [... continues for all steps in workflow ...]

TO ADD A NEW WORKFLOW:
  → Create one row in Workflows table
  → Create step rows in WorkflowSteps table
  → Set active = TRUE
  → Dispatch engine picks it up on next scheduled run
  → Zero code changes required

A representative workflow cadence — timings and channels vary by treatment type and clinical urgency:

Day Channel Message Type Objective
Day 3 SMS Warm re-introduction Re-establish context from consultation. Low friction. No hard ask.
Day 7 Email Treatment overview + FAQ Address common objections — cost, time, pain — in the patient's own terms.
Day 14 SMS Social proof signal Patient story reference. Creates trust without clinical claim.
Day 21 Email Availability nudge Introduce booking incentive or show upcoming appointment slots.
Day 30 SMS Direct booking invitation Clear CTA. One click to book. This is the conversion message.
Day 40 Email Final check-in Low-pressure close. Acknowledge they may have moved forward elsewhere. Leave the door open.

The Dispatch Engine: How Messages Get Sent on Schedule

The dispatch engine is an n8n workflow that runs on a scheduled trigger — every 15 minutes during UK business hours (8am–8pm), hourly overnight. Each execution performs the same set of operations:

  1. Query Airtable for all Enrolment records where status = "active" and next_send_at <= NOW().
  2. For each due record: fetch the patient record, the next step definition, and the template from Airtable.
  3. AI personalisation: pass the template and patient variables to Claude API for a personalised rewrite.
  4. Send: dispatch via Twilio (SMS) or Resend (email) based on the step channel.
  5. Audit log: write the send event — message SID, patient ID, workflow step, timestamp, delivery status — to the AuditLog table.
  6. Advance the enrolment: update next_send_at to the next step's timestamp, or set status = "completed" if the sequence is finished.

The engine is stateless. It holds no patient state in memory. Every execution reads from and writes back to Airtable. It can be restarted, rescheduled, or migrated without data loss — and it survives server restarts without losing track of where any patient is in their sequence.

AI Personalisation: Why Generic Templates Kill Response Rates

Template messages are a trust problem. A patient who attended a consultation for Invisalign does not want to receive a message that reads like it was written for the entire mailing list. The moment a follow-up feels broadcast, it gets ignored — or worse, it generates a STOP reply.

Personalisation at scale used to require either a large team or a very rigid mail merge system. We replaced both with Claude API.

The Personalisation Prompt Architecture

Each template stored in Airtable contains two things: a base message with placeholder variables, and a personalisation instruction. The Claude API call receives both:

// Personalisation API call (n8n HTTP Request Node)

const template = {
  base_message:
    "Hi , it was great to meet you on  " +
    "to discuss . We wanted to follow up and see if " +
    "you had any questions about your treatment plan. We're here to help.",

  personalisation_instructions:
    "Rewrite this SMS message to feel warm, personal, and professional — " +
    "like a message from a caring dental professional, not a marketing " +
    "department. Keep it under 160 characters. Do not mention pricing. " +
    "Do not use exclamation marks. Maintain the key information but make " +
    "it feel individually written. Return only the final message text."
};

const patient = {
  first_name: "Sarah",
  consult_date: "14th May",
  treatment_type: "Invisalign clear aligners"
};

// Fill template variables
let filled = template.base_message
  .replace("", patient.first_name)
  .replace("", patient.consult_date)
  .replace("", patient.treatment_type);

const prompt = `${template.personalisation_instructions}\n\nOriginal:\n${filled}`;

// Claude returns:
// "Hi Sarah, it was lovely meeting you on the 14th to chat through your
//  Invisalign options. Do you have any questions about the next steps?
//  We're here whenever you're ready. To stop messages, reply STOP."
Design Constraint: AI Is Guardrailed, Not Autonomous

Claude rewrites the template — it does not generate free-form clinical content. The base template is written by the practice team. The AI improves naturalness and tone. It cannot introduce pricing information, clinical claims, or content outside the original template's scope. The practice stays in full control of clinical accuracy. The AI eliminates robotic copy.

Inbound Reply Handler: Two-Way SMS Is the Whole Point

A one-way SMS follow-up system is a broadcast tool. It is not a patient communication system. The moment a practice sends messages they cannot receive replies to, they have created a compliance risk and a patient experience failure simultaneously.

Every outbound SMS is sent from a Twilio UK long code number registered to the practice. When a patient replies, Twilio fires a webhook to our n8n reply handler endpoint within milliseconds.

INBOUND SMS RECEIVED
         │
         ▼
  [Parse Twilio Webhook Payload]
  Extract: From (normalised), Body, MessageSid, Timestamp
         │
         ▼
  [Body === "STOP"?] ─── YES ──► Write to OptOuts table
         │                        Set all active enrolments → opted_out
         │ NO                     Log to AuditLog
         │                        Send GDPR confirmation SMS
         ▼
  [Lookup Patient by normalised phone]
         │
         ├── Not found ──► Log unknown sender. No further action.
         │
         ▼
  [Set enrolment status → "human_required"]
  [Write reply content to PatientReplies table]
  [Set flagged_for_review = TRUE on patient record]
  [Write to AuditLog: event_type = "inbound_reply"]
         │
         ▼
  [Dashboard auto-surfaces reply in morning queue]

The reply handler does not attempt to auto-respond to non-STOP messages. That would require clinical judgement the system is not equipped to make. Instead, it surfaces the reply in the dashboard for a human to action — and it stops all automated messages immediately, so the patient is not receiving another scheduled SMS while someone is composing a reply.

The Full Audit Trail: Every Message, Every Status, Forever

Healthcare communication carries regulatory weight. The ICO expects organisations to be able to demonstrate exactly what was sent to whom and when. The audit log makes compliance reporting a ten-second operation.

Every event in the system writes a record to the AuditLog Airtable table:

Field Type Description
event_type Single select sms_sent · email_sent · inbound_reply · opt_out · enrolment · dedup_skip · error
patient_id Linked record Reference to Patients table
workflow_id Text Workflow identifier at time of event
step_number Number Step within the workflow sequence
channel Single select sms · email · system
message_sid Text Twilio MessageSID or Resend email ID
delivery_status Single select queued · sent · delivered · failed · undelivered
event_timestamp DateTime (UTC) Precise timestamp of event
notes Long text Error messages, skip reasons, anonymised reply content

Twilio delivery status webhooks update the delivery_status field asynchronously — so the audit log does not just record that a message was sent, it records whether it actually reached the patient's handset.

The Daily Operations Dashboard: Sixty Seconds Each Morning

The practice coordinator opens the dashboard at 8:30am. In sixty seconds, they see everything that needs attention. Not a spreadsheet. Not a report to generate. A live, filtered view.

The dashboard is built using Airtable Interface Designer — no additional infrastructure, no separate web app, nothing to maintain. It uses the same Airtable base as the automation engine, surfacing filtered views with role-appropriate permissions. Works on mobile.

Dashboard Panels — What the Coordinator Sees Each Morning
Panel 1
Replies Awaiting Response All patients who replied in the last 24 hours. Flagged for human follow-up. Shows patient name, reply content, which workflow they are in, and treatment type.
Panel 2
Today's Scheduled Sends All messages going out today. Visible for awareness. No action required, but staff can see what communication is happening.
Panel 3
Failed Deliveries Messages that failed delivery in the last 48 hours. Often an invalid phone number. Staff can update the number and manually re-queue.
Panel 4
Sequences Completed This Week Patients who finished a full workflow without booking. These are candidates for a personal phone call — the system has done its automated job, now a human takes over.
Panel 5
Opt-Outs (Rolling 7 Days) Recent STOP requests. Confirms compliance is working. A spike here is a signal to review message frequency or content.

Database Architecture: The Airtable Schema That Makes It All Work

Airtable is the patient CRM, the workflow configuration store, the opt-out registry, the audit log, and the dashboard data source simultaneously. For this to work without becoming a maintenance nightmare, the schema must be clean and normalised from day one.

AIRTABLE BASE — FULL RELATIONAL SCHEMA
══════════════════════════════════════════════════════════════

TABLE: Patients
  patient_id (Autonumber, Primary)  full_name (Text)
  normalised_phone (Text, Unique)   email (Email)
  created_at (DateTime)              source (manual_form | csv_import)
  [Linked: Enrolments, AuditLog, PatientReplies]

TABLE: Enrolments
  enrolment_id (Autonumber, Primary)
  patient_id → Patients
  workflow_id (Text, references Workflows table)
  enrolled_at (DateTime)             current_step (Number)
  next_send_at (DateTime)
  status (active | completed | opted_out | human_required | paused)

TABLE: Workflows
  workflow_id (Text, Primary)       name (Text)
  duration_days (Number)            active (Checkbox)
  [Linked: WorkflowSteps]

TABLE: WorkflowSteps
  step_id (Autonumber)              workflow_id → Workflows
  step_number (Number)              send_after_days (Number)
  channel (sms | email)              template_id → MessageTemplates

TABLE: MessageTemplates
  template_id (Autonumber)          name (Text)
  base_message (Long Text)          personalisation_instructions (Long Text)
  channel (sms | email)              subject_line (Text, email only)

TABLE: OptOuts
  optout_id (Autonumber)            normalised_phone (Text, Unique)
  opted_out_at (DateTime)            source_workflow (Text)
  source_message_sid (Text)

TABLE: AuditLog
  event_id (Autonumber)              event_type (Single Select)
  patient_id → Patients              workflow_id (Text)
  step_number (Number)               channel (sms | email | system)
  message_sid (Text)                 delivery_status (Single Select)
  event_timestamp (DateTime UTC)    notes (Long Text)

TABLE: PatientReplies
  reply_id (Autonumber)              patient_id → Patients
  reply_body (Long Text)             received_at (DateTime)
  twilio_message_sid (Text)          reviewed (Checkbox)

Handover: Building for People Who Are Not You

This is the part most automation developers get wrong. The system is technically complete. The workflows run. The messages send. The opt-outs work. And then the developer hands it over — and the practice team cannot figure out how to add a new treatment type to an existing workflow.

We built the handover as a deliberate deliverable, not an afterthought. Three components:

Operations Manual — Four Guides in Plain English

A short documentation set covering the four things a non-technical user will ever need to do: how to add a new workflow, how to edit a message template, how to run a CSV import, and how to use the daily dashboard. No technical language. Annotated screenshots. Loom video walkthroughs for each process.

Handover Call with the Practice Team

A live session walking the practice coordinator and practice manager through every dashboard panel, running a live test import with a dummy patient, demonstrating a reply triggering the human review flag, and answering operational questions. The session was recorded and stored in their shared drive.

Incident Response Protocol

What to do if the dispatch engine stops running. What to do if Twilio returns a delivery failure spike. What to do if the Claude API call fails — the fallback is to send the base template without AI personalisation. The team needed to know what fallback behaviour looks like so they are not alarmed when it activates.

Technical Takeaways: What Builders Can Replicate

If you are building a similar system for a healthcare or professional services practice, these are the architectural decisions we would make again without hesitation:

  • Phone number as the dedup key, normalised to E.164, always. Name and email are too inconsistent across exports and manual entry. The phone number is the patient identity anchor.
  • Store workflow configuration in the database, not in the orchestration tool. n8n workflows should be logic, not content. Templates, cadences, and step definitions belong in a table a non-developer can edit.
  • Two-way SMS is not optional in healthcare. Any follow-up system that cannot receive replies is non-compliant with reasonable patient communication standards and creates an unreachable dead-end for patients who need to respond.
  • AI personalisation with guardrails outperforms templates without AI. The base template defines what is said — the LLM improves how it is said. Keep clinical content in the template. Let the AI handle tone.
  • Opt-out must be architectural, not operational. If opt-out can be bypassed by an admin setting, it will eventually be bypassed. Make the block the first gate in the enrolment flow, not a checkbox in a settings page.
  • Build the dashboard for the person who will use it daily, not the developer who built it. If the coordinator needs technical training to review the morning queue, the dashboard has failed.
Most automation fails in healthcare not because the technology is wrong — but because the system treats compliance as an afterthought and treats the non-technical operator as an edge case. We reversed both assumptions.

The Bottom Line

Private dental practices sit on significant unrealised revenue in their open treatment plans. The consultations have already happened. The trust has already been established. The patient has already shown intent. The only thing standing between the practice and a committed booking is a consistent, personalised, and compliant follow-up system.

Building one from scratch is a significant engineering project. But the alternative — continuing to let that revenue leak through manual processes and inconsistent follow-up — costs far more in the long run.

The system we built for this Cambridgeshire practice is fully automated, fully GDPR-compliant, and fully operable by a non-technical team. It will continue running, personalising, and following up without developer intervention — until a patient books, opts out, or the practice adds the next workflow.

That is what automation is supposed to do.

•   •   •
In This Article The Problem Architecture Overview Entry Layer Dedup & Opt-Out Six Workflows Dispatch Engine AI Personalisation Reply Handler Audit Trail Daily Dashboard Database Schema Handover Takeaways
Tech Stack
n8n Orchestration
Airtable Database / CRM
Twilio Two-Way SMS
Resend Email Delivery
Claude API AI Personalisation