Skip to content
Learn Agentic AI9 min read0 views

Conversational AI for Appointment Scheduling: Building Booking Agents

Build a conversational booking agent that integrates with calendar APIs, handles timezone conversions, checks real-time availability, and manages confirmation and reminder flows.

Why Booking Agents Matter

Appointment scheduling is one of the highest-ROI applications of conversational AI. Every business that relies on meetings — from healthcare clinics to SaaS sales teams — loses revenue when prospects drop off during the booking process. A conversational booking agent eliminates that friction by handling the entire flow through natural language: understanding the request, checking availability, proposing times, handling timezone differences, and sending confirmations.

Architecture Overview

A booking agent needs four capabilities: natural language understanding to parse scheduling intent, a calendar integration layer for real-time availability, timezone logic to prevent mismatches, and a notification system for confirmations and reminders. We will wire these together using tool-calling with the OpenAI Agents SDK.

Calendar Integration Layer

The foundation is a clean abstraction over your calendar provider. This example uses Google Calendar, but the pattern applies to any provider with a REST API.

from datetime import datetime, timedelta
from dataclasses import dataclass
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build


@dataclass
class TimeSlot:
    start: datetime
    end: datetime
    available: bool = True


class CalendarService:
    def __init__(self, credentials: Credentials):
        self.service = build("calendar", "v3", credentials=credentials)

    def get_available_slots(
        self, calendar_id: str, date: str, duration_minutes: int = 30
    ) -> list[TimeSlot]:
        """Fetch free slots for a given date."""
        day_start = datetime.fromisoformat(f"{date}T09:00:00")
        day_end = datetime.fromisoformat(f"{date}T17:00:00")

        body = {
            "timeMin": day_start.isoformat() + "Z",
            "timeMax": day_end.isoformat() + "Z",
            "items": [{"id": calendar_id}],
        }
        result = self.service.freebusy().query(body=body).execute()
        busy_periods = result["calendars"][calendar_id]["busy"]

        # Build slots and mark busy ones
        slots = []
        current = day_start
        while current + timedelta(minutes=duration_minutes) <= day_end:
            slot_end = current + timedelta(minutes=duration_minutes)
            is_busy = any(
                datetime.fromisoformat(b["start"].replace("Z", ""))
                < slot_end
                and datetime.fromisoformat(b["end"].replace("Z", ""))
                > current
                for b in busy_periods
            )
            slots.append(TimeSlot(
                start=current, end=slot_end, available=not is_busy
            ))
            current = slot_end
        return [s for s in slots if s.available]

    def create_event(
        self, calendar_id: str, slot: TimeSlot, attendee_email: str,
        summary: str,
    ) -> str:
        event = {
            "summary": summary,
            "start": {"dateTime": slot.start.isoformat(), "timeZone": "UTC"},
            "end": {"dateTime": slot.end.isoformat(), "timeZone": "UTC"},
            "attendees": [{"email": attendee_email}],
        }
        result = self.service.events().insert(
            calendarId=calendar_id, body=event, sendUpdates="all"
        ).execute()
        return result["htmlLink"]

Timezone Handling

Timezone errors are the single most common failure mode in booking agents. Always store times in UTC internally and convert to the user's timezone only at the presentation layer.

from zoneinfo import ZoneInfo


def convert_slots_to_local(
    slots: list[TimeSlot], user_timezone: str
) -> list[dict]:
    tz = ZoneInfo(user_timezone)
    return [
        {
            "start": slot.start.replace(tzinfo=ZoneInfo("UTC"))
                .astimezone(tz)
                .strftime("%I:%M %p"),
            "end": slot.end.replace(tzinfo=ZoneInfo("UTC"))
                .astimezone(tz)
                .strftime("%I:%M %p"),
            "start_utc": slot.start.isoformat(),
        }
        for slot in slots
    ]


