Every morning, before your first meeting, your AI reads today's calendar and emails you a prep brief: who you're meeting, what you should know going in, smart questions to ask, and what outcome to aim for. You read it over coffee. You walk in ready. The whole thing runs automatically. This guide shows you exactly how to build it — no complex integrations, no OAuth headaches, just a Python script and a cron job.
Every major calendar app — Google Calendar, Apple Calendar, Outlook — lets you export a private ICS feed URL. This is just a link that always points to your current calendar data. You can fetch it any time without logging in.
Our script fetches that URL each morning, parses today's meetings, and for each one sends the title, time, description, and attendee list to OpenAI. The AI reads all of that and writes you a prep note: what the meeting is probably about, what you should prepare, and what questions would make you look sharp.
Then it bundles all the prep notes into a single email and sends it to your inbox. You get one email, one read, and you're ready for the whole day.
python3 --version.That's it. No OAuth setup. No Google Cloud Console. No API keys beyond OpenAI.
Google Calendar: Open Google Calendar in your browser → Settings (gear icon) → Settings → find your calendar on the left sidebar under "Settings for my calendars" → click it → scroll to "Secret address in iCal format" → copy that URL. It looks like: https://calendar.google.com/calendar/ical/your-email%40gmail.com/private-XXXX/basic.ics
Apple iCloud: calendar.apple.com → click the share icon next to your calendar → check "Public Calendar" → copy the webcal:// URL (replace webcal:// with https://).
Outlook/Microsoft 365: Outlook web → Settings → View all Outlook settings → Calendar → Shared calendars → Publish a calendar → Copy the ICS link.
Go to myaccount.google.com/security → make sure 2-Step Verification is on → search for "App Passwords" → create one, name it "Meeting Briefing" → copy the 16-character password. This lets the script send email as you without storing your real password.
Open Terminal and run:pip3 install icalendar openai requests
That installs everything. Takes about 30 seconds.
Create a file called meeting_briefer.py anywhere you like — your home directory or a ~/scripts/ folder both work. Paste the full script from the next section.
Update the five config variables at the top of the script. Then run it manually once: python3 meeting_briefer.py. If today has meetings, you'll get an email within about 60 seconds. If it's a weekend or your calendar is empty, it exits cleanly.
Run crontab -e and add this line:0 7 * * 1-5 python3 /path/to/meeting_briefer.py
That runs it at 7:00 AM every weekday. Change 7 to any hour you like. Use the full path to the script file.
Copy this exactly. Update the five config values at the top. Everything else is ready to go.
#!/usr/bin/env python3
"""
Meeting Briefer — reads today's calendar and emails you a prep brief.
Runs in ~20-30 seconds. Costs about $0.01-0.03 per run.
"""
import smtplib
import ssl
import requests
from datetime import date, datetime, timezone
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from icalendar import Calendar
from openai import OpenAI
# ─── CONFIG (edit these 5 values) ──────────────────────────────────────────
ICS_URL = "YOUR_PRIVATE_ICS_URL_HERE"
OPENAI_API_KEY = "sk-..."
SMTP_EMAIL = "[email protected]" # Gmail address you send FROM
SMTP_PASSWORD = "xxxx xxxx xxxx xxxx" # App Password (not your real password)
TO_EMAIL = "[email protected]" # Where to send the briefing (can be same)
# ─── END CONFIG ─────────────────────────────────────────────────────────────
client = OpenAI(api_key=OPENAI_API_KEY)
def fetch_todays_meetings(ics_url: str) -> list[dict]:
"""Fetch ICS feed and return today's meetings sorted by start time."""
response = requests.get(ics_url, timeout=15)
response.raise_for_status()
cal = Calendar.from_ical(response.content)
today = date.today()
meetings = []
for component in cal.walk():
if component.name != "VEVENT":
continue
dtstart = component.get("DTSTART")
if not dtstart:
continue
# Handle both date-only and datetime events
start_val = dtstart.dt
if isinstance(start_val, datetime):
start_date = start_val.date()
else:
start_date = start_val
if start_date != today:
continue
# Extract time string
if isinstance(start_val, datetime):
# Normalize to local time display
time_str = start_val.strftime("%I:%M %p").lstrip("0")
else:
time_str = "All day"
# Extract attendees
attendees_raw = component.get("ATTENDEE", [])
if not isinstance(attendees_raw, list):
attendees_raw = [attendees_raw]
attendees = []
for a in attendees_raw:
cn = a.params.get("CN", "")
if cn and cn.lower() not in (SMTP_EMAIL.lower(), "you"):
attendees.append(cn)
meetings.append({
"title": str(component.get("SUMMARY", "Untitled Meeting")),
"time": time_str,
"description": str(component.get("DESCRIPTION", "")).strip()[:800],
"location": str(component.get("LOCATION", "")).strip(),
"attendees": attendees[:8], # cap at 8 to keep prompt sane
})
meetings.sort(key=lambda m: m["time"])
return meetings
def generate_prep_brief(meeting: dict) -> str:
"""Ask OpenAI to write a prep brief for one meeting."""
attendee_str = ", ".join(meeting["attendees"]) if meeting["attendees"] else "not listed"
prompt = f"""You are a sharp executive assistant. Prepare a concise meeting brief.
Meeting: {meeting['title']}
Time: {meeting['time']}
Attendees: {attendee_str}
Location/Link: {meeting['location'] or 'not specified'}
Description: {meeting['description'] or 'none provided'}
Write a brief with these four sections. Be specific and useful, not generic.
**What This Meeting Is Probably About**
One or two sentences based on the title, description, and attendees.
**What to Prepare**
2–3 concrete things to have ready or think about beforehand.
**Smart Questions to Ask**
3 questions that would make you look engaged and sharp in this meeting.
**Desired Outcome**
One sentence: what a successful outcome looks like.
Keep the whole thing under 200 words. Plain English only."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.4,
max_tokens=350,
)
return response.choices[0].message.content.strip()
def build_email_body(meetings: list[dict], briefs: list[str]) -> str:
"""Build plain-text email body."""
today_str = date.today().strftime("%A, %B %-d")
lines = [
f"Your meeting briefing for {today_str}",
f"{len(meetings)} meeting{'s' if len(meetings) != 1 else ''} today",
"",
"=" * 60,
]
for meeting, brief in zip(meetings, briefs):
lines += [
"",
f"⏰ {meeting['time']} — {meeting['title']}",
"-" * 40,
brief,
"",
"=" * 60,
]
lines += [
"",
"— Your AI Meeting Briefer",
" Powered by Ask Patrick (askpatrick.co)",
]
return "\n".join(lines)
def send_email(subject: str, body: str) -> None:
"""Send plain-text email via Gmail SMTP."""
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = SMTP_EMAIL
msg["To"] = TO_EMAIL
msg.attach(MIMEText(body, "plain"))
context = ssl.create_default_context()
with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
server.login(SMTP_EMAIL, SMTP_PASSWORD)
server.sendmail(SMTP_EMAIL, TO_EMAIL, msg.as_string())
def main():
print("Fetching today's calendar...")
meetings = fetch_todays_meetings(ICS_URL)
if not meetings:
print("No meetings today. Nothing to send.")
return
print(f"Found {len(meetings)} meeting(s). Generating briefs...")
briefs = []
for m in meetings:
print(f" → Briefing: {m['title']} at {m['time']}")
brief = generate_prep_brief(m)
briefs.append(brief)
today_str = date.today().strftime("%A %-d %b")
subject = f"📋 Meeting Briefing — {today_str} ({len(meetings)} meeting{'s' if len(meetings) != 1 else ''})"
body = build_email_body(meetings, briefs)
print("Sending email...")
send_email(subject, body)
print("Done. Briefing sent.")
if __name__ == "__main__":
main()
On a Mac, cron often can't find Python unless you use the full path. Find yours by running which python3 in Terminal. Then use that full path in your crontab line, e.g. /usr/local/bin/python3 instead of just python3.
Here's what a real briefing email looks like for a day with two meetings. The meeting titles and descriptions are the only thing the AI has to work with — this is what it generates:
Your meeting briefing for Monday, March 4
2 meetings today
════════════════════════════════
⏰ 9:00 AM — Quarterly review with Marcus Chen
What This Meeting Is Probably About
A recurring business review with Marcus — likely covering Q4 results and Q1 targets. Given it's early March, expect him to push on what changed from the last review.
What to Prepare
• Pull up the three metrics you committed to last quarter — be ready to explain each one specifically
• Have one concrete Q1 initiative you can describe in two sentences
• Know your current blockers so you don't get caught off guard
Smart Questions to Ask
1. "What's the one thing you most want to see progress on this quarter?"
2. "Are there any resourcing decisions you're working through that would affect our roadmap?"
3. "How are you thinking about the priority tradeoff between X and Y?"
Desired Outcome
Marcus leaves with confidence in your execution and clarity on Q1 priorities.
════════════════════════════════
⏰ 2:30 PM — Intro call — Priya Sharma, Acme Corp
What This Meeting Is Probably About
A first conversation with a prospect or new contact. The goal is mutual qualification — figuring out if there's a fit worth exploring further.
What to Prepare
• Have a 60-second explanation of what you do and who it's for
• Think about what problem Acme Corp most likely has that you could solve
• Decide in advance what a "successful intro" looks like (a follow-up? a demo? a proposal?)
Smart Questions to Ask
1. "What prompted you to take this call right now?"
2. "What does the team currently use for [your area], and what's frustrating about it?"
3. "Who else would be involved in a decision like this?"
Desired Outcome
A clear next step agreed on by both sides before the call ends.
Two meetings, two tailored briefs, one email. The whole run took 28 seconds and cost $0.02.
The script does nothing until you schedule it. Here's the exact cron setup:
# Open your crontab
crontab -e
# Add this line (runs at 7:00 AM, Monday through Friday):
0 7 * * 1-5 /usr/local/bin/python3 /Users/yourname/scripts/meeting_briefer.py >> /tmp/meeting_briefer.log 2>&1
The >> /tmp/meeting_briefer.log 2>&1 part logs any errors to a file so you can debug if something goes wrong. Check that file if you stop getting emails.
If you want the briefing at a different time — say 6:30 AM — change 0 7 to 30 6. If you want weekends too, change 1-5 to *.
If your meetings don't always start early, you can modify the script to only brief you on the next upcoming meeting instead of all of today's. Add a filter that only returns meetings starting in the next 30–90 minutes, then run the cron every 30 minutes with */30 8-17 * * 1-5. You'll get a targeted brief right before each call.
The four-section brief format (What It's About / What to Prepare / Questions / Outcome) is what works best in practice, but you can change it. Find the prompt variable in generate_prep_brief() and edit the instructions.
Some variations that work well:
The model is gpt-4o-mini by default. If your meetings are complex or politically sensitive and you want richer briefs, switch to gpt-4o. It costs about 10x more — roughly $0.20–0.30 per day — but the quality difference for nuanced context is real.
Don't. Use a Gmail App Password. Real passwords are blocked by Google for third-party SMTP access anyway. If you get an "authentication failed" error, it's almost always this.
If you ever revoke and regenerate your Google Calendar secret link (some people do this for security), the old URL stops working. Keep your current URL saved somewhere safe. If emails stop arriving, this is the first thing to check.
The AI can only work with what you give it. A meeting titled "Call" with no description will produce a very generic brief. Fix this by adding even one sentence to your calendar events: "Discussing Q2 budget with finance team" is enough for the AI to produce something specific.
If your calendar stores events in UTC and your machine runs in a different timezone, meetings may show up on the wrong day. The script uses Python's date.today() which respects your system timezone. Make sure your system timezone is set correctly with date in Terminal.
| Usage | Meetings/Day | Cost Per Run | Monthly Cost |
|---|---|---|---|
| Light schedule | 1–2 meetings | ~$0.01 | ~$0.22 |
| Typical schedule | 3–5 meetings | ~$0.03 | ~$0.66 |
| Heavy meeting day | 6–10 meetings | ~$0.06 | ~$1.32 |
| Upgrade to gpt-4o | 3–5 meetings | ~$0.25 | ~$5.50 |
| Typical user (gpt-4o-mini) | — | — | Under $1/mo |
At typical usage this costs less per month than a single coffee. The calendar fetch is free. The email sending via Gmail is free.
Once it's running, spend 5 minutes each Friday reviewing the briefs it sent that week. Ask yourself:
The biggest lever is the quality of your calendar data. Meetings that take 10 seconds to describe well will produce briefs that are genuinely useful. Meetings titled "Chat" with no attendees will produce briefs that are guesses. Both improve over time as you build the habit of annotating your invites.
Library members get the full prompt variants, the per-meeting HTML email version, and the Slack-delivery alternative (posts to a private channel instead of email).
Join the Library — $9/mo →80+ guides, all updated within the last 7 days. Cancel any time.