events-calendar-bot
A weekly job that curates upcoming NYC events matching a fixed taste profile and writes them to a private Google Calendar named "Events", idempotently (re-runs never create duplicates).
It runs as a Claude Code scheduled routine (claude.ai/code/routines): the routine itself web-searches and writes to the calendar through the Google Calendar connector. There is no server, no Anthropic API key, and no Google service account — the only deployed artifact is the routine prompt.
Layout
| File | Role |
|---|---|
ROUTINE_PROMPT.md |
The paste-ready routine prompt. This is the deliverable — paste it into the routine. Generated from the template + reconcile.py. |
ROUTINE_PROMPT.template.md |
Editable source for the prompt (everything except the embedded script). |
reconcile.py |
Deterministic dedup + a CLI. Embedded verbatim into ROUTINE_PROMPT.md; the routine writes it to /tmp and runs it. Pure stdlib, zero deps. |
tests/test_reconcile.py |
Unit tests, built from real calendar data. |
How dedup works (the thing that must not break)
Each run lists what's already on the Events calendar and runs reconcile.py to
drop any candidate already present. Identity is (normalized title, start date)
with fuzzy title matching — it strips a trailing (tickets req'd) / (day-of option) tag and a trailing — Venue segment, then compares by token overlap —
so the same event reported with a slightly different title across runs still
dedups. It does not rely on a stored key: the claude.ai Calendar connector's
create_event can't write extendedProperties, and the existing hand-created
events have no key, so matching works purely from title + date.
Local test (no network, no calendar access)
python3 -m unittest discover -s tests -v
# or run the deduper against two JSON files (candidate list + a calendar export):
python3 reconcile.py candidates.json existing.json --explain
Regenerate the prompt after editing
If you change reconcile.py or ROUTINE_PROMPT.template.md, rebuild the prompt:
python3 - <<'PY'
import pathlib
tpl = pathlib.Path('ROUTINE_PROMPT.template.md').read_text()
src = pathlib.Path('reconcile.py').read_text().rstrip('\n')
pathlib.Path('ROUTINE_PROMPT.md').write_text(tpl.replace('<<<RECONCILE_PY>>>', src))
PY
Set up the routine (one time)
- Open claude.ai/code/routines → create a routine.
- Prompt: paste the full contents of
ROUTINE_PROMPT.md, then replace<YOUR_CALENDAR_ID>with your calendar's ID and edit the curation profile to your own interests and neighborhoods. - Connectors: include the Google Calendar connector (it's opt-in per routine — remove the others to limit tool access). No GitHub repo is needed.
- Schedule: weekly, Thursday 11:00 America/New_York (routines floor at 1-hour intervals; weekly is fine).
- Network: default/trusted is enough (web search + the connector).
- Dry run first: the prompt ships with
DRY_RUN = true. Use "Run now" and review the printed plan — this is also the end-to-end check that the calendar connector works inside a scheduled run. - Go live: edit the prompt's
DRY_RUNtofalse, save, and enable the schedule.
Target calendar
- Name: Events (secondary calendar)
- ID:
<YOUR_CALENDAR_ID>(your secondary calendar, e.g.c_xxxxxxxx@group.calendar.google.com) - Time zone:
America/New_York - Events are written non-blocking (
availability: AVAILABILITY_FREE) and tagged Graphite (colorId 8).