Skip to content

Reference google calendar client

Built 2026-04-30 to fix the colorId-disappearing problem. n8n's calendar workflows were doing four things wrong (see superseded note), so we cut them out entirely and went direct to the Google Calendar v3 REST API.

Where things live

  • Client: forge/scripts/forge_google_calendar.py
  • OAuth secrets: ~/.forge-secrets/google-calendar.env (chmod 600)
  • GOOGLE_CALENDAR_CLIENT_ID, GOOGLE_CALENDAR_CLIENT_SECRET, GOOGLE_CALENDAR_REFRESH_TOKEN, GOOGLE_CALENDAR_SCOPE
  • Origin: extracted from n8n credential t5YQ9F5vvgEFd6qL (Google Calendar account - [email protected]) via docker exec n8n n8n export:credentials --decrypted
  • Scopes granted: auth/calendar + auth/calendar.events (full read/write all calendars)

API surface

from forge_google_calendar import GCal
gc = GCal(calendar_id="primary")  # or any calendar id
gc.create_event(summary, start, end, description=None, location=None,
                color_id=None, time_zone="America/Chicago", extra=None)
gc.list_events(time_min, time_max, max_results=250, q=None, single_events=True)
gc.get_event(event_id)
gc.update_event(event_id, summary=None, start=None, end=None, ...)  # PATCH
gc.delete_event(event_id)

start/end accept ISO strings (with T for timed, without for all-day) or pre-built {"dateTime": ..., "timeZone": ...} dicts. Access tokens auto-refresh; one in-memory cache per process.

Smoke test

forge_google_calendar.py self-test
# Creates a 30-min event with colorId=11, gets it, updates colorId to 9, deletes it.

Wired into

  • forge_telegram_inbox_brain.py, all 4 calendar tools (create/update/list/delete) plus tool_create_task auto-sync, tool_log_sleep, tool_log_time_block, tool_briefing, tool_weekly_review. Lazy-init via _gcal().
  • forge_time_daily_aggregator.py, classify() now reads colorId first (via _COLOR_TO_CATEGORY), falls back to summary-text patterns for events created in Google Calendar UI.

Category to colorId map

Single source of truth is _CATEGORY_COLOR in forge_telegram_inbox_brain.py. Keep _COLOR_TO_CATEGORY in forge_time_daily_aggregator.py synced as the inverse.

Category colorId Color name
Work / Forge 9 blueberry
Personal / Family 10 basil
Sleep 1 lavender
Errands 5 banana
Workout / Health 11 tomato
Entertainment 6 tangerine
Travel 7 peacock

Why this is more sustainable than n8n

  • No silent field-stripping (n8n was dropping colorId on create AND list)
  • update_event actually works (n8n was returning 500 for every payload shape)
  • delete uses standard REST DELETE /events/{id}, no eventId-vs-event_id param quirks
  • All Google Calendar fields available (attendees, recurrence, reminders, conferenceData)
  • One less hop (n8n → Google) so faster + more debuggable
  • Auto access-token refresh; refresh_token is long-lived

OAuth bootstrap notes (for future migrations)

If the refresh_token ever revokes, three options to re-acquire: - (a) Re-run the OAuth flow in n8n UI for the existing credential and re-extract via export:credentials - (b) Build a one-off forge_google_oauth_bootstrap.py with the same client_id/secret - (c) Create a new OAuth client in Google Cloud Console (project tied to [email protected])

Cleanup TODO

The three n8n workflows (create-calendar-event, list-calendar-events, update-calendar-event, delete-calendar-event) are now unused by forge code. They can be archived/deleted in the n8n UI when convenient. The OAuth credential t5YQ9F5vvgEFd6qL should stay because retiring it might revoke the refresh_token Justin's forge backend now uses.

[Claude Code, 2026-04-30]