Skip to content
Learn Agentic AI
Learn Agentic AI13 min read2 views

AI Patient Recall Agent: Automated Reactivation of Overdue Patients

Build an AI agent that identifies overdue patients, runs multi-step communication sequences to bring them back, and tracks reactivation success rates with real Python implementation code.

The Cost of Lost Patients

A typical dental practice loses 15 to 20 percent of its active patient base each year simply because patients fall off the recall schedule. Each lost patient represents thousands of dollars in lifetime value. Manual recall efforts — calling down a list — are time-consuming and inconsistent.

An AI patient recall agent solves this by continuously scanning for overdue patients, launching personalized outreach sequences, and tracking which messages actually bring patients back.

Identifying Overdue Patients

The first step is defining what "overdue" means. Most practices set recall intervals based on the type of visit: six months for cleanings, twelve months for comprehensive exams. The agent queries the database to find patients who have exceeded their recall window.

flowchart TD
    START["AI Patient Recall Agent: Automated Reactivation o…"] --> A
    A["The Cost of Lost Patients"]
    A --> B
    B["Identifying Overdue Patients"]
    B --> C
    C["Multi-Step Communication Sequences"]
    C --> D
    D["Success Tracking and Analytics"]
    D --> E
    E["Running the Recall Agent on a Schedule"]
    E --> F
    F["FAQ"]
    F --> DONE["Key Takeaways"]
    style START fill:#4f46e5,stroke:#4338ca,color:#fff
    style DONE fill:#059669,stroke:#047857,color:#fff
from dataclasses import dataclass
from datetime import date, timedelta
from typing import Optional
from enum import Enum


class RecallInterval(Enum):
    CLEANING = 180       # 6 months
    COMPREHENSIVE = 365  # 12 months
    PERIO = 90           # 3 months for periodontal patients
    PEDIATRIC = 180      # 6 months


@dataclass
class OverduePatient:
    patient_id: str
    name: str
    phone: str
    email: str
    last_visit_date: date
    last_visit_type: str
    days_overdue: int
    recall_attempts: int
    preferred_contact: str


class OverdueDetector:
    def __init__(self, db):
        self.db = db

    async def find_overdue_patients(
        self, practice_id: str, min_days_overdue: int = 0,
    ) -> list[OverduePatient]:
        rows = await self.db.fetch("""
            WITH last_visits AS (
                SELECT
                    p.id, p.first_name || ' ' || p.last_name AS name,
                    p.phone, p.email, p.preferred_contact,
                    MAX(a.start_time::date) AS last_visit,
                    a.type AS visit_type,
                    COALESCE(r.attempt_count, 0) AS attempts
                FROM patients p
                JOIN appointments a ON a.patient_id = p.id
                LEFT JOIN recall_tracking r
                    ON r.patient_id = p.id
                    AND r.recall_cycle = DATE_PART(
                        'year', CURRENT_DATE
                    )
                WHERE a.status = 'completed'
                  AND p.practice_id = $1
                  AND p.is_active = true
                GROUP BY p.id, p.first_name, p.last_name,
                         p.phone, p.email, p.preferred_contact,
                         a.type, r.attempt_count
            )
            SELECT *, (CURRENT_DATE - last_visit) AS days_since
            FROM last_visits
            WHERE (CURRENT_DATE - last_visit) > $2
            ORDER BY days_since DESC
        """, practice_id, min_days_overdue)

        overdue = []
        for row in rows:
            interval = self._get_interval(row["visit_type"])
            days_overdue = row["days_since"] - interval
            if days_overdue > 0:
                overdue.append(OverduePatient(
                    patient_id=row["id"],
                    name=row["name"],
                    phone=row["phone"],
                    email=row["email"],
                    last_visit_date=row["last_visit"],
                    last_visit_type=row["visit_type"],
                    days_overdue=days_overdue,
                    recall_attempts=row["attempts"],
                    preferred_contact=row["preferred_contact"],
                ))
        return overdue

    def _get_interval(self, visit_type: str) -> int:
        mapping = {
            "cleaning": RecallInterval.CLEANING.value,
            "comprehensive": RecallInterval.COMPREHENSIVE.value,
            "perio_maintenance": RecallInterval.PERIO.value,
        }
        return mapping.get(visit_type, 180)

