What You'll Build
If you've ever watched a front desk team juggle phone calls, paper calendars, and back-to-back appointment conflicts, you already know the problem this tutorial solves. By the end of this guide, you'll have a working multi-agent healthcare scheduling system built in Python using the Claude API that can book appointments, detect conflicts, and suggest optimized time slots — all through a conversational interface.
The system uses Claude's tool use feature to give the AI real scheduling capabilities, not just the ability to talk about them. You'll walk away with production-ready code you can drop into a clinic, therapy practice, or any appointment-based business.
All the code you need is broken into clear steps below. Each snippet builds on the last, and by Step 4 you'll have the complete working system. Copy the pieces in order and they'll run together as one cohesive application.
Prerequisites
- Python 3.9 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python classes and functions
anthropicPython SDK installed (pip install anthropic)- Optional but helpful: some understanding of how tool use works in LLM APIs
Step 1: Initialize the Claude Client and Define Tools
The first thing we need to do is set up the Anthropic client and define the tools our scheduling agent will use. Think of tools as functions that Claude can decide to call when it needs to take a real action — like checking a calendar or booking a slot.
We're defining four core tools here: one to check available slots, one to book an appointment, one to cancel, and one to list existing appointments. These become the agent's "hands."
scheduling_tools.pyimport anthropic
import json
from datetime import datetime, timedelta
from typing import Any
# Initialize the Anthropic client
client = anthropic.Anthropic()
# In-memory appointment store for this demo
# In production, replace this with a real database connection
appointments_db: dict[str, dict] = {}
# Define the tools Claude can use during scheduling conversations
SCHEDULING_TOOLS = [
{
"name": "check_available_slots",
"description": "Check available appointment slots for a given date and provider. Returns a list of open time slots.",
"input_schema": {
"type": "object",
"properties": {
"date": {
"type": "string",
"description": "Date to check in YYYY-MM-DD format"
},
"provider_id": {
"type": "string",
"description": "The ID of the healthcare provider (e.g., 'dr_smith')"
},
"duration_minutes": {
"type": "integer",
"description": "Required appointment duration in minutes (default 30)"
}
},
"required": ["date", "provider_id"]
}
},
{
"name": "book_appointment",
"description": "Book an appointment for a patient with a specific provider at a given time.",
"input_schema": {
"type": "object",
"properties": {
"patient_name": {
"type": "string",
"description": "Full name of the patient"
},
"provider_id": {
"type": "string",
"description": "The ID of the healthcare provider"
},
"date": {
"type": "string",
"description": "Appointment date in YYYY-MM-DD format"
},
"time": {
"type": "string",
"description": "Appointment time in HH:MM format (24-hour)"
},
"appointment_type": {
"type": "string",
"description": "Type of appointment (e.g., 'checkup', 'follow-up', 'consultation')"
},
"duration_minutes": {
"type": "integer",
"description": "Duration of appointment in minutes"
}
},
"required": ["patient_name", "provider_id", "date", "time", "appointment_type"]
}
},
{
"name": "cancel_appointment",
"description": "Cancel an existing appointment by appointment ID.",
"input_schema": {
"type": "object",
"properties": {
"appointment_id": {
"type": "string",
"description": "The unique ID of the appointment to cancel"
},
"reason": {
"type": "string",
"description": "Reason for cancellation"
}
},
"required": ["appointment_id"]
}
},
{
"name": "list_appointments",
"description": "List all appointments for a provider on a specific date, or all appointments for a patient.",
"input_schema": {
"type": "object",
"properties": {
"provider_id": {
"type": "string",
"description": "Filter by provider ID (optional)"
},
"patient_name": {
"type": "string",
"description": "Filter by patient name (optional)"
},
"date": {
"type": "string",
"description": "Filter by date in YYYY-MM-DD format (optional)"
}
},
"required": []
}
}
]
Notice we're keeping the appointments in a simple dictionary for now. In a real deployment, you'd swap that out for a PostgreSQL table or whatever your clinic already uses. The tool definitions themselves don't change — that's the beauty of this architecture.
Step 2: Build the Scheduling Agent with Tool Use
Now we build the actual agent class. This is where the scheduling logic lives — the functions that get called when Claude decides to use one of the tools we defined above.
Each tool function does real work: it checks for conflicts, generates appointment IDs, and returns structured data that Claude can reason about in its next response.
scheduling_agent.pyimport anthropic
import json
import uuid
from datetime import datetime, timedelta
from typing import Any
client = anthropic.Anthropic()
appointments_db: dict[str, dict] = {}
# Provider schedules define when each doctor is available
PROVIDER_SCHEDULES = {
"dr_smith": {
"name": "Dr. Sarah Smith",
"specialty": "General Practice",
"hours": {"start": "09:00", "end": "17:00"},
"slot_duration": 30
},
"dr_jones": {
"name": "Dr. Marcus Jones",
"specialty": "Cardiology",
"hours": {"start": "08:00", "end": "16:00"},
"slot_duration": 45
}
}
class HealthcareSchedulingAgent:
def __init__(self):
self.client = anthropic.Anthropic()
self.conversation_history = []
self.system_prompt = """You are a helpful healthcare scheduling assistant for a medical clinic.
Your job is to help patients book, reschedule, and cancel appointments efficiently.
Always confirm appointment details before booking. Be friendly but concise.
When checking availability, always use the check_available_slots tool first.
Never book an appointment without first verifying the slot is available."""
def check_available_slots(self, date: str, provider_id: str, duration_minutes: int = 30) -> dict:
"""Generate available time slots by checking against existing bookings."""
if provider_id not in PROVIDER_SCHEDULES:
return {"error": f"Provider {provider_id} not found", "available_slots": []}
provider = PROVIDER_SCHEDULES[provider_id]
slot_duration = provider["slot_duration"]
# Parse provider working hours
start_hour, start_min = map(int, provider["hours"]["start"].split(":"))
end_hour, end_min = map(int, provider["hours"]["end"].split(":"))
# Build list of all possible slots for the day
all_slots = []
current = datetime.strptime(f"{date} {provider['hours']['start']}", "%Y-%m-%d %H:%M")
end_time = datetime.strptime(f"{date} {provider['hours']['end']}", "%Y-%m-%d %H:%M")
while current + timedelta(minutes=slot_duration) <= end_time:
all_slots.append(current.strftime("%H:%M"))
current += timedelta(minutes=slot_duration)
# Remove slots that are already booked for this provider on this date
booked_slots = [
appt["time"] for appt in appointments_db.values()
if appt["provider_id"] == provider_id and appt["date"] == date and appt["status"] == "confirmed"
]
available = [slot for slot in all_slots if slot not in booked_slots]
return {
"provider_name": provider["name"],
"specialty": provider["specialty"],
"date": date,
"available_slots": available,
"slot_duration_minutes": slot_duration,
"total_available": len(available)
}
def book_appointment(
self,
patient_name: str,
provider_id: str,
date: str,
time: str,
appointment_type: str,
duration_minutes: int = 30
) -> dict:
"""Book an appointment after validating no conflict exists."""
# Run conflict detection before booking
conflict = self._detect_conflict(provider_id, date, time, duration_minutes)
if conflict:
return {
"success": False,
"error": "Time slot conflict detected",
"conflict_details": conflict,
"suggestion": "Please check available slots and choose a different time."
}
appointment_id = f"APT-{uuid.uuid4().hex[:8].upper()}"
appointments_db[appointment_id] = {
"appointment_id": appointment_id,
"patient_name": patient_name,
"provider_id": provider_id,
"provider_name": PROVIDER_SCHEDULES.get(provider_id, {}).get("name", "Unknown"),
"date": date,
"time": time,
"appointment_type": appointment_type,
"duration_minutes": duration_minutes or PROVIDER_SCHEDULES.get(provider_id, {}).get("slot_duration", 30),
"status": "confirmed",
"created_at": datetime.now().isoformat()
}
return {
"success": True,
"appointment_id": appointment_id,
"confirmation": f"Appointment confirmed for {patient_name} with {PROVIDER_SCHEDULES.get(provider_id, {}).get('name', provider_id)} on {date} at {time}.",
"details": appointments_db[appointment_id]
}
def cancel_appointment(self, appointment_id: str, reason: str = "Patient request") -> dict:
"""Cancel an appointment and free up the slot."""
if appointment_id not in appointments_db:
return {"success": False, "error": f"Appointment {appointment_id} not found"}
appointments_db[appointment_id]["status"] = "cancelled"
appointments_db[appointment_id]["cancellation_reason"] = reason
appointments_db[appointment_id]["cancelled_at"] = datetime.now().isoformat()
return {
"success": True,
"message": f"Appointment {appointment_id} has been cancelled.",
"freed_slot": {
"date": appointments_db[appointment_id]["date"],
"time": appointments_db[appointment_id]["time"],
"provider_id": appointments_db[appointment_id]["provider_id"]
}
}
def list_appointments(
self,
provider_id: str = None,
patient_name: str = None,
date: str = None
) -> dict:
"""Return filtered list of appointments."""
results = []
for appt in appointments_db.values():
if appt["status"] != "confirmed":
continue
if provider_id and appt["provider_id"] != provider_id:
continue
if patient_name and patient_name.lower() not in appt["patient_name"].lower():
continue
if date and appt["date"] != date:
continue
results.append(appt)
# Sort by date then time for readability
results.sort(key=lambda x: (x["date"], x["time"]))
return {
"appointments": results,
"total_count": len(results)
}
def _detect_conflict(self, provider_id: str, date: str, time: str, duration_minutes: int) -> dict | None:
"""Check if a new appointment would overlap with an existing one."""
new_start = datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M")
slot_dur = duration_minutes or PROVIDER_SCHEDULES.get(provider_id, {}).get("slot_duration", 30)
new_end = new_start + timedelta(minutes=slot_dur)
for appt_id, appt in appointments_db.items():
if appt["provider_id"] != provider_id:
continue
if appt["date"] != date:
continue
if appt["status"] != "confirmed":
continue
existing_start = datetime.strptime(f"{appt['date']} {appt['time']}", "%Y-%m-%d %H:%M")
existing_end = existing_start + timedelta(minutes=appt["duration_minutes"])
# Check for any overlap between time windows
if new_start < existing_end and new_end > existing_start:
return {
"conflicting_appointment_id": appt_id,
"patient": appt["patient_name"],
"time": appt["time"],
"duration": appt["duration_minutes"]
}
return None
def process_tool_call(self, tool_name: str, tool_input: dict) -> Any:
"""Route tool calls from Claude to the appropriate method."""
tool_map = {
"check_available_slots": self.check_available_slots,
"book_appointment": self.book_appointment,
"cancel_appointment": self.cancel_appointment,
"list_appointments": self.list_appointments
}
if tool_name not in tool_map:
return {"error": f"Unknown tool: {tool_name}"}
return tool_map[tool_name](**tool_input)
Claude is great at reasoning, but you never want your business logic to live only in a prompt. The
_detect_conflict method runs in Python before any booking goes through — Claude just gets a clear success or failure response to work with.
Step 3: Implement Conflict Detection and Optimization
The conflict detection already lives in the agent class, but let's add a dedicated optimizer that finds the best available slot when a requested time is taken. This turns a dead-end "sorry, that's booked" into a genuinely helpful "here are your next three open options."
slot_optimizer.pyimport anthropic
import json
from datetime import datetime, timedelta
from typing import Any
# Reuse the same appointments_db and PROVIDER_SCHEDULES from scheduling_agent.py
# In production, these would be imported from a shared module or database layer
appointments_db: dict[str, dict] = {}
PROVIDER_SCHEDULES = {
"dr_smith": {
"name": "Dr. Sarah Smith",
"specialty": "General Practice",
"hours": {"start": "09:00", "end": "17:00"},
"slot_duration": 30
},
"dr_jones": {
"name": "Dr. Marcus Jones",
"specialty": "Cardiology",
"hours": {"start": "08:00", "end": "16:00"},
"slot_duration": 45
}
}
def find_next_available_slots(
provider_id: str,
preferred_date: str,
num_suggestions: int = 3,
days_to_search: int = 7
) -> dict:
"""
Find the next N available slots starting from preferred_date.
Searches forward up to days_to_search days to find enough options.
"""
if provider_id not in PROVIDER_SCHEDULES:
return {"error": f"Provider {provider_id} not found", "suggestions": []}
provider = PROVIDER_SCHEDULES[provider_id]
slot_duration = provider["slot_duration"]
suggestions = []
search_date = datetime.strptime(preferred_date, "%Y-%m-%d")
for day_offset in range(days_to_search):
current_date = search_date + timedelta(days=day_offset)
date_str = current_date.strftime("%Y-%m-%d")
# Skip weekends — adjust this logic for your clinic's schedule
if current_date.weekday() >= 5:
continue
# Build all slots for this day
start_dt = datetime.strptime(f"{date_str} {provider['hours']['start']}", "%Y-%m-%d %H:%M")
end_dt = datetime.strptime(f"{date_str} {provider['hours']['end']}", "%Y-%m-%d %H:%M")
current_slot = start_dt
while current_slot + timedelta(minutes=slot_duration) <= end_dt:
time_str = current_slot.strftime("%H:%M")
# Check if this slot is already booked
is_booked = any(
appt["provider_id"] == provider_id
and appt["date"] == date_str
and appt["time"] == time_str
and appt["status"] == "confirmed"
for appt in appointments_db.values()
)
if not is_booked:
suggestions.append({
"date": date_str,
"time": time_str,
"day_of_week": current_date.strftime("%A"),
"formatted": f"{current_date.strftime('%A, %B %d')} at {time_str}"
})
if len(suggestions) >= num_suggestions:
break
current_slot += timedelta(minutes=slot_duration)
if len(suggestions) >= num_suggestions:
break
return {
"provider_name": provider["name"],
"preferred_date": preferred_date,
"suggestions": suggestions,
"total_found": len(suggestions)
}
def validate_appointment_request(
patient_name: str,
provider_id: str,
date: str,
time: str,
appointment_type: str
) -> dict:
"""
Validate all fields of an appointment request before attempting to book.
Returns a validation result with any issues found.
"""
issues = []
# Validate patient name is not empty
if not patient_name or len(patient_name.strip()) < 2:
issues.append("Patient name must be at least 2 characters.")
# Validate provider exists
if provider_id not in PROVIDER_SCHEDULES:
issues.append(f"Provider '{provider_id}' does not exist. Available: {list(PROVIDER_SCHEDULES.keys())}")
# Validate date format and that it's not in the past
try:
appt_date = datetime.strptime(date, "%Y-%m-%d")
if appt_date.date() < datetime.now().date():
issues.append("Appointment date cannot be in the past.")
if appt_date.weekday() >= 5:
issues.append("Appointments are not available on weekends.")
except ValueError:
issues.append(f"Invalid date format '{date}'. Use YYYY-MM-DD.")
# Validate time format and that it falls within provider hours
try:
appt_time = datetime.strptime(time, "%H:%M")
if provider_id in PROVIDER_SCHEDULES:
start = datetime.strptime(PROVIDER_SCHEDULES[provider_id]["hours"]["start"], "%H:%M")
end = datetime.strptime(PROVIDER_SCHEDULES[provider_id]["hours"]["end"], "%H:%M")
if not (start <= appt_time < end):
issues.append(
f"Time {time} is outside provider hours "
f"({PROVIDER_SCHEDULES[provider_id]['hours']['start']} - "
f"{PROVIDER_SCHEDULES[provider_id]['hours']['end']})."
)
except ValueError:
issues.append(f"Invalid time format '{time}'. Use HH:MM (24-hour).")
# Validate appointment type
valid_types = ["checkup", "follow-up", "consultation", "urgent", "procedure"]
if appointment_type.lower() not in valid_types:
issues.append(f"Appointment type must be one of: {valid_types}")
return {
"is_valid": len(issues) == 0,
"issues": issues,
"message": "Validation passed" if len(issues) == 0 else f"{len(issues)} issue(s) found"
}
Step 4: Create the Agent Loop with Multi-Turn Interactions
This is the core of the whole system — the loop that lets Claude handle back-and-forth scheduling conversations, call tools when it needs to, and keep context across multiple messages. This is where everything comes together.
We're also adding a demonstration scenario at the bottom so you can run the file directly and see real output.
agent_loop.pyimport anthropic
import json
from datetime import datetime
from typing import Any
# ── Tool definitions (same as scheduling_tools.py) ──────────────────────────
SCHEDULING_TOOLS = [
{
"name": "check_available_slots",
"description": "Check available