---
title: "Calendar Event Agents: Pre-Meeting Prep, Post-Meeting Summaries, and Follow-Ups"
description: "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."
canonical: https://callsphere.ai/blog/calendar-event-agents-pre-meeting-prep-summaries-follow-ups
category: "Learn Agentic AI"
tags: ["Calendar Automation", "AI Agents", "Meeting Productivity", "Google Calendar", "FastAPI"]
author: "CallSphere Team"
published: 2026-03-17T00:00:00.000Z
updated: 2026-05-07T16:36:33.444Z
---

# 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.

```mermaid
flowchart LR
    CLIENT(["Client SDK"])
    GW["API Gateway
auth plus rate limit"]
    APP["FastAPI app
handlers and DI"]
    VAL["Pydantic validation"]
    SVC["Service layer
business logic"]
    DB[(Database)]
    QUEUE[(Background queue)]
    OBS[(Tracing)]
    CLIENT --> GW --> APP --> VAL --> SVC
    SVC --> DB
    SVC --> QUEUE
    SVC --> OBS
    SVC --> CLIENT
    style GW fill:#4f46e5,stroke:#4338ca,color:#fff
    style APP fill:#f59e0b,stroke:#d97706,color:#1f2937
    style DB fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
```

```python
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.

```python
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.

```python
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.

```python
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.

```python
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

---

Source: https://callsphere.ai/blog/calendar-event-agents-pre-meeting-prep-summaries-follow-ups