Multi-Step Communication Sequences

A single reminder rarely works. The recall agent runs a sequence of escalating outreach steps, starting gentle and increasing urgency. Each step uses the patient's preferred communication channel.

from datetime import datetime


@dataclass
class RecallStep:
    step_number: int
    channel: str          # "sms", "email", "phone"
    delay_days: int       # days after previous step
    template: str
    is_final: bool = False


DEFAULT_SEQUENCE = [
    RecallStep(1, "sms", 0, "friendly_reminder",),
    RecallStep(2, "email", 3, "value_reminder"),
    RecallStep(3, "sms", 7, "urgency_reminder"),
    RecallStep(4, "phone", 14, "personal_call", is_final=True),
]


class RecallSequencer:
    def __init__(self, db, sms_client, email_client):
        self.db = db
        self.sms = sms_client
        self.email = email_client
        self.templates = TemplateEngine()

    async def run_sequence(
        self, patient: OverduePatient,
        sequence: list[RecallStep] = None,
    ):
        sequence = sequence or DEFAULT_SEQUENCE
        current_step = await self._get_current_step(
            patient.patient_id
        )

        if current_step is None:
            next_step = sequence[0]
        else:
            next_idx = current_step + 1
            if next_idx >= len(sequence):
                await self._mark_exhausted(patient.patient_id)
                return
            next_step = sequence[next_idx]

        last_contact = await self._get_last_contact_date(
            patient.patient_id
        )
        if last_contact:
            days_since = (date.today() - last_contact).days
            if days_since < next_step.delay_days:
                return  # not time yet

        message = self.templates.render(
            next_step.template,
            patient_name=patient.name,
            days_overdue=patient.days_overdue,
            last_visit=patient.last_visit_date.isoformat(),
        )

        if next_step.channel == "sms":
            await self.sms.send(patient.phone, message)
        elif next_step.channel == "email":
            await self.email.send(patient.email, message)
        elif next_step.channel == "phone":
            await self._queue_call_task(patient, message)

        await self.db.execute("""
            INSERT INTO recall_log
                (patient_id, step_number, channel,
                 sent_at, message_preview)
            VALUES ($1, $2, $3, $4, $5)
        """, patient.patient_id, next_step.step_number,
             next_step.channel, datetime.utcnow(),
             message[:200])

Success Tracking and Analytics

The agent tracks which patients actually book after receiving recall messages. This data feeds back into optimizing the sequence timing and messaging.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

class RecallAnalytics:
    def __init__(self, db):
        self.db = db

    async def get_reactivation_rate(
        self, practice_id: str, period_days: int = 90,
    ) -> dict:
        stats = await self.db.fetchrow("""
            SELECT
                COUNT(DISTINCT rl.patient_id) AS contacted,
                COUNT(DISTINCT CASE
                    WHEN a.id IS NOT NULL
                    THEN rl.patient_id
                END) AS reactivated,
                AVG(CASE
                    WHEN a.id IS NOT NULL
                    THEN rl.step_number
                END) AS avg_steps_to_convert
            FROM recall_log rl
            JOIN patients p ON p.id = rl.patient_id
            LEFT JOIN appointments a
                ON a.patient_id = rl.patient_id
                AND a.created_at > rl.sent_at
                AND a.status IN ('scheduled', 'completed')
            WHERE p.practice_id = $1
              AND rl.sent_at > CURRENT_DATE - $2
        """, practice_id, period_days)

        contacted = stats["contacted"] or 0
        reactivated = stats["reactivated"] or 0
        return {
            "contacted": contacted,
            "reactivated": reactivated,
            "rate": round(
                reactivated / contacted * 100, 1
            ) if contacted > 0 else 0,
            "avg_steps": round(
                float(stats["avg_steps_to_convert"] or 0), 1
            ),
        }

