By Sagar Shankaran, Founder of CallSphere
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.
Key takeaways
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.
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.
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
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
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",
}
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
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"]
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.
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.
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

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
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.
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.
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.
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.
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.
Head-to-head: OpenAI Frontier and Anthropic's managed agent stack — strengths, fit, and what each means for enterprise AI voice and chat deployment.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI