Calendar Event Agents: Pre-Meeting Prep, Post-Meeting Summaries, and Follow-Ups
Build an AI calendar agent that prepares meeting briefs, generates post-meeting summaries with action items, and sends automated follow-up emails using Google Calendar webhooks.
Why Calendar Events Drive Valuable Agent Workflows
Meetings consume 15-25% of the average knowledge worker's week, yet most people walk into meetings unprepared and walk out without clear action items. Calendar events are natural trigger points for AI agents because each event has a known start time, end time, attendee list, and often a description that signals the meeting's purpose.
A calendar event agent can deliver three high-value workflows: pre-meeting preparation (gathering context about attendees and topics 30 minutes before), post-meeting summarization (processing notes or transcripts after the meeting ends), and follow-up automation (sending action items and thank-you messages to attendees).
Calendar Webhook Setup
Google Calendar supports push notifications that alert your endpoint when events are created, updated, or deleted. Register a watch on the user's calendar to start receiving notifications.
import os
import httpx
from fastapi import FastAPI, Request, BackgroundTasks
from datetime import datetime, timedelta
from openai import AsyncOpenAI
app = FastAPI()
llm = AsyncOpenAI()
GOOGLE_CALENDAR_API = "https://www.googleapis.com/calendar/v3"
async def register_calendar_watch(calendar_id: str, access_token: str):
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{GOOGLE_CALENDAR_API}/calendars/{calendar_id}/events/watch",
headers={"Authorization": f"Bearer {access_token}"},
json={
"id": f"watch-{calendar_id}",
"type": "web_hook",
"address": "https://your-agent.com/calendar/webhook",
"expiration": int(
(datetime.utcnow() + timedelta(days=7)).timestamp() * 1000
),
},
)
return resp.json()
@app.post("/calendar/webhook")
async def calendar_webhook(request: Request, background_tasks: BackgroundTasks):
channel_id = request.headers.get("X-Goog-Channel-ID", "")
resource_state = request.headers.get("X-Goog-Resource-State", "")
if resource_state == "sync":
return {"status": "sync_acknowledged"}
background_tasks.add_task(handle_calendar_change, channel_id)
return {"status": "accepted"}
Google sends a lightweight notification that something changed, not the full event data. Your handler must fetch the updated events separately.
Fetching Changed Events
Use the sync token pattern to efficiently fetch only events that changed since your last check.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
sync_tokens: dict[str, str] = {}
async def handle_calendar_change(channel_id: str):
calendar_id = get_calendar_for_channel(channel_id)
access_token = await get_access_token(calendar_id)
params = {"singleEvents": True, "orderBy": "startTime"}
token = sync_tokens.get(calendar_id)
if token:
params["syncToken"] = token
else:
params["timeMin"] = datetime.utcnow().isoformat() + "Z"
params["timeMax"] = (
datetime.utcnow() + timedelta(days=7)
).isoformat() + "Z"
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{GOOGLE_CALENDAR_API}/calendars/{calendar_id}/events",
headers={"Authorization": f"Bearer {access_token}"},
params=params,
)
data = resp.json()
sync_tokens[calendar_id] = data.get("nextSyncToken", "")
for event in data.get("items", []):
await process_calendar_event(event, calendar_id)
Pre-Meeting Preparation Agent
Schedule a prep task that fires 30 minutes before each meeting. The agent gathers context about attendees and topics, then sends a brief.
from apscheduler.schedulers.asyncio import AsyncIOScheduler
scheduler = AsyncIOScheduler()
async def process_calendar_event(event: dict, calendar_id: str):
if event.get("status") == "cancelled":
scheduler.remove_job(f"prep-{event['id']}", jobstore="default")
return
start_str = event.get("start", {}).get("dateTime")
if not start_str:
return
start_time = datetime.fromisoformat(start_str)
prep_time = start_time - timedelta(minutes=30)
if prep_time > datetime.now(start_time.tzinfo):
scheduler.add_job(
generate_meeting_prep,
"date",
run_date=prep_time,
args=[event, calendar_id],
id=f"prep-{event['id']}",
replace_existing=True,
)
async def generate_meeting_prep(event: dict, calendar_id: str):
attendees = [a["email"] for a in event.get("attendees", [])]
attendee_context = await gather_attendee_context(attendees)
prompt = f"""Prepare a brief meeting prep document.
Meeting: {event.get('summary', 'No title')}
Time: {event['start']['dateTime']}
Description: {event.get('description', 'No description')}
Attendees: {', '.join(attendees)}
Attendee context:
{attendee_context}
Generate:
1. Meeting purpose (1-2 sentences based on title and description)
2. Key attendee info (role, recent interactions, relevant context)
3. Suggested talking points (3-5 bullet points)
4. Questions to prepare for"""
response = await llm.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
)
prep_doc = response.choices[0].message.content
owner_email = get_calendar_owner_email(calendar_id)
await send_email(
to=owner_email,
subject=f"Meeting Prep: {event.get('summary', 'Upcoming Meeting')}",
body=prep_doc,
)
Post-Meeting Summary Generation
After a meeting ends, process notes or transcripts to generate a structured summary with action items.
async def generate_post_meeting_summary(
event: dict, transcript: str | None = None, notes: str | None = None
):
content = transcript or notes or "No transcript or notes available"
prompt = f"""Generate a structured meeting summary.
Meeting: {event.get('summary', 'No title')}
Attendees: {[a['email'] for a in event.get('attendees', [])]}
Content: {content[:6000]}
Format the summary as:
## Key Decisions
- List each decision made
## Action Items
- [Owner] Description (Due: date if mentioned)
## Discussion Highlights
- Key points discussed
## Open Questions
- Unresolved items requiring follow-up"""
response = await llm.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
)
return response.choices[0].message.content
Automated Follow-Up Emails
Send personalized follow-ups to each attendee with their specific action items highlighted.
async def send_follow_ups(event: dict, summary: str):
action_items = extract_action_items(summary)
attendees = event.get("attendees", [])
for attendee in attendees:
email = attendee["email"]
their_items = [
item for item in action_items
if email in item.get("owner", "").lower()
]
prompt = f"""Write a brief follow-up email for {email} after this meeting.
Meeting: {event.get('summary')}
Full summary: {summary}
Their action items: {their_items}
Keep it under 150 words. Be professional and specific about their tasks."""
response = await llm.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
)
await send_email(
to=email,
subject=f"Follow-up: {event.get('summary', 'Meeting')}",
body=response.choices[0].message.content,
)
FAQ
How do I get meeting transcripts automatically?
Integrate with a transcription service like Fireflies.ai, Otter.ai, or Google Meet's built-in recording. These services provide webhook callbacks when transcripts are ready. Link the transcript to the calendar event using the event ID or time window matching.
How far in advance should the prep agent run?
Thirty minutes works well for most meetings. For important client calls or board meetings, extend this to 2-4 hours to allow time for manual review and additions. Make the lead time configurable per calendar or meeting type.
What if a meeting is rescheduled?
The calendar webhook fires on updates too. When the event start time changes, cancel the existing prep job and schedule a new one at the updated time. The replace_existing=True parameter in APScheduler handles this automatically.
#CalendarAutomation #AIAgents #MeetingProductivity #GoogleCalendar #FastAPI #AgenticAI #LearnAI #AIEngineering
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.