AI Automation How-To

How to Build an AI-Powered SMS Appointment Reminder Pipeline

The Problem with Static SMS Reminders

Standard SMS appointment reminders rely on rigid templates: "Your appointment is tomorrow at 2 PM. Reply 1 to confirm, 2 to cancel."

This format breaks the moment a customer replies with natural language like "I can make it, but I'll be 10 minutes late" or "Can we move this to Friday?" Static scripts cannot process these edge cases. They fail to update your calendar, require manual staff intervention to resolve, and lead to lost bookings.

Integrating a Large Language Model (LLM) into your SMS pipeline solves this. The system parses unstructured customer responses, determines intent, updates your scheduling database directly, and sends an appropriate response—all without human intervention.

Here is how to architect, code, and deploy an automated AI SMS reminder system.


Architecture Overview

The system consists of four primary components:

  1. Database & Scheduler: Stores customer details, appointment times, and booking statuses (e.g., PostgreSQL). A cron job triggers the reminder engine.
  2. SMS Gateway: Handles incoming and outgoing network messages (e.g., Twilio).
  3. Execution Engine / Webhook Handler: An API endpoint that receives inbound SMS payloads, sends the message to the AI model with structured context, and executes function calls based on the output.
  4. LLM Engine: Processes natural text, converts it into structured JSON (intent, target time, confirmation status), and generates the final reply (e.g., OpenAI gpt-4o-mini).
[ Cron Scheduler ] ──> Sends Scheduled SMS via API ──> [ Customer Phone ]
                                                              │
                                                        Replies via SMS
                                                              │
[ Database ] <── Webhook Handler <── [ SMS Gateway ] <────────┘
      │               │
      └── Update DB   └── Pass context ──> [ LLM Engine ]

Step 1: Triggering the Scheduled Reminder

To avoid spamming customers, run a periodic task (via cron or a worker like Celery) that queries your database for appointments occurring within a specific window (e.g., exactly 24 hours out) where reminder_sent is false.

Python Implementation using Twilio SDK

import os
from datetime import datetime, timedelta
from twilio.rest import Client
import psycopg2

# Initialize Twilio Client
account_sid = os.environ['TWILIO_ACCOUNT_SID']
auth_token = os.environ['TWILIO_AUTH_TOKEN']
client = Client(account_sid, auth_token)

def send_daily_reminders():
    # Connect to PostgreSQL
    conn = psycopg2.connect(os.environ['DATABASE_URL'])
    cur = conn.cursor()
    
    # Select appointments 24 hours away
    now = datetime.now()
    window_start = now + timedelta(hours=23, minutes=55)
    window_end = now + timedelta(hours=24, minutes=5)
    
    query = """
        SELECT id, customer_name, phone_number, appointment_time 
        FROM appointments 
        WHERE appointment_time BETWEEN %s AND %s 
        AND reminder_sent = FALSE;
    """
    cur.execute(query, (window_start, window_end))
    appointments = cur.fetchall()
    
    for appt in appointments:
        appt_id, name, phone, appt_time = appt
        formatted_time = appt_time.strftime("%I:%M %p")
        
        message_body = (
            f"Hi {name}, this is a reminder for your appointment tomorrow at {formatted_time}. "
            f"Reply to confirm, reschedule, or cancel."
        )
        
        # Dispatch SMS
        message = client.messages.create(
            body=message_body,
            from_=os.environ['TWILIO_PHONE_NUMBER'],
            to=phone
        )
        
        # Mark as sent in DB
        cur.execute(
            "UPDATE appointments SET reminder_sent = TRUE, twilio_sid = %s WHERE id = %s",
            (message.sid, appt_id)
        )
        conn.commit()

    cur.close()
    conn.close()

Step 2: Processing Inbound Customer Replies with AI

When a customer replies, the SMS provider hits your webhook endpoint. The engine must accept the inbound text, fetch the customer's active appointment record, and pass both to the LLM using Structured Outputs (JSON Schema enforcement).

FastAPI Webhook Engine with OpenAI Function Calling

from fastapi import FastAPI, Form, Response
from openai import OpenAI
import os
import json
import psycopg2

app = FastAPI()
ai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

SYSTEM_PROMPT = """
You are an automated scheduling assistant for a clinic. Analyze the customer's SMS reply.
Classify their intent into one of: CONFIRM, CANCEL, RESCHEDULE, LATENESS, UNKNOWN.

Output JSON strictly adhering to this schema:
{
  "intent": "CONFIRM" | "CANCEL" | "RESCHEDULE" | "LATENESS" | "UNKNOWN",
  "minutes_late": int or null,
  "requested_reschedule_text": string or null,
  "sms_response": "Short reply back to the user under 160 characters."
}
"""

@app.post("/sms/webhook")
async def handle_inbound_sms(From: str = Form(...), Body: str = Form(...)):
    conn = psycopg2.connect(os.environ['DATABASE_URL'])
    cur = conn.cursor()
    
    # Fetch latest active appointment for this phone number
    cur.execute(
        "SELECT id, appointment_time FROM appointments WHERE phone_number = %s ORDER BY appointment_time DESC LIMIT 1",
        (From,)
    )
    row = cur.fetchone()
    
    if not row:
        return Response(content="<Response></Response>", media_type="text/xml")
        
    appt_id, appt_time = row

    # Request structured classification from LLM
    response = ai_client.chat.completions.create(
        model="gpt-4o-mini",
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": f"Appointment Time: {appt_time}. Customer Reply: '{Body}'"}
        ],
        temperature=0.0
    )
    
    data = json.loads(response.choices[0].message.content)
    intent = data.get("intent")
    sms_response = data.get("sms_response")
    
    # State updates based on extracted intent
    if intent == "CONFIRM":
        cur.execute("UPDATE appointments SET status = 'CONFIRMED' WHERE id = %s", (appt_id,))
    elif intent == "CANCEL":
        cur.execute("UPDATE appointments SET status = 'CANCELLED' WHERE id = %s", (appt_id,))
    elif intent == "LATENESS":
        minutes = data.get("minutes_late", 0)
        cur.execute("UPDATE appointments SET notes = %s WHERE id = %s", (f"Running {minutes} mins late", appt_id))
    elif intent == "RESCHEDULE":
        cur.execute("UPDATE appointments SET status = 'PENDING_RESCHEDULE', notes = %s WHERE id = %s", 
                    (data.get("requested_reschedule_text"), appt_id))
        
    conn.commit()
    cur.close()
    conn.close()

    # Format TwiML XML response back to Twilio
    twiml_xml = f"<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><Message>{sms_response}</Message></Response>"
    return Response(content=twiml_xml, media_type="text/xml")

Tooling & Cost Comparisons

Building custom backend infrastructure offers maximum control, but low-code orchestration engines cut implementation time significantly.

CategoryPaid / Industry StandardFree / Open-Source AlternativeNotes
SMS GatewayTwilio ($0.0079 / SMS sent or received)SignalWire ($0.00375 / SMS)SignalWire is significantly cheaper for high volume; Twilio has superior developer docs.
Workflow EngineMake.com ($9+/month)n8n (Free self-hosted via Docker)Self-hosting n8n gives you complete data privacy and unlimited workflow executions for free.
LLM ProviderOpenAI gpt-4o-mini ($0.15 / 1M input tokens)Ollama + Llama 3.1 8B (Free self-hosted)Local models remove per-message token costs completely, requiring only a basic GPU server.

Key Best Practices

  1. Enforce Temperature = 0.0: Deterministic outputs are required when altering database states based on intent. Do not allow creative text generation for status updating logic.
  2. Handle Opt-outs Explicitly: Ensure your webhooks do not intercept regulatory keywords (STOP, UNSUBSCRIBE). Let your gateway provider (Twilio/SignalWire) handle compliance automatically at the carrier level.
  3. Set Character Limits: SMS segments break at 160 characters. Instruct your LLM prompt explicitly to limit sms_response to under 150 characters to prevent double-billing on outbound segments.

On pricing: plans and rates change, and this post was written on a specific date. Check the vendor's current pricing page before you commit to anything.

FAQ

How much does an AI SMS reminder system cost per month?
It scales with volume: you pay your SMS gateway per message sent and received, plus a small per-token charge for the LLM. Multiply your monthly message count by the current rates on your gateway's and model provider's pricing pages. SignalWire lists a lower per-message price than Twilio, so compare both at your volume.
How do I prevent the AI from generating incorrect appointment times?
Do not rely on the LLM to update calendar databases directly with plain text. Use structured outputs (JSON schema) to extract raw parameters, then process the database updates inside your backend code using strict date-parsing libraries.
What is the best free framework to automate SMS AI reminders without coding?
You can self-host n8n using Docker for free. n8n includes native nodes for Twilio, PostgreSQL, and OpenAI, allowing you to build the entire SMS trigger and webhook processing pipeline visually without subscription fees.
How do I handle TCPA compliance for automated SMS reminders?
Obtain explicit opt-in consent during booking before sending SMS messages. Ensure your provider handles mandatory keywords (`STOP`, `HELP`, `START`) automatically, and store opt-out statuses in your database to prevent sending further messages to unsubscribed numbers.

More in AI Automation How-To

Questions & answers

Ask anything about this post — you'll get a reply here.

  • No questions yet — be the first.

One concrete AI-automation tip a week

No hype. Unsubscribe anytime.