← Back to Blog

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
  • anthropic and python-dotenv packages (we'll install these in Step 1)
  • A terminal and a code editor — that's genuinely all you need
📋 Full Source Code Note
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.

terminal
mkdir healthcare-scheduler
cd healthcare-scheduler
pip install anthropic python-dotenv

Create a .env file in that folder with your key:

.env
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.py
import 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"
⚠️ Don't skip the .env file. Hardcoding your API key directly in the script is the fastest way to accidentally expose it if you ever push to GitHub. The 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.

scheduler.py (continued)
# 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.

💡 Why a loop and not a single call?
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:

sample output
============================================================
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.

Error 3: Infinite loop — agent never reaches