By Sagar Shankaran, Founder of CallSphere
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.
Key takeaways
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.
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
}
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 for healthcare in your browser — 60 seconds, no signup.
@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")
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
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
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.
See the healthcare AI agent handle a real call — complete, industry-specific, and live in your browser. No signup.
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.
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

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
AWS HealthScribe became the open scribe layer EHR vendors built on top of in 2026. Here's the API surface, the per-encounter pricing, the BAA terms.
Apollo, Manipal, and Narayana scaled AI agents across Bangalore in 2026. Here's the deployments across radiology, intake, and follow-up, the costs.
Notable's AI agents now handle scheduling, intake, and revenue cycle for 6,000+ clinics in 2026. Here's the multi-agent architecture, the per-clinic pricing.
Abridge raised $250M in April 2026 at a $2.7B valuation. We break down the deployment numbers, the EHR integrations across Epic and Cerner. The Q2 2026 buyer briefing.
Enterprise CIO Guide perspective on Hippocratic AI's deployment numbers show healthcare voice agents are moving from pilot to production across major US health systems.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.