← Back to Blog

What You'll Build

If you've been searching for a real how to build AI agents tutorial that goes beyond toy examples, this is it. You're going to build a production-ready multi-agent system in Python that schedules healthcare appointments, checks provider availability, and fires off confirmation messages — all coordinated by Claude API.

By the end, you'll have three specialized agents (Scheduler, Availability Checker, and Confirmation Agent) that hand off work to each other through an orchestration loop. This is the same pattern we use at Naples AI when building healthcare automation for Southwest Florida clinics.

📋 Full Source Code Note: The complete, working code is broken into steps below so you understand what each piece does. Every snippet builds on the last — by Step 5 you'll have the entire system running. Copy each block in order and you'll have a working multi-agent scheduler.

Prerequisites

  • Python 3.10 or higher installed
  • An Anthropic API key (get one here)
  • Basic familiarity with Python classes and functions
  • anthropic SDK installed: pip install anthropic
  • python-dotenv for environment variables: pip install python-dotenv
  • No healthcare database required — we simulate one in-memory for this tutorial

Step 1: Set Up Your Claude API Client and Environment

First, let's get the environment wired up. Create a .env file in your project root and add your API key. Never hardcode credentials — you'll regret it the first time you push to GitHub.

.env
ANTHROPIC_API_KEY=your_api_key_here

Now create your main project file and set up the base client. This is the foundation every agent in the system will share.

healthcare_scheduler.py
import os
import json
import datetime
from dotenv import load_dotenv
import anthropic

load_dotenv()

# Initialize the shared Anthropic client once — all agents use this instance
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-5"

# In-memory simulated database — replace with your real DB in production
PROVIDER_SCHEDULES = {
    "dr_martinez": {
        "name": "Dr. Elena Martinez",
        "specialty": "Family Medicine",
        "available_slots": [
            "2026-08-10 09:00", "2026-08-10 10:00", "2026-08-10 14:00",
            "2026-08-11 11:00", "2026-08-11 15:00", "2026-08-12 09:30"
        ]
    },
    "dr_chen": {
        "name": "Dr. Kevin Chen",
        "specialty": "Cardiology",
        "available_slots": [
            "2026-08-10 13:00", "2026-08-11 09:00", "2026-08-12 14:00"
        ]
    }
}

APPOINTMENTS = {}  # Stores confirmed bookings keyed by appointment_id

PATIENT_RECORDS = {
    "P001": {"name": "Maria Santos", "email": "[email protected]", "phone": "239-555-0142"},
    "P002": {"name": "James Kowalski", "email": "[email protected]", "phone": "239-555-0198"}
}

print("✅ Environment loaded. Claude client ready.")
print(f"   Model: {MODEL}")
print(f"   Providers loaded: {len(PROVIDER_SCHEDULES)}")
print(f"   Patients loaded: {len(PATIENT_RECORDS)}")

Run this file with python healthcare_scheduler.py and you should see the confirmation output. If you get an AuthenticationError, double-check your .env file is in the same directory as your script.

Step 2: Define Your Agent Roles

Each agent gets its own system prompt that locks it into a specific role. This is what makes multi-agent systems so powerful — you're not asking one model to do everything, you're routing tasks to specialists. Think of it like a real medical office with a receptionist, a scheduler, and a coordinator.

Add these role definitions to your file. The system prompts are intentionally tight and focused — a confused agent produces confused results.

healthcare_scheduler.py (continued)
AGENT_ROLES = {
    "availability_checker": {
        "name": "Availability Checker",
        "system_prompt": (
            "You are a medical scheduling availability agent. Your only job is to check "
            "whether a specific provider has open appointment slots on a requested date or date range. "
            "Always use the check_availability tool before responding. "
            "Return results in a clear, structured format listing available times. "
            "Never book appointments — only report availability."
        )
    },
    "scheduler": {
        "name": "Appointment Scheduler",
        "system_prompt": (
            "You are a medical appointment scheduling agent. Your job is to book appointments "
            "for patients with the correct provider at the correct time. "
            "Always verify the slot is available using check_availability before calling schedule_appointment. "
            "Confirm the patient ID exists before booking. "
            "If a slot is unavailable, suggest the next available time instead of failing silently."
        )
    },
    "confirmation_agent": {
        "name": "Confirmation Agent",
        "system_prompt": (
            "You are a patient communication agent responsible for sending appointment confirmations. "
            "After a successful booking, use the send_confirmation tool to notify the patient. "
            "Include the provider name, appointment date and time, and any preparation instructions "
            "relevant to the specialty. Keep messages friendly and clear."
        )
    }
}

