Skip to content
Building a Calendar Management Agent: Scheduling, Rescheduling, and Conflict Resolution
Learn Agentic AI14 min read13 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:

flowchart LR
    INPUT(["User intent"])
    PARSE["Parse plus<br/>classify"]
    PLAN["Plan and tool<br/>selection"]
    AGENT["Agent loop<br/>LLM plus tools"]
    GUARD{"Guardrails<br/>and policy"}
    EXEC["Execute and<br/>verify result"]
    OBS[("Trace and metrics")]
    OUT(["Outcome plus<br/>next action"])
    INPUT --> PARSE --> PLAN --> AGENT --> GUARD
    GUARD -->|Pass| EXEC --> OUT
    GUARD -->|Fail| AGENT
    AGENT --> OBS
    style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
    style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
    style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
    style OUT fill:#059669,stroke:#047857,color:#fff
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.

Hear it before you finish reading

Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.

Try Live Demo →

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:

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:

Still reading? Stop comparing — try CallSphere live.

CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.

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

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.

Related Articles You May Like

AI Agents

Personal AI Assistant: How to Pick One for Business in 2026

A founder's guide to the personal AI assistant market: best AI assistant apps, business-grade options, and how CallSphere's voice agent fits in.

AI Agents

Free AI Agents in 2026: When Free Wins and When It Costs You

A founder's guide to free AI agents, low-code AI agent builders, and how to know when you should pay for a real platform like CallSphere.

Agentic AI

Graphiti: How Temporal Knowledge Graphs Give AI Voice Agents Persistent Memory (2026 Guide)

Graphiti is the open-source temporal knowledge graph for AI agents in 2026. Learn how bi-temporal memory beats vector RAG for voice agents and long-running LLMs.

AI Agents

Chatbot App vs ChatGPT: What's the Difference, and Which Do I Need?

Chatbot app vs ChatGPT in 2026: a founder's clear take on the difference, when to use which, and how a real AI chatbot app development works.

HVAC

Building an HVAC After-Hours Emergency Escalation System: A Complete Engineering Guide

How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.

Enterprise AI

OpenAI Frontier vs Anthropic Managed Agents: 2026 Comparison

Head-to-head: OpenAI Frontier and Anthropic's managed agent stack — strengths, fit, and what each means for enterprise AI voice and chat deployment.