AGENTTEMPLATE
Gmail + Calendar Agent
This template builds a CrewAI crew that reads your Gmail, reads your Google Calendar, and creates calendar events for actionable emails.
Prerequisites
- Python 3.10+.
- A Google Cloud project with Gmail API and Calendar API enabled. Steps to set this up: a. Go to console.cloud.google.com → New Project → name it. b. APIs & Services → Library → enable "Gmail API" and "Google Calendar API." c. APIs & Services → OAuth consent screen → External, fill in the basics, add your email as a test user. d. APIs & Services → Credentials → Create Credentials → OAuth client ID → Desktop app → download the JSON file as `credentials.json`.
- `pip install crewai crewai-tools google-auth google-auth-oauthlib google-api-python-client`.
- An LLM endpoint. For frontier: set `OPENAI_API_KEY` (or `ANTHROPIC_API_KEY`). For local: have Ollama running on localhost:11434 with a model like `qwen2.5:14b`.
python
"""
Gmail + Calendar Agent
Reads recent unread email, identifies actionable items, drafts
calendar events, asks for user confirmation before creating them.
"""
import os
import pickle
from datetime import datetime, timedelta
from pathlib import Path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from crewai import Agent, Task, Crew, Process
from crewai_tools import tool
# ---------- Google auth ----------
SCOPES = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/calendar",
]
TOKEN_PATH = Path("token.pickle")
CREDS_PATH = Path("credentials.json")
def get_google_services():
"""Authenticate once, cache the token, return Gmail + Calendar
services."""
creds = None
if TOKEN_PATH.exists():
with open(TOKEN_PATH, "rb") as f:
creds = pickle.load(f)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
str(CREDS_PATH), SCOPES
)
creds = flow.run_local_server(port=0)
with open(TOKEN_PATH, "wb") as f:
pickle.dump(creds, f)
gmail = build("gmail", "v1", credentials=creds)
cal = build("calendar", "v3", credentials=creds)
return gmail, cal
# ---------- Tools ----------
@tool("read_recent_emails")
def read_recent_emails(max_results: int = 10) -> str:
"""Read the most recent unread emails from the inbox.
Returns a list with sender, subject, snippet, and date."""
gmail, _ = get_google_services()
results = gmail.users().messages().list(
userId="me", q="is:unread", maxResults=max_results
).execute()
messages = results.get("messages", [])
out = []
for m in messages:
msg = gmail.users().messages().get(
userId="me", id=m["id"], format="metadata",
metadataHeaders=["From", "Subject", "Date"],
).execute()
headers = {h["name"]: h["value"]
for h in msg["payload"]["headers"]}
out.append({
"from": headers.get("From", ""),
"subject": headers.get("Subject", ""),
"snippet": msg.get("snippet", ""),
"date": headers.get("Date", ""),
})
return str(out)
@tool("read_calendar_today")
def read_calendar_today() -> str:
"""Return today's calendar events."""
_, cal = get_google_services()
now = datetime.utcnow()
start = now.replace(hour=0, minute=0, second=0).isoformat() + "Z"
end = (now.replace(hour=23, minute=59, second=59)).isoformat() + "Z"
events = cal.events().list(
calendarId="primary", timeMin=start, timeMax=end,
singleEvents=True, orderBy="startTime",
).execute()
return str(events.get("items", []))
@tool("create_calendar_event")
def create_calendar_event(
summary: str, start_iso: str, end_iso: str, description: str = ""
) -> str:
"""Create a calendar event. Times in ISO 8601, e.g.,
'2026-04-28T14:00:00-04:00'."""
_, cal = get_google_services()
event = {
"summary": summary,
"description": description,
"start": {"dateTime": start_iso},
"end": {"dateTime": end_iso},
}
created = cal.events().insert(
calendarId="primary", body=event
).execute()
return f"Created event: {created.get('htmlLink')}"
# ---------- Agents ----------
triage_agent = Agent(
role="Email Triage Specialist",
goal=(
"Read recent unread emails and identify which ones contain "
"actionable items that should become calendar events."
),
backstory=(
"You are a meticulous executive assistant. You skim emails "
"quickly and recognize commitments, meeting requests, and "
"deadlines."
),
tools=[read_recent_emails],
verbose=True,
)
scheduler_agent = Agent(
role="Scheduler",
goal=(
"Take actionable items from triaged emails, check the "
"calendar for conflicts, and propose calendar events to the "
"user for approval."
),
backstory=(
"You are a careful scheduler who never double-books and "
"always proposes events with reasonable defaults (30-min "
"blocks unless the email implies otherwise)."
),
tools=[read_calendar_today, create_calendar_event],
verbose=True,
)
# ---------- Tasks ----------
triage_task = Task(
description=(
"Read the 10 most recent unread emails. For each, decide "
"whether it contains an actionable item (a meeting request, "
"a deadline, an event invitation, a task with a date). "
"Output a list of actionable items as JSON: "
"[{from, subject, action, suggested_date_or_time}, ...]."
),
agent=triage_agent,
expected_output=(
"A JSON list of actionable items. Empty list if no actionable "
"items found."
),
)
scheduling_task = Task(
description=(
"For each actionable item from the triage step, check today's "
"calendar for conflicts and propose a calendar event. "
"DO NOT create the event automatically. Instead, output a "
"list of proposed events as JSON: "
"[{summary, start_iso, end_iso, description, "
"conflicts_with}, ...]. The user will review and approve."
),
agent=scheduler_agent,
expected_output="JSON list of proposed events for user review.",
)
# ---------- Crew ----------
crew = Crew(
agents=[triage_agent, scheduler_agent],
tasks=[triage_task, scheduling_task],
process=Process.sequential,
verbose=True,
)
if __name__ == "__main__":
result = crew.kickoff()
print("\n\n=== PROPOSED EVENTS (for your review) ===")
print(result)
print("\nReview the proposed events. To create them, run "
"create_calendar_event with each one.")Notes for adapting this template:
- The agents currently propose events but do not create them
automatically. This is intentional. Read 6.4 on agent caution.
When you trust the crew, change
scheduling_taskto callcreate_calendar_eventdirectly. - To switch to a local model, set the LLM in CrewAI's config to
point at Ollama:
from crewai import LLM ollama_llm = LLM(model="ollama/qwen2.5:14b", base_url="http://localhost:11434") triage_agent = Agent(..., llm=ollama_llm) - Run on a schedule: wrap the script in a launchd plist (macOS) or systemd timer (Linux) that runs every 15-60 minutes.