Building a Dental Appointment Agent: Schedule Management, Reminders, and Insurance Verification
Learn how to build an AI agent that manages dental appointment scheduling, sends reminder sequences, verifies insurance eligibility, and matches patients to available time slots with working Python code.
Why Dental Practices Need Scheduling Agents
Front desk staff at dental practices spend an estimated 60 percent of their phone time handling appointment requests, rescheduling, and verifying insurance. An AI appointment agent handles these tasks around the clock, reducing no-shows through automated reminders and catching insurance issues before the patient arrives.
This tutorial walks through building a complete dental appointment agent that manages the schedule, sends reminders at the right times, and verifies insurance coverage before each visit.
Core Data Models
Start by defining the data structures that represent appointments, patients, and provider schedules.
from dataclasses import dataclass, field
from datetime import datetime, date, time, timedelta
from enum import Enum
from typing import Optional
import uuid
class AppointmentType(Enum):
CLEANING = "cleaning"
EXAM = "exam"
FILLING = "filling"
CROWN = "crown"
ROOT_CANAL = "root_canal"
EXTRACTION = "extraction"
EMERGENCY = "emergency"
PROCEDURE_DURATIONS = {
AppointmentType.CLEANING: 60,
AppointmentType.EXAM: 30,
AppointmentType.FILLING: 45,
AppointmentType.CROWN: 90,
AppointmentType.ROOT_CANAL: 120,
AppointmentType.EXTRACTION: 60,
AppointmentType.EMERGENCY: 30,
}
@dataclass
class Patient:
id: str
first_name: str
last_name: str
phone: str
email: str
insurance_id: Optional[str] = None
insurance_group: Optional[str] = None
last_visit: Optional[date] = None
@dataclass
class TimeSlot:
provider_id: str
start: datetime
end: datetime
is_available: bool = True
@dataclass
class Appointment:
id: str = field(default_factory=lambda: str(uuid.uuid4()))
patient_id: str = ""
provider_id: str = ""
appointment_type: AppointmentType = AppointmentType.EXAM
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
insurance_verified: bool = False
reminder_sent: bool = False
status: str = "scheduled"
Schedule Management Engine
The scheduling engine finds open slots, handles conflicts, and respects provider availability. The key challenge is avoiding double-booking while maximizing chair utilization.
class ScheduleManager:
def __init__(self, db_connection):
self.db = db_connection
async def find_available_slots(
self,
appointment_type: AppointmentType,
preferred_date: date,
provider_id: Optional[str] = None,
search_days: int = 7,
) -> list[TimeSlot]:
duration = PROCEDURE_DURATIONS[appointment_type]
available = []
for day_offset in range(search_days):
check_date = preferred_date + timedelta(days=day_offset)
if check_date.weekday() >= 5:
continue # skip weekends
query = """
SELECT p.id as provider_id, p.name,
s.start_time, s.end_time
FROM provider_schedules s
JOIN providers p ON p.id = s.provider_id
WHERE s.schedule_date = $1
AND ($2::uuid IS NULL OR p.id = $2)
ORDER BY s.start_time
"""
rows = await self.db.fetch(
query, check_date, provider_id
)
for row in rows:
slots = self._split_into_slots(
row["provider_id"],
row["start_time"],
row["end_time"],
duration,
)
for slot in slots:
if await self._is_slot_free(slot):
available.append(slot)
return available
def _split_into_slots(
self, provider_id, start, end, duration_min
):
slots = []
current = start
while current + timedelta(minutes=duration_min) <= end:
slots.append(TimeSlot(
provider_id=provider_id,
start=current,
end=current + timedelta(minutes=duration_min),
))
current += timedelta(minutes=15) # 15-min increments
return slots
async def _is_slot_free(self, slot: TimeSlot) -> bool:
conflict = await self.db.fetchrow("""
SELECT id FROM appointments
WHERE provider_id = $1
AND status != 'cancelled'
AND start_time < $3
AND end_time > $2
""", slot.provider_id, slot.start, slot.end)
return conflict is None
async def book_appointment(
self, patient: Patient, slot: TimeSlot,
appt_type: AppointmentType,
) -> Appointment:
if not await self._is_slot_free(slot):
raise ValueError("Slot is no longer available")
appt = Appointment(
patient_id=patient.id,
provider_id=slot.provider_id,
appointment_type=appt_type,
start_time=slot.start,
end_time=slot.end,
)
await self.db.execute("""
INSERT INTO appointments
(id, patient_id, provider_id, type,
start_time, end_time, status)
VALUES ($1, $2, $3, $4, $5, $6, 'scheduled')
""", appt.id, appt.patient_id, appt.provider_id,
appt.appointment_type.value,
appt.start_time, appt.end_time)
return appt
Reminder Sequence System
Reminders reduce no-shows by up to 40 percent. The agent sends a sequence: confirmation immediately after booking, a reminder 48 hours before, and a final reminder two hours before the appointment.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
from enum import Enum as PyEnum
class ReminderStage(PyEnum):
CONFIRMATION = "confirmation"
DAY_BEFORE = "48_hours"
SAME_DAY = "2_hours"
class ReminderEngine:
SCHEDULE = {
ReminderStage.CONFIRMATION: timedelta(minutes=0),
ReminderStage.DAY_BEFORE: timedelta(hours=-48),
ReminderStage.SAME_DAY: timedelta(hours=-2),
}
def __init__(self, sms_client, email_client):
self.sms = sms_client
self.email = email_client
async def process_pending_reminders(self, db):
now = datetime.utcnow()
appointments = await db.fetch("""
SELECT a.*, p.phone, p.email, p.first_name
FROM appointments a
JOIN patients p ON p.id = a.patient_id
WHERE a.status = 'scheduled'
AND a.start_time > $1
""", now)
for appt in appointments:
for stage, offset in self.SCHEDULE.items():
send_at = appt["start_time"] + offset
if now >= send_at:
already_sent = await db.fetchrow("""
SELECT id FROM reminders
WHERE appointment_id = $1
AND stage = $2
""", appt["id"], stage.value)
if not already_sent:
await self._send_reminder(
appt, stage
)
await db.execute("""
INSERT INTO reminders
(appointment_id, stage, sent_at)
VALUES ($1, $2, $3)
""", appt["id"], stage.value, now)
async def _send_reminder(self, appt, stage):
message = self._build_message(appt, stage)
await self.sms.send(appt["phone"], message)
await self.email.send(appt["email"], message)
Insurance Verification Integration
Before the appointment, the agent verifies the patient's insurance eligibility by calling the payer's API. This catches expired plans and missing coverage before the patient arrives.
class InsuranceVerifier:
def __init__(self, clearinghouse_client):
self.client = clearinghouse_client
async def verify_eligibility(
self, patient: Patient, procedure_code: str,
service_date: date,
) -> dict:
response = await self.client.check_eligibility(
subscriber_id=patient.insurance_id,
group_number=patient.insurance_group,
procedure_code=procedure_code,
service_date=service_date.isoformat(),
)
return {
"eligible": response.get("active", False),
"copay": response.get("copay_amount"),
"deductible_remaining": response.get(
"deductible_remaining"
),
"coverage_percent": response.get(
"coinsurance_percent", 0
),
"plan_name": response.get("plan_description"),
"requires_preauth": response.get(
"preauthorization_required", False
),
}
Wiring It Into the Agent Loop
Expose each capability as a tool so the language model can call the right function based on the patient's request.
from agents import Agent, function_tool
@function_tool
async def find_openings(
procedure: str, preferred_date: str,
provider_name: str = "",
) -> str:
appt_type = AppointmentType(procedure)
pref = date.fromisoformat(preferred_date)
slots = await schedule_mgr.find_available_slots(
appt_type, pref
)
if not slots:
return "No openings found in the next 7 days."
lines = [
f"{s.start:%A %B %d at %I:%M %p}" for s in slots[:5]
]
return "Available slots:\n" + "\n".join(lines)
dental_agent = Agent(
name="Dental Appointment Agent",
instructions=(
"You help patients schedule dental appointments. "
"Find openings, book slots, and verify insurance. "
"Always confirm the procedure type and preferred "
"date before searching."
),
tools=[find_openings],
)
FAQ
How does the agent prevent double-booking when two patients call at the same time?
The book_appointment method re-checks slot availability inside the same database transaction that creates the appointment record. Using a database-level constraint or SELECT ... FOR UPDATE ensures that only one booking succeeds for any given time range, even under concurrent requests.
What happens if insurance verification fails or the payer API is down?
The agent flags the appointment as "insurance pending" and schedules a retry. The front desk receives a notification so they can follow up manually if automated verification does not succeed within 24 hours of the appointment.
Can the reminder schedule be customized per practice?
Yes. The SCHEDULE dictionary in ReminderEngine is configurable. Practices can adjust timing, add additional stages like a one-week reminder, or disable specific channels such as SMS-only or email-only based on patient preferences.
#DentalAI #AppointmentScheduling #InsuranceVerification #HealthcareAI #Python #AgenticAI #LearnAI #AIEngineering
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.