Skip to content
Learn Agentic AI14 min read0 views

Building a Calendar Management Agent: Scheduling, Rescheduling, and Conflict Resolution

Build an AI-powered calendar management agent that checks availability across time zones, resolves scheduling conflicts, and handles rescheduling workflows using the Google Calendar API.

The Complexity Behind Simple Scheduling

Scheduling a meeting between three people across two time zones sounds trivial until you account for existing commitments, buffer times between meetings, lunch blocks, focus time preferences, and the fact that one participant is in Tokyo while another is in New York. Calendar management agents handle this complexity by querying availability, proposing optimal slots, and resolving conflicts automatically.

In this guide, we build a calendar management agent that integrates with Google Calendar, checks availability across multiple attendees, handles timezone conversions, and uses an LLM to negotiate scheduling conflicts.

Authenticating with Google Calendar

The Google Calendar API uses OAuth2. For a service agent that manages calendars on behalf of users, a service account with domain-wide delegation is the cleanest approach:

from google.oauth2 import service_account
from googleapiclient.discovery import build
from datetime import datetime, timedelta
import pytz

SCOPES = ["https://www.googleapis.com/auth/calendar"]

def get_calendar_service(user_email: str):
    """Get a Calendar API service delegated to a specific user."""
    credentials = service_account.Credentials.from_service_account_file(
        "service-account.json",
        scopes=SCOPES,
    )
    delegated = credentials.with_subject(user_email)
    return build("calendar", "v3", credentials=delegated)

With domain-wide delegation, the agent can read and write calendars for any user in the organization without individual OAuth flows.

Checking Availability with FreeBusy

The FreeBusy API is the correct way to check availability. It returns busy blocks for multiple calendars in a single call, which is far more efficient than fetching all events:

from dataclasses import dataclass

@dataclass
class TimeSlot:
    start: datetime
    end: datetime

def get_busy_times(
    service, emails: list[str], start: datetime, end: datetime
) -> dict[str, list[TimeSlot]]:
    """Query free/busy information for multiple users."""
    body = {
        "timeMin": start.isoformat(),
        "timeMax": end.isoformat(),
        "items": [{"id": email} for email in emails],
    }
    result = service.freebusy().query(body=body).execute()

    busy = {}
    for email in emails:
        calendar_busy = result["calendars"].get(email, {}).get("busy", [])
        busy[email] = [
            TimeSlot(
                start=datetime.fromisoformat(b["start"]),
                end=datetime.fromisoformat(b["end"]),
            )
            for b in calendar_busy
        ]
    return busy

Finding Common Available Slots

With busy times for all attendees, the agent computes overlapping free windows. This interval-based approach merges all busy blocks and finds gaps:

See AI Voice Agents Handle Real Calls

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

def find_available_slots(
    busy_times: dict[str, list[TimeSlot]],
    search_start: datetime,
    search_end: datetime,
    duration_minutes: int = 30,
    working_hours: tuple[int, int] = (9, 17),
) -> list[TimeSlot]:
    """Find common available slots across all attendees."""
    # Merge all busy times into a single sorted list
    all_busy = []
    for blocks in busy_times.values():
        all_busy.extend(blocks)
    all_busy.sort(key=lambda s: s.start)

    # Merge overlapping busy blocks
    merged = []
    for block in all_busy:
        if merged and block.start <= merged[-1].end:
            merged[-1] = TimeSlot(merged[-1].start, max(merged[-1].end, block.end))
        else:
            merged.append(TimeSlot(block.start, block.end))

    # Find gaps that fit the requested duration
    available = []
    cursor = search_start
    min_duration = timedelta(minutes=duration_minutes)

    for block in merged:
        if block.start - cursor >= min_duration:
            # Check working hours constraint
            if cursor.hour >= working_hours[0] and block.start.hour <= working_hours[1]:
                available.append(TimeSlot(cursor, block.start))
        cursor = max(cursor, block.end)

    # Check gap after last busy block
    if search_end - cursor >= min_duration:
        available.append(TimeSlot(cursor, search_end))

    return available

