Skip to content
Learn Agentic AI
Learn Agentic AI14 min read15 views

Building a Post-Operative Care Agent: Recovery Instructions and Follow-Up Scheduling

Build an AI agent that delivers personalized post-operative care instructions, monitors patient recovery through symptom check-ins, triggers clinical alerts when needed, and schedules follow-up appointments automatically.

The Gap Between Discharge and Recovery

After a dental procedure, patients leave with a printed instruction sheet they often lose before reaching their car. Questions arise at night and on weekends when the office is closed. A post-operative care agent fills this gap by delivering instructions at the right time, checking in on recovery milestones, and escalating to the clinical team when symptoms suggest complications.

Post-Op Instruction Engine

The instruction engine maps each procedure type to a timeline of care instructions. Instead of dumping all information at once, it delivers relevant guidance at each stage of recovery.

flowchart LR
    CALLER(["Patient or Caregiver"])
    subgraph TEL["Telephony"]
        SIP["Twilio SIP and PSTN"]
    end
    subgraph BRAIN["Healthcare AI Agent"]
        STT["Streaming STT<br/>Deepgram or Whisper"]
        NLU{"Intent and<br/>Entity Extraction"}
        TOOLS["Tool Calls"]
        TTS["Streaming TTS<br/>ElevenLabs or Rime"]
    end
    subgraph DATA["Live Data Plane"]
        CRM[("CRM and Notes")]
        CAL[("Calendar and<br/>Schedule")]
        KB[("Knowledge Base<br/>and Policies")]
    end
    subgraph OUT["Outcomes"]
        O1(["Appointment booked"])
        O2(["Prescription refill request"])
        O3(["Triage to clinician"])
    end
    CALLER --> SIP --> STT --> NLU
    NLU -->|Lookup| TOOLS
    TOOLS <--> CRM
    TOOLS <--> CAL
    TOOLS <--> KB
    NLU --> TTS --> SIP --> CALLER
    NLU -->|Resolved| O1
    NLU -->|Schedule| O2
    NLU -->|Escalate| O3
    style CALLER fill:#f1f5f9,stroke:#64748b,color:#0f172a
    style NLU fill:#4f46e5,stroke:#4338ca,color:#fff
    style O1 fill:#059669,stroke:#047857,color:#fff
    style O2 fill:#0ea5e9,stroke:#0369a1,color:#fff
    style O3 fill:#f59e0b,stroke:#d97706,color:#1f2937
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional
from enum import Enum

class RecoveryPhase(Enum):
    IMMEDIATE = "immediate"       # 0-2 hours
    FIRST_DAY = "first_day"       # 2-24 hours
    EARLY_RECOVERY = "early"      # 1-3 days
    MID_RECOVERY = "mid"          # 3-7 days
    LATE_RECOVERY = "late"        # 7-14 days

@dataclass
class CareInstruction:
    phase: RecoveryPhase
    title: str
    instructions: list[str]
    warnings: list[str] = field(default_factory=list)
    send_at_offset_hours: int = 0

EXTRACTION_INSTRUCTIONS = [
    CareInstruction(
        phase=RecoveryPhase.IMMEDIATE,
        title="Right After Your Extraction",
        instructions=[
            "Keep biting on the gauze for 30-45 minutes.",
            "Do not spit, use a straw, or rinse your mouth.",
            "Apply an ice pack to your cheek: 20 minutes "
            "on, 20 minutes off.",
            "Take prescribed pain medication before the "
            "numbness wears off.",
        ],
        warnings=[
            "Some bleeding is normal. If bleeding does not "
            "slow after 2 hours of steady gauze pressure, "
            "call the office.",
        ],
        send_at_offset_hours=0,
    ),
    CareInstruction(
        phase=RecoveryPhase.FIRST_DAY,
        title="First 24 Hours",
        instructions=[
            "Eat soft foods: yogurt, mashed potatoes, soup.",
            "Do not smoke or use tobacco products.",
            "Sleep with your head elevated on an extra pillow.",
            "Take ibuprofen 400mg every 6 hours for pain.",
        ],
        warnings=[
            "Call us if you develop a fever above 101F.",
        ],
        send_at_offset_hours=4,
    ),
    CareInstruction(
        phase=RecoveryPhase.EARLY_RECOVERY,
        title="Days 2-3 Recovery Check",
        instructions=[
            "Gently rinse with warm salt water after meals.",
            "You can begin eating slightly firmer foods.",
            "Continue taking medication as prescribed.",
            "Swelling should start to decrease.",
        ],
        warnings=[
            "Increasing pain after day 3 could indicate "
            "dry socket. Contact us if pain suddenly "
            "worsens.",
        ],
        send_at_offset_hours=48,
    ),
    CareInstruction(
        phase=RecoveryPhase.MID_RECOVERY,
        title="One Week Check-In",
        instructions=[
            "Resume normal brushing, being gentle near "
            "the extraction site.",
            "Most discomfort should be gone by now.",
            "Resume normal diet as comfort allows.",
        ],
        send_at_offset_hours=168,
    ),
]