def detect_timezone_from_message(message: str) -> str | None:
    """Simple keyword detection for timezone hints."""
    tz_map = {
        "est": "America/New_York",
        "eastern": "America/New_York",
        "cst": "America/Chicago",
        "central": "America/Chicago",
        "pst": "America/Los_Angeles",
        "pacific": "America/Los_Angeles",
        "ist": "Asia/Kolkata",
        "gmt": "Europe/London",
        "utc": "UTC",
    }
    lower = message.lower()
    for keyword, tz in tz_map.items():
        if keyword in lower:
            return tz
    return None

Building the Agent with Tool Calls

The booking agent uses tools to check availability, propose times, and create events. The LLM handles the conversational flow while the tools handle the data operations.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

from agents import Agent, Runner, function_tool


@function_tool
def check_availability(date: str, duration_minutes: int = 30) -> str:
    """Check available time slots for a given date (YYYY-MM-DD)."""
    cal = CalendarService(get_credentials())
    slots = cal.get_available_slots("primary", date, duration_minutes)
    if not slots:
        return f"No available slots on {date}."
    local_slots = convert_slots_to_local(slots, "America/New_York")
    lines = [f"- {s['start']} to {s['end']}" for s in local_slots[:6]]
    return f"Available slots on {date}:\n" + "\n".join(lines)


@function_tool
def book_appointment(
    date: str, time_utc: str, attendee_email: str, purpose: str
) -> str:
    """Book an appointment at the specified UTC time."""
    cal = CalendarService(get_credentials())
    start = datetime.fromisoformat(time_utc)
    slot = TimeSlot(start=start, end=start + timedelta(minutes=30))
    link = cal.create_event("primary", slot, attendee_email, purpose)
    return f"Appointment booked. Calendar link: {link}"


booking_agent = Agent(
    name="BookingAgent",
    instructions="""You are a scheduling assistant. Help users book
    appointments by checking availability and confirming bookings.
    Always confirm the date, time, and timezone before booking.
    Ask for the attendee's email if not provided.""",
    tools=[check_availability, book_appointment],
)

Confirmation and Reminder Flow

After booking, the agent should send a confirmation message immediately and schedule reminders. A simple task queue handles the deferred sends.

import asyncio
from datetime import datetime, timedelta


async def send_confirmation(email: str, details: dict, notifier):
    message = (
        f"Your appointment is confirmed for "
        f"{details['date']} at {details['time']}.\n"
        f"Purpose: {details['purpose']}\n"
        f"Calendar link: {details['link']}"
    )
    await notifier.send_email(email, "Appointment Confirmed", message)


async def schedule_reminder(
    email: str, appointment_time: datetime, notifier
):
    reminder_time = appointment_time - timedelta(hours=1)
    delay = (reminder_time - datetime.utcnow()).total_seconds()
    if delay > 0:
        await asyncio.sleep(delay)
        await notifier.send_email(
            email,
            "Reminder: Appointment in 1 hour",
            f"Your appointment is in 1 hour at "
            f"{appointment_time.strftime('%I:%M %p UTC')}.",
        )

FAQ

How do I handle scheduling across multiple team members' calendars?

Query the freebusy endpoint for all team members simultaneously and compute the intersection of available slots. Present only times where at least one qualified team member is free, and assign the meeting to whichever available member best matches the prospect's needs.

What if the user provides an ambiguous date like "next Tuesday"?

Use a date parsing library like dateparser or python-dateutil to resolve relative dates. Always confirm the resolved date with the user before checking availability. For example, respond with "I understand you mean Tuesday, March 24th — is that correct?" before proceeding.

How do I prevent double-booking in high-concurrency scenarios?

Use optimistic locking. Before creating the event, re-check availability one final time. If the slot was taken between the user's selection and the booking attempt, inform them immediately and offer the next available slot. Google Calendar's API will also reject conflicting events if configured with the sendUpdates parameter.


#SchedulingAgent #CalendarIntegration #ConversationalAI #TimezoneHandling #Python #AgenticAI #LearnAI #AIEngineering

Share this article
C

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.