Handling Timezone Conversions

Time zones are the most common source of scheduling bugs. The agent normalizes everything to UTC internally and converts to local time only for display:

def normalize_to_utc(dt: datetime, timezone_str: str) -> datetime:
    """Convert a local datetime to UTC."""
    local_tz = pytz.timezone(timezone_str)
    if dt.tzinfo is None:
        dt = local_tz.localize(dt)
    return dt.astimezone(pytz.utc)

def display_in_timezone(dt: datetime, timezone_str: str) -> str:
    """Format a UTC datetime for display in a local timezone."""
    local_tz = pytz.timezone(timezone_str)
    local_dt = dt.astimezone(local_tz)
    return local_dt.strftime("%A, %B %d at %I:%M %p %Z")

# Example: attendees in different zones
attendees = {
    "alice@company.com": "America/New_York",
    "bob@company.com": "Asia/Tokyo",
    "carol@company.com": "Europe/London",
}

Conflict Resolution with LLM Reasoning

When no common slot exists, the agent uses an LLM to propose the best compromise. It considers factors like who has the most flexible schedule, meeting priority, and time zone fairness:

from openai import OpenAI

client = OpenAI()

def resolve_conflict(
    attendees: dict[str, str],
    busy_times: dict[str, list[TimeSlot]],
    meeting_purpose: str,
) -> str:
    """Use LLM to suggest a conflict resolution strategy."""
    busy_summary = ""
    for email, blocks in busy_times.items():
        tz = attendees[email]
        times = [
            f"  {display_in_timezone(b.start, tz)} - {display_in_timezone(b.end, tz)}"
            for b in blocks
        ]
        busy_summary += f"{email} ({tz}):\n" + "\n".join(times) + "\n\n"

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": (
                    "You are a scheduling assistant. When no common free slot exists, "
                    "propose the best compromise. Consider: meeting urgency, time zone "
                    "fairness (avoid repeatedly burdening the same timezone), and which "
                    "attendees are optional vs required."
                ),
            },
            {
                "role": "user",
                "content": (
                    f"Meeting purpose: {meeting_purpose}\n\n"
                    f"Attendee busy times:\n{busy_summary}\n"
                    "Suggest the best scheduling approach."
                ),
            },
        ],
    )
    return response.choices[0].message.content

Creating and Rescheduling Events

Once a slot is confirmed, the agent creates or updates the calendar event:

def create_event(
    service, summary: str, start: datetime, end: datetime,
    attendee_emails: list[str], description: str = "",
) -> str:
    """Create a calendar event with attendees."""
    event = {
        "summary": summary,
        "description": description,
        "start": {"dateTime": start.isoformat(), "timeZone": "UTC"},
        "end": {"dateTime": end.isoformat(), "timeZone": "UTC"},
        "attendees": [{"email": e} for e in attendee_emails],
        "reminders": {"useDefault": True},
    }
    result = service.events().insert(
        calendarId="primary", body=event, sendUpdates="all"
    ).execute()
    return result["id"]

FAQ

How do I handle recurring meetings that need rescheduling?

Use the recurringEventId field to identify event series. To reschedule a single occurrence, modify that instance only. To reschedule the entire series, update the parent event. The FreeBusy API automatically accounts for recurring events when checking availability.

What about buffer time between meetings?

Add a configurable buffer (typically 10-15 minutes) by extending each busy block's end time before computing available slots. This prevents back-to-back meetings and gives attendees transition time.

How do I respect "Do Not Disturb" or focus time blocks?

Query each user's calendar for events marked as "outOfOffice" or with specific keywords like "Focus Time." Treat these as busy blocks during availability computation. Google Calendar's working hours settings can also be fetched via the Settings API and applied as constraints.


#CalendarAutomation #AIAgents #GoogleCalendarAPI #Scheduling #WorkflowAutomation #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.