PROCEDURE_INSTRUCTIONS = {
    "extraction": EXTRACTION_INSTRUCTIONS,
    "root_canal": [],   # similar structure omitted
    "implant": [],      # similar structure omitted
    "crown": [],        # similar structure omitted
}

Symptom Monitoring and Check-In System

The agent sends scheduled check-in messages that ask the patient to report their symptoms. It uses structured questions to gather quantifiable data.

Hear it before you finish reading

Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.

Try Live Demo →
@dataclass
class SymptomCheckIn:
    question: str
    response_type: str  # "scale_1_10", "yes_no", "free_text"
    alert_threshold: Optional[int] = None
    follow_up_question: Optional[str] = None

CHECK_IN_QUESTIONS = {
    RecoveryPhase.FIRST_DAY: [
        SymptomCheckIn(
            "On a scale of 1-10, how is your pain level?",
            "scale_1_10",
            alert_threshold=8,
        ),
        SymptomCheckIn(
            "Is the bleeding controlled?",
            "yes_no",
            follow_up_question=(
                "Is it steady oozing or active bleeding?"
            ),
        ),
        SymptomCheckIn(
            "Have you been able to take your medication?",
            "yes_no",
        ),
    ],
    RecoveryPhase.EARLY_RECOVERY: [
        SymptomCheckIn(
            "How is your pain compared to yesterday? "
            "(1=much better, 5=same, 10=much worse)",
            "scale_1_10",
            alert_threshold=7,
        ),
        SymptomCheckIn(
            "Do you have any swelling?",
            "yes_no",
            follow_up_question="Is it increasing or stable?",
        ),
        SymptomCheckIn(
            "Are you experiencing any unusual taste "
            "or bad odor?",
            "yes_no",
            alert_threshold=1,  # any yes triggers review
        ),
    ],
}

class SymptomMonitor:
    def __init__(self, db, sms_client, alert_service):
        self.db = db
        self.sms = sms_client
        self.alerts = alert_service

    async def send_check_in(
        self, patient_id: str, procedure_id: str,
        phase: RecoveryPhase,
    ):
        questions = CHECK_IN_QUESTIONS.get(phase, [])
        if not questions:
            return

        patient = await self.db.fetchrow(
            "SELECT * FROM patients WHERE id = $1",
            patient_id,
        )

        for q in questions:
            await self.sms.send(
                patient["phone"], q.question
            )
            await self.db.execute("""
                INSERT INTO symptom_check_ins
                    (patient_id, procedure_id, phase,
                     question, sent_at)
                VALUES ($1, $2, $3, $4, $5)
            """, patient_id, procedure_id, phase.value,
                 q.question, datetime.utcnow())

    async def process_response(
        self, patient_id: str, response_text: str,
    ):
        latest_question = await self.db.fetchrow("""
            SELECT * FROM symptom_check_ins
            WHERE patient_id = $1
              AND response IS NULL
            ORDER BY sent_at DESC LIMIT 1
        """, patient_id)

        if not latest_question:
            return

        question_def = self._find_question_def(
            latest_question["phase"],
            latest_question["question"],
        )

        parsed_value = self._parse_response(
            response_text, question_def.response_type
        )

        await self.db.execute("""
            UPDATE symptom_check_ins
            SET response = $2, responded_at = $3
            WHERE id = $1
        """, latest_question["id"], str(parsed_value),
             datetime.utcnow())

        if self._should_alert(question_def, parsed_value):
            await self.alerts.notify_clinical_team(
                patient_id=patient_id,
                alert_type="symptom_concern",
                details=(
                    f"Patient reported {parsed_value} for: "
                    f"{question_def.question}"
                ),
            )

    def _parse_response(self, text, response_type):
        if response_type == "scale_1_10":
            import re
            numbers = re.findall(r"\d+", text)
            return int(numbers[0]) if numbers else 5
        elif response_type == "yes_no":
            lower = text.lower().strip()
            return 1 if lower in ("yes", "y", "yeah") else 0
        return text

    def _should_alert(self, question_def, value):
        if question_def.alert_threshold is None:
            return False
        return int(value) >= question_def.alert_threshold

    def _find_question_def(self, phase, question_text):
        phase_enum = RecoveryPhase(phase)
        for q in CHECK_IN_QUESTIONS.get(phase_enum, []):
            if q.question == question_text:
                return q
        return SymptomCheckIn(question_text, "free_text")