print("\n✅ Agent roles defined:")
for role_key, role_data in AGENT_ROLES.items():
    print(f"   - {role_data['name']}")

Step 3: Create Tool Definitions for Calendar and Patient Database Access

Tools are how Claude agents interact with your real systems. Each tool definition tells Claude exactly what parameters it needs, what types they are, and what the tool does. Getting this right is the difference between an agent that works and one that hallucinates arguments.

Here are the three core tools — check_availability, schedule_appointment, and send_confirmation — plus the Python functions that actually execute them when Claude calls them.

healthcare_scheduler.py (continued)
TOOL_DEFINITIONS = [
    {
        "name": "check_availability",
        "description": (
            "Check available appointment slots for a specific healthcare provider. "
            "Returns a list of open time slots or an empty list if none are available."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "provider_id": {
                    "type": "string",
                    "description": "The provider's system ID, e.g. 'dr_martinez' or 'dr_chen'"
                },
                "date_filter": {
                    "type": "string",
                    "description": "Optional date string in YYYY-MM-DD format to filter results. Leave empty for all slots."
                }
            },
            "required": ["provider_id"]
        }
    },
    {
        "name": "schedule_appointment",
        "description": (
            "Book an appointment slot for a patient with a provider. "
            "The slot must be currently available. Returns a confirmation ID on success."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "provider_id": {
                    "type": "string",
                    "description": "The provider's system ID"
                },
                "patient_id": {
                    "type": "string",
                    "description": "The patient's system ID, e.g. 'P001'"
                },
                "appointment_slot": {
                    "type": "string",
                    "description": "The exact slot string from check_availability, e.g. '2026-08-10 09:00'"
                }
            },
            "required": ["provider_id", "patient_id", "appointment_slot"]
        }
    },
    {
        "name": "send_confirmation",
        "description": (
            "Send an appointment confirmation to a patient via email and SMS. "
            "Requires a valid appointment ID returned from schedule_appointment."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "appointment_id": {
                    "type": "string",
                    "description": "The confirmation ID returned from schedule_appointment"
                },
                "preparation_notes": {
                    "type": "string",
                    "description": "Any special preparation instructions for the patient"
                }
            },
            "required": ["appointment_id"]
        }
    }
]


def execute_tool(tool_name: str, tool_input: dict) -> dict:
    """Routes tool calls from Claude to the correct Python function."""
    if tool_name == "check_availability":
        return tool_check_availability(tool_input)
    elif tool_name == "schedule_appointment":
        return tool_schedule_appointment(tool_input)
    elif tool_name == "send_confirmation":
        return tool_send_confirmation(tool_input)
    else:
        return {"error": f"Unknown tool: {tool_name}"}


def tool_check_availability(params: dict) -> dict:
    provider_id = params.get("provider_id")
    date_filter = params.get("date_filter", "")

    if provider_id not in PROVIDER_SCHEDULES:
        return {"error": f"Provider '{provider_id}' not found in system."}

    provider = PROVIDER_SCHEDULES[provider_id]
    slots = provider["available_slots"]

    # Filter by date if provided
    if date_filter:
        slots = [s for s in slots if s.startswith(date_filter)]

    return {
        "provider_name": provider["name"],
        "specialty": provider["specialty"],
        "available_slots": slots,
        "total_available": len(slots)
    }


