What You'll Build
By the end of this tutorial, you'll have a working AI scheduling agent that handles appointment booking, checks provider availability, and sends patient reminders — all powered by Claude's tool use API. The whole thing runs in Python, uses the Anthropic SDK, and clocks in under 150 lines of production-quality code. This is the exact kind of healthcare scheduling automation with the Claude API that replaces a full-time front desk task for a fraction of the cost.
The agent accepts natural language requests like "Book me a checkup with Dr. Patel on Friday afternoon" and autonomously calls the right tools to make it happen. No hardcoded decision trees, no brittle regex — just Claude reasoning through the request and executing the correct sequence of actions.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python classes and functions
anthropicandpython-dotenvpackages (we'll install these in Step 1)- A terminal and a code editor — that's genuinely all you need
The complete, working code is broken into clearly labeled steps below. Each snippet builds on the last, and by Step 4 you'll have the entire agent assembled. Copy each block in order and you'll have a running system. A consolidated version is shown in Step 3 for reference.
Step 1: Set Up Your Claude API Project and Dependencies
First, create a project folder and install the two packages you need. I like keeping environment variables in a .env file so the API key never ends up in version control.
mkdir healthcare-scheduler cd healthcare-scheduler pip install anthropic python-dotenv
Create a .env file in that folder with your key:
ANTHROPIC_API_KEY=sk-ant-your-key-here
Now create the main file. This first snippet sets up imports and loads the environment. Everything else we write will live in this same file.
scheduler.pyimport os
import json
from datetime import datetime, timedelta
from typing import Any
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"dotenv pattern takes ten seconds and saves real headaches.
Step 2: Define the Scheduling Tools
Claude's tool use feature lets you hand the model a list of functions it can call. You define each tool with a name, a description, and a JSON schema for its inputs — Claude reads those descriptions and decides when and how to call each one. Think of it like giving a new hire a reference sheet for which button to press.
We're defining three tools: check_availability, create_appointment, and send_reminder. Below the tool definitions, we write the actual Python functions that execute when Claude picks a tool.
# Tool schema definitions — Claude reads these to decide which tool to call
TOOLS = [
{
"name": "check_availability",
"description": (
"Check whether a specific provider has open appointment slots "
"on a given date. Returns available time slots as a list."
),
"input_schema": {
"type": "object",
"properties": {
"provider_name": {
"type": "string",
"description": "Full name of the doctor or provider, e.g. 'Dr. Patel'"
},
"date": {
"type": "string",
"description": "Date to check in YYYY-MM-DD format"
}
},
"required": ["provider_name", "date"]
}
},
{
"name": "create_appointment",
"description": (
"Book a confirmed appointment for a patient with a specific provider "
"at a specific date and time. Returns a confirmation ID."
),
"input_schema": {
"type": "object",
"properties": {
"patient_name": {
"type": "string",
"description": "Full name of the patient"
},
"provider_name": {
"type": "string",
"description": "Full name of the doctor or provider"
},
"date": {
"type": "string",
"description": "Appointment date in YYYY-MM-DD format"
},
"time_slot": {
"type": "string",
"description": "Appointment time in HH:MM format, e.g. '14:30'"
},
"appointment_type": {
"type": "string",
"description": "Type of visit, e.g. 'checkup', 'follow-up', 'urgent care'"
}
},
"required": ["patient_name", "provider_name", "date", "time_slot", "appointment_type"]
}
},
{
"name": "send_reminder",
"description": (
"Send an appointment reminder to a patient via their preferred contact method. "
"Use this after successfully booking an appointment."
),
"input_schema": {
"type": "object",
"properties": {
"patient_name": {
"type": "string",
"description": "Full name of the patient"
},
"contact_info": {
"type": "string",
"description": "Patient email address or phone number"
},
"appointment_details": {
"type": "string",
"description": "Human-readable summary of the appointment details"
},
"reminder_method": {
"type": "string",
"enum": ["email", "sms"],
"description": "How to send the reminder: 'email' or 'sms'"
}
},
"required": ["patient_name", "contact_info", "appointment_details", "reminder_method"]
}
}
]
# --- Tool execution functions ---
# These run the actual logic when Claude decides to call a tool.
# In a real system, these would connect to your EHR database or scheduling backend.
def check_availability(provider_name: str, date: str) -> dict:
"""Simulates querying a provider's calendar for open slots."""
# Simulate some providers being busier than others
base_slots = ["09:00", "09:30", "10:00", "11:00", "14:00", "14:30", "15:00", "16:00"]
busy_providers = {"Dr. Smith": ["09:00", "10:00", "14:00"]}
booked = busy_providers.get(provider_name, [])
available = [s for s in base_slots if s not in booked]
return {
"provider": provider_name,
"date": date,
"available_slots": available,
"total_open": len(available)
}
def create_appointment(
patient_name: str,
provider_name: str,
date: str,
time_slot: str,
appointment_type: str
) -> dict:
"""Simulates writing a new appointment record to the scheduling system."""
confirmation_id = f"APT-{datetime.now().strftime('%Y%m%d%H%M%S')}"
return {
"status": "confirmed",
"confirmation_id": confirmation_id,
"patient": patient_name,
"provider": provider_name,
"date": date,
"time": time_slot,
"type": appointment_type,
"message": f"Appointment successfully booked. Confirmation: {confirmation_id}"
}
def send_reminder(
patient_name: str,
contact_info: str,
appointment_details: str,
reminder_method: str
) -> dict:
"""Simulates dispatching a reminder via email or SMS."""
# In production, plug in SendGrid for email or Twilio for SMS here
print(f"\n[REMINDER SENT via {reminder_method.upper()}]")
print(f" To: {patient_name} ({contact_info})")
print(f" Message: {appointment_details}\n")
return {
"status": "sent",
"method": reminder_method,
"recipient": patient_name,
"contact": contact_info
}
def execute_tool(tool_name: str, tool_input: dict) -> Any:
"""Routes a tool call from Claude to the correct Python function."""
if tool_name == "check_availability":
return check_availability(**tool_input)
elif tool_name == "create_appointment":
return create_appointment(**tool_input)
elif tool_name == "send_reminder":
return send_reminder(**tool_input)
else:
return {"error": f"Unknown tool: {tool_name}"}Step 3: Build the Main Scheduling Agent Class
The agent class is the brain of the operation. It holds conversation history, sends messages to Claude, and knows how to handle tool call responses. Keeping this in a class makes it easy to run multiple patient conversations in parallel later.
scheduler.py (continued)class HealthcareSchedulingAgent:
"""
A Claude-powered agent that handles patient scheduling requests
by reasoning over available tools and executing them autonomously.
"""
def __init__(self):
self.conversation_history = []
self.system_prompt = (
"You are a professional healthcare scheduling assistant. "
"Your job is to help patients book appointments, check provider availability, "
"and send appointment reminders. Always check availability before booking. "
"After booking, always send a reminder. Be concise, warm, and accurate. "
"Today's date is " + datetime.now().strftime("%Y-%m-%d") + ". "
"When a patient mentions a relative day like 'Friday', resolve it to a real date."
)
def run(self, patient_request: str) -> str:
"""
Accept a natural-language patient request and return the agent's final response.
This method drives the full agentic loop internally.
"""
print(f"\n{'='*60}")
print(f"PATIENT REQUEST: {patient_request}")
print(f"{'='*60}")
# Add the patient's message to conversation history
self.conversation_history.append({
"role": "user",
"content": patient_request
})
# Hand off to the agentic loop
final_response = self._run_agentic_loop()
return final_response
def _run_agentic_loop(self) -> str:
"""
The core loop: send messages to Claude, handle tool calls,
feed results back, and repeat until Claude gives a final text response.
"""
while True:
response = client.messages.create(
model=MODEL,
max_tokens=4096,
system=self.system_prompt,
tools=TOOLS,
messages=self.conversation_history
)
print(f"\n[Claude stop reason: {response.stop_reason}]")
# If Claude is done reasoning and has a final answer, return it
if response.stop_reason == "end_turn":
final_text = self._extract_text(response)
self.conversation_history.append({
"role": "assistant",
"content": response.content
})
return final_text
# If Claude wants to call tools, handle all tool calls in this turn
if response.stop_reason == "tool_use":
tool_results = self._handle_tool_calls(response)
# Add Claude's tool-calling response to history
self.conversation_history.append({
"role": "assistant",
"content": response.content
})
# Add all tool results back so Claude can continue reasoning
self.conversation_history.append({
"role": "user",
"content": tool_results
})
# Loop again — Claude will now process the tool results
def _handle_tool_calls(self, response) -> list:
"""Execute every tool Claude requested and collect results."""
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"\n[Tool call: {block.name}]")
print(f" Input: {json.dumps(block.input, indent=2)}")
result = execute_tool(block.name, block.input)
print(f" Result: {json.dumps(result, indent=2)}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
return tool_results
def _extract_text(self, response) -> str:
"""Pull plain text out of Claude's response content blocks."""
text_parts = []
for block in response.content:
if hasattr(block, "text"):
text_parts.append(block.text)
return " ".join(text_parts).strip()Step 4: Implement the Agentic Loop with Tool Use
The agentic loop is already baked into the _run_agentic_loop method above, but let me walk through exactly what happens on each iteration. Claude returns either end_turn (it's done) or tool_use (it needs to call a function). When it's tool_use, we execute the tools and append the results to history, then call Claude again. This continues until Claude has everything it needs to give the patient a final answer.
This is the multi-agent AI workflow pattern for healthcare: Claude doesn't just generate text, it actively orchestrates a sequence of actions to complete a real task. The loop keeps Claude in control of the reasoning while your Python functions handle the actual data operations.
Claude sometimes needs to call tools in sequence — check availability, then book, then remind. Each tool result becomes new context that informs the next decision. A single API call can't do that. The loop is what makes this an agent rather than just a fancy autocomplete.
Step 5: Test With Sample Patient Requests
Now let's wire up the entry point and run three realistic patient scenarios. This block goes at the bottom of your file.
scheduler.py (continued)def main():
agent = HealthcareSchedulingAgent()
# --- Test Case 1: Standard appointment booking with reminder ---
response1 = agent.run(
"Hi, I'm Maria Gonzalez. I'd like to book a checkup with Dr. Patel "
"on 2026-06-15. My email is maria.g@example.com. Please send me a reminder."
)
print(f"\nFINAL RESPONSE:\n{response1}")
# Reset history between patients
agent.conversation_history = []
# --- Test Case 2: Check availability before deciding ---
response2 = agent.run(
"This is James Whitfield. Is Dr. Smith available on June 16, 2026? "
"If so, book me for a follow-up and text a reminder to 239-555-0187."
)
print(f"\nFINAL RESPONSE:\n{response2}")
# Reset history between patients
agent.conversation_history = []
# --- Test Case 3: Urgent care with specific time preference ---
response3 = agent.run(
"I'm Sofia Reyes and I need an urgent care slot with Dr. Nguyen "
"on 2026-06-17 in the afternoon. Book the earliest available slot "
"and email a confirmation to sofia.r@example.com."
)
print(f"\nFINAL RESPONSE:\n{response3}")
if __name__ == "__main__":
main()Run the script with python scheduler.py and you'll see output like this:
============================================================
PATIENT REQUEST: Hi, I'm Maria Gonzalez. I'd like to book a checkup with Dr. Patel
on 2026-06-15. My email is maria.g@example.com. Please send me a reminder.
============================================================
[Claude stop reason: tool_use]
[Tool call: check_availability]
Input: {
"provider_name": "Dr. Patel",
"date": "2026-06-15"
}
Result: {
"provider": "Dr. Patel",
"date": "2026-06-15",
"available_slots": ["09:00", "09:30", "10:00", "11:00", "14:00", "14:30", "15:00", "16:00"],
"total_open": 8
}
[Claude stop reason: tool_use]
[Tool call: create_appointment]
Input: {
"patient_name": "Maria Gonzalez",
"provider_name": "Dr. Patel",
"date": "2026-06-15",
"time_slot": "09:00",
"appointment_type": "checkup"
}
Result: {
"status": "confirmed",
"confirmation_id": "APT-20260612143022",
"patient": "Maria Gonzalez",
"provider": "Dr. Patel",
"date": "2026-06-15",
"time": "09:00",
"type": "checkup",
"message": "Appointment successfully booked. Confirmation: APT-20260612143022"
}
[Tool call: send_reminder]
Input: {
"patient_name": "Maria Gonzalez",
"contact_info": "maria.g@example.com",
"appointment_details": "Checkup with Dr. Patel on June 15, 2026 at 9:00 AM. Confirmation: APT-20260612143022",
"reminder_method": "email"
}
[REMINDER SENT via EMAIL]
To: Maria Gonzalez (maria.g@example.com)
Message: Checkup with Dr. Patel on June 15, 2026 at 9:00 AM. Confirmation: APT-20260612143022
[Claude stop reason: end_turn]
FINAL RESPONSE:
Great news, Maria! I've booked your checkup with Dr. Patel for Monday, June 15, 2026
at 9:00 AM. Your confirmation number is APT-20260612143022. A reminder has been sent
to maria.g@example.com. See you then!How It Works
Here's the plain-English version of what just happened. When Maria's request came in, Claude read the system prompt, the available tools, and her message. It decided the right first move was to check Dr. Patel's availability — so it returned a tool_use stop reason with the correct inputs filled in.
Our loop executed check_availability, got the open slots back, and fed those results to Claude as a new message. Claude then decided to book the 9:00 AM slot and simultaneously request a reminder — both in the same tool-use turn. After we executed both tools and returned the results, Claude had everything it needed to write the friendly confirmation message and returned end_turn.
The whole thing feels like magic, but mechanically it's just a while loop that passes context back and forth. Claude does the reasoning; Python does the executing. That clean separation is exactly what makes this pattern so powerful for a Claude API patient management system.
Common Errors and Fixes
Error 1: AuthenticationError on first run
anthropic.AuthenticationError: 401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}Fix: Your .env file either doesn't exist, is in the wrong directory, or has a typo in the key name. Make sure the file is in the same folder you're running the script from, the variable is named exactly ANTHROPIC_API_KEY, and there are no spaces around the = sign. Double-check the key value at console.anthropic.com.
Error 2: KeyError when executing a tool
TypeError: check_availability() got an unexpected keyword argument 'doctor_name'
Fix: This means Claude used a different parameter name than your function expects. Check that the "properties" keys in your tool schema exactly match your Python function's argument names. In our case, the schema uses provider_name and the function signature must also say provider_name — not doctor_name or anything else.