Alert Trigger System

When symptom responses exceed thresholds, the agent escalates to the clinical team through the appropriate channel based on severity.

class AlertSeverity(Enum):
    LOW = "low"         # log only
    MEDIUM = "medium"   # notification to provider
    HIGH = "high"       # immediate page
    CRITICAL = "critical"  # emergency protocol

class ClinicalAlertService:
    def __init__(self, db, pager, notification_svc):
        self.db = db
        self.pager = pager
        self.notify = notification_svc

    async def notify_clinical_team(
        self, patient_id: str, alert_type: str,
        details: str,
    ):
        severity = self._assess_severity(
            alert_type, details
        )

        await self.db.execute("""
            INSERT INTO clinical_alerts
                (patient_id, alert_type, severity,
                 details, created_at, resolved)
            VALUES ($1, $2, $3, $4, $5, false)
        """, patient_id, alert_type, severity.value,
             details, datetime.utcnow())

        if severity == AlertSeverity.HIGH:
            provider = await self._get_treating_provider(
                patient_id
            )
            await self.pager.send(
                provider["phone"],
                f"POST-OP ALERT: {details}",
            )
        elif severity == AlertSeverity.CRITICAL:
            await self.pager.send_emergency(
                patient_id, details
            )

    def _assess_severity(self, alert_type, details):
        if "fever" in details.lower():
            return AlertSeverity.HIGH
        if "bleeding" in details.lower():
            return AlertSeverity.HIGH
        if "increasing pain" in details.lower():
            return AlertSeverity.MEDIUM
        return AlertSeverity.LOW

Follow-Up Appointment Auto-Scheduling

The agent automatically schedules follow-up appointments based on the procedure type and recovery timeline.

FOLLOW_UP_RULES = {
    "extraction": {"days_after": 7, "type": "post_op_check"},
    "root_canal": {"days_after": 14, "type": "crown_consult"},
    "implant": {"days_after": 10, "type": "implant_check"},
}

class FollowUpScheduler:
    def __init__(self, db, schedule_manager):
        self.db = db
        self.scheduler = schedule_manager

    async def schedule_follow_up(
        self, patient_id: str, procedure_type: str,
        procedure_date: datetime, provider_id: str,
    ):
        rule = FOLLOW_UP_RULES.get(procedure_type)
        if not rule:
            return None

        target_date = (
            procedure_date + timedelta(days=rule["days_after"])
        ).date()

        slots = await self.scheduler.find_available_slots(
            appointment_type=rule["type"],
            preferred_date=target_date,
            provider_id=provider_id,
        )

        if slots:
            appointment = await self.scheduler.book_appointment(
                patient_id=patient_id,
                slot=slots[0],
                appointment_type=rule["type"],
            )
            return appointment
        return None

FAQ

How does the agent know when to escalate versus when to reassure the patient?

The alert system uses a combination of threshold-based rules and trend analysis. A single high pain score triggers a notification, but the system also watches for trends — pain that increases day over day even if below the absolute threshold still gets flagged. The clinical team defines the thresholds per procedure type, and the system never provides medical advice beyond the pre-approved care instructions.

Still reading? Stop comparing — try CallSphere live.

CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.

What if the patient does not respond to a symptom check-in?

If a patient misses a check-in, the agent sends one follow-up message two hours later. If there is still no response, the front desk receives a task to call the patient directly. The system never assumes that silence means everything is fine — a non-response after a surgical procedure is treated as a reason for human follow-up.

Can the post-op agent handle multiple simultaneous procedures, such as multiple extractions done in one visit?

Yes. Each procedure creates its own recovery timeline, but the agent consolidates messages so the patient receives one check-in that covers all procedures rather than separate messages for each tooth. The most conservative recovery instructions take precedence — for example, if one extraction was surgical and one was simple, the surgical recovery guidelines apply to the overall care plan.


#PostOpCare #RecoveryMonitoring #HealthcareAI #PatientFollowUp #Python #AgenticAI #LearnAI #AIEngineering

Share

Try CallSphere AI Voice Agents

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