def tool_schedule_appointment(params: dict) -> dict:
    provider_id = params.get("provider_id")
    patient_id = params.get("patient_id")
    slot = params.get("appointment_slot")

    if provider_id not in PROVIDER_SCHEDULES:
        return {"error": f"Provider '{provider_id}' not found."}

    if patient_id not in PATIENT_RECORDS:
        return {"error": f"Patient '{patient_id}' not found."}

    provider = PROVIDER_SCHEDULES[provider_id]

    if slot not in provider["available_slots"]:
        return {"error": f"Slot '{slot}' is no longer available. Please check availability again."}

    # Remove booked slot from availability
    provider["available_slots"].remove(slot)

    # Generate a confirmation ID and store the appointment
    appt_id = f"APT-{len(APPOINTMENTS) + 1001}"
    APPOINTMENTS[appt_id] = {
        "appointment_id": appt_id,
        "patient_id": patient_id,
        "patient_name": PATIENT_RECORDS[patient_id]["name"],
        "provider_id": provider_id,
        "provider_name": provider["name"],
        "specialty": provider["specialty"],
        "slot": slot,
        "booked_at": datetime.datetime.now().isoformat(),
        "confirmation_sent": False
    }

    return {
        "success": True,
        "appointment_id": appt_id,
        "patient_name": PATIENT_RECORDS[patient_id]["name"],
        "provider_name": provider["name"],
        "slot": slot
    }


def tool_send_confirmation(params: dict) -> dict:
    appt_id = params.get("appointment_id")
    prep_notes = params.get("preparation_notes", "No special preparation required.")

    if appt_id not in APPOINTMENTS:
        return {"error": f"Appointment ID '{appt_id}' not found."}

    appt = APPOINTMENTS[appt_id]
    patient = PATIENT_RECORDS[appt["patient_id"]]

    # In production, this would call your email/SMS API
    confirmation_message = (
        f"Dear {appt['patient_name']}, your appointment with {appt['provider_name']} "
        f"({appt['specialty']}) is confirmed for {appt['slot']}. "
        f"Preparation notes: {prep_notes}"
    )

    APPOINTMENTS[appt_id]["confirmation_sent"] = True

    return {
        "success": True,
        "appointment_id": appt_id,
        "email_sent_to": patient["email"],
        "sms_sent_to": patient["phone"],
        "confirmation_message": confirmation_message
    }


print("\n✅ Tool definitions and handlers ready.")
💡 Pro Tip: The input_schema descriptions aren't just documentation — Claude reads them to decide what values to pass. The more specific your descriptions, the more accurately Claude will call your tools. Vague descriptions lead to wrong arguments.

Step 4: Build the Orchestration Loop for Multi-Agent Coordination

This is the core of the whole system. The orchestrator decides which agent handles each task, runs that agent's tool loop until it's done, then passes the result to the next agent. It's a clean handoff pattern — each agent gets a focused job and returns a structured result.

The run_agent method handles the agentic loop: it sends a message, checks if Claude wants to call a tool, executes that tool, feeds the result back, and keeps going until Claude returns a final text response. This loop is what makes it truly agentic.

healthcare_scheduler.py (continued)
class HealthcareSchedulingOrchestrator:
    """
    Coordinates multiple Claude agents to handle the full appointment scheduling workflow.
    Each agent is isolated with its own role and only sees the context it needs.
    """

    def __init__(self):
        self.client = client
        self.model = MODEL
        self.tools = TOOL_DEFINITIONS
        self.conversation_log = []

    def run_agent(self, role_key: str, user_message: str) -> str:
        """
        Runs a single agent through its full agentic tool-use loop.
        Returns the agent's final text response when it stops calling tools.
        """
        role = AGENT_ROLES[role_key]
        messages = [{"role": "user", "content": user_message}]

        print(f"\n{'='*55}")
        print(f"  AGENT: {role['name']}")
        print(f"  TASK:  {user_message[:80]}...")
        print(f"{'='*55}")

        # Agentic loop — keeps running until Claude stops requesting tool calls
        while True:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=1024,
                system=role["system_prompt"],
                tools=self.tools,
                messages=messages
            )

            # If Claude is done, return its final text response
            if response.stop_reason == "end_turn":
                final_text = ""
                for block in response.content:
                    if hasattr(block, "text"):
                        final_text = block.text
                print(f"\n  ✅ Agent completed. Response: {final_text[:120]}...")
                return final_text

            # Claude wants to call a tool — process all tool_use blocks
            if response.stop_reason == "tool_use":
                # Add Claude's response to the message history
                messages.append({"role": "assistant", "content": response.content})

                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)}")

                        # Execute the tool and get the result
                        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)
                        })

                # Feed tool results back to Claude
                messages.append({"role": "user", "content": tool_results})

    def schedule_appointment_workflow(
        self,
        patient_id: str,
        provider_id: str,
        preferred_date: str,
        reason: str
    ) -> dict:
        """
        Full orchestrated workflow: check availability → book → confirm.
        Returns a summary dict with the final appointment details.
        """
        print(f"\n{'#'*55}")
        print(f"  ORCHESTRATOR: Starting scheduling workflow")
        print(f"  Patient: {patient_id} | Provider: {provider_id}")
        print(f"  Date: {preferred_date} | Reason: {reason}")
        print(f"{'#'*55}")

        # --- AGENT 1: Check Availability ---
        availability_result = self.run_agent(
            role_key="availability_checker",
            user_message=(
                f"Check available appointment slots for provider '{provider_id}' "
                f"on or around {preferred_date}. Return all available times."
            )
        )

        # --- AGENT 2: Schedule the Appointment ---
        scheduling_result = self.run_agent(
            role_key="scheduler",
            user_message=(
                f"Book an appointment for patient '{patient_id}' with provider '{provider_id}'. "
                f"Availability check result: {availability_result}. "
                f"Preferred date: {preferred_date}. Appointment reason: {reason}. "
                f"Pick the earliest available slot on or after the preferred date."
            )
        )

        # Extract appointment ID from APPOINTMENTS dict (most recently added)
        appt_id = None
        for aid, appt in APPOINTMENTS.items():
            if appt["patient_id"] == patient_id and not appt["confirmation_sent"]:
                appt_id = aid

        if not appt_id:
            return {
                "success": False,
                "error": "Scheduling agent did not create an appointment.",
                "details": scheduling_result
            }

        # --- AGENT 3: Send Confirmation ---
        specialty = APPOINTMENTS[appt_id]["specialty"]
        prep_notes = self._get_prep_notes(specialty, reason)

        confirmation_result = self.run_agent(
            role_key="confirmation_agent",
            user_message=(
                f"Send a confirmation for appointment ID '{appt_id}'. "
                f"Include these preparation notes: {prep_notes}. "
                f"The patient's appointment reason is: {reason}."
            )
        )

        # Build final summary
        appt = APPOINTMENTS[appt_id]
        return {
            "success": True,
            "appointment_id": appt_id,
            "patient_name": appt["patient_name"],
            "provider_name": appt["provider_name"],
            "specialty": appt["specialty"],
            "scheduled_slot": appt["slot"],
            "confirmation_sent": appt["confirmation_sent"],
            "scheduling_notes": scheduling_result,
            "confirmation_notes": confirmation_result
        }

    def _get_prep_notes(self, specialty: str, reason: str) -> str:
        """Returns basic preparation instructions based on medical specialty."""
        notes_map = {
            "Family Medicine": "Please arrive 15 minutes early with your insurance card and a photo ID.",
            "Cardiology": (
                "Please avoid caffeine for 24 hours before your appointment. "
                "Bring a list of all current medications."
            )
        }
        return notes_map.get(specialty, "Please arrive 10 minutes early.")

Step 5: Implement Error Handling and Fallback Logic

Real healthcare systems fail in interesting ways — slots disappear between check and book, patient IDs come in with wrong formats, network calls time out. You need graceful fallbacks, not stack traces. Here's the runner function with full error handling wrapped around the orchestrator.

healthcare_scheduler.py (continued)
def run_scheduling_demo():
    """
    Entry point that demonstrates the full multi-agent workflow
    with proper error handling and a clean summary output.
    """
    orchestrator = HealthcareSchedulingOrchestrator()

    # Test Case 1: Successful scheduling
    print("\n" + "🏥 " * 20)
    print("TEST CASE 1: Standard appointment scheduling")
    print("🏥 " * 20)

    try:
        result = orchestrator.schedule_appointment_workflow(
            patient_id="P001",
            provider_id="dr_martinez",
            preferred_date="2026-08-10",
            reason="Annual physical exam and blood pressure follow-up"
        )

        if result["success"]:
            print("\n" + "=" * 55)
            print("  ✅ APPOINTMENT SCHEDULED SUCCESSFULLY")
            print("=" * 55)
            print(f"  Appointment ID:  {result['appointment_id']}")
            print(f"  Patient:         {result['patient_name']}")
            print(f"  Provider:        {result['provider_name']} ({result['specialty']})")
            print(f"  Scheduled Time:  {result['scheduled_slot']}")