Running the Recall Agent on a Schedule

The agent runs as a background job, processing the overdue list daily and advancing each patient through their recall sequence.

import asyncio


class RecallAgent:
    def __init__(self, db, sms_client, email_client):
        self.detector = OverdueDetector(db)
        self.sequencer = RecallSequencer(
            db, sms_client, email_client
        )
        self.analytics = RecallAnalytics(db)

    async def run_daily_recall(self, practice_id: str):
        overdue = await self.detector.find_overdue_patients(
            practice_id, min_days_overdue=7
        )

        for patient in overdue:
            try:
                await self.sequencer.run_sequence(patient)
            except Exception as e:
                print(
                    f"Recall failed for {patient.patient_id}: {e}"
                )

        stats = await self.analytics.get_reactivation_rate(
            practice_id
        )
        print(
            f"Recall stats: {stats['reactivated']}/"
            f"{stats['contacted']} reactivated "
            f"({stats['rate']}%)"
        )

FAQ

How do you prevent the recall agent from contacting patients who have already scheduled an appointment?

The overdue detector query joins against the appointments table and only surfaces patients with no future scheduled appointments. The sequencer also checks for new bookings before each outreach step, so if a patient schedules between steps, the sequence stops automatically.

What is a good reactivation rate to aim for?

Industry benchmarks show that automated recall systems achieve 15 to 25 percent reactivation rates. Practices that combine SMS and email with a personal phone call at the final step tend to hit the higher end. The analytics module lets you compare rates across different sequence configurations to continuously improve.

How do you handle patients who explicitly ask to stop receiving recall messages?

The agent must respect opt-out requests. When a patient replies "STOP" to an SMS or clicks an unsubscribe link in an email, the system sets an opted_out flag on the patient record. The overdue detector filters out opted-out patients, and the sequencer checks this flag before every send.


#PatientRecall #HealthcareAI #Reactivation #DentalPractice #Python #AgenticAI #LearnAI #AIEngineering

Share
C

Written by

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.

Related Articles You May Like

Healthcare

Reducing ER Boarding with AI Voice Triage: Nurse Line Automation That Diverts Non-Emergent Calls

How AI nurse triage agents route non-emergent callers away from the ER toward urgent care, telehealth, and self-care — measurably reducing door-to-provider time.

Use Cases

Patient Recall and Reactivation Get Ignored: Use Chat and Voice Agents to Bring Patients Back

Clinics and practices often lose revenue because recall and reactivation outreach is inconsistent. Learn how AI chat and voice agents automate the workflow.

AI Interview Prep

7 AI Coding Interview Questions From Anthropic, Meta & OpenAI (2026 Edition)

Real AI coding interview questions from Anthropic, Meta, and OpenAI in 2026. Includes implementing attention from scratch, Anthropic's progressive coding screens, Meta's AI-assisted round, and vector search — with solution approaches.

Learn Agentic AI

Building a Multi-Agent Data Pipeline: Ingestion, Transformation, and Analysis Agents

Build a three-agent data pipeline with ingestion, transformation, and analysis agents that process data from APIs, CSVs, and databases using Python.

Learn Agentic AI

AI Agents for Healthcare: Appointment Scheduling, Insurance Verification, and Patient Triage

How healthcare AI agents handle real workflows: appointment booking with provider matching, insurance eligibility checks, symptom triage, HIPAA compliance, and EHR integration patterns.

Learn Agentic AI

Building a Research Agent with Web Search and Report Generation: Complete Tutorial

Build a research agent that searches the web, extracts and synthesizes data, and generates formatted reports using OpenAI Agents SDK and web search tools.