Skip to content

Google Calendar Cleanup

Alaners' Google Calendars accumulate toil: events nobody attends, recurring meetings that outlive their purpose, and "forever" series with no end date. The calendar cleanup feature trims this automatically, without spamming attendees or forcing constant re-scheduling.

It applies three rules to each active alaner's primary calendar, acting only on events the alaner organizes (organizer.self). All summaries are posted to #calendar-lifecycle-cleanup. Two of the rules run inside the existing daily run_tasks sweep — no new cron.

Scope

Only AlanAccountType.alaner accounts that are is_active are touched. External users and service accounts are never in scope.

The three rules

Rule Mechanism What it does Window / cap
1 GoogleCalendarTerminateFullyDeclinedEvents (daily task) Deletes events where every human attendee has declined. Series are expanded (single_events=True), so only the declined instances are removed — never the whole series. [now - 7d, now + 30d]
3 GoogleCalendarCapOpenEndedRecurring (daily task) Caps recurring meetings that lack UNTIL/COUNT at created_at + 3 months (floored to now + 1d if that is already past). Idempotent — already-bounded series are skipped. [now - 7d, now + 30d]
2 expire_recurring_meetings --until=YYYY-MM-DD (one-off CLI) Caps every currently-recurring meeting at a chosen date. RQ-enqueued per alaner. Skips series already bounded on or before the target. user-chosen --until

Rule 1 — terminate fully-declined events

GoogleCalendarTerminateFullyDeclinedEvents gates in should_run (active alaner only) and acts in run. all_human_attendees_declined ignores resource attendees and the organizer; a solo event (organizer only) is never deleted. Deletion uses delete_calendar_event on the "primary" calendar.

Rule 3 — cap open-ended recurring meetings

GoogleCalendarCapOpenEndedRecurring skips non-recurring events and events whose RRULE already carries UNTIL or COUNT. It reads the event's created timestamp; events without one are skipped and counted (reported as a :warning: line). The new UNTIL is written with cap_rrule_until + patch_calendar_event_recurrence.

Rule 2 — force-cap all recurring meetings (manual)

A one-off Click command for a deliberate, org-wide cap at a chosen date (e.g. end of a season). It only touches series whose UNTIL is missing or strictly later than --until; earlier bounds are left alone.

How calendars are accessed

Tasks and the CLI resolve a client with get_calendar_service(impersonated_as=<alaner email>) — a Google service account using domain-wide delegation (credentials.with_subject), so "primary" and organizer.self resolve to that alaner's own calendar.

The two daily tasks run in the same per-user job and share one API fetch via list_organized_events in providers/google_calendar_lifecycle_logic.py, cached with @cached_for(minutes=10, local_ram_cache_only=True, no_serialization=True). Rule 1 (single_events=True) and rule 3 (single_events=False) cache separately.

Recurrence edits go through patch_calendar_event_recurrence (shared/services/google/calendar.py) with sendUpdates="none"no notification emails are sent to attendees when a cap is applied.

Operating it

  1. Enable the daily tasks. They are inert until a UserLifecycleTaskStatus row exists and is active. After merge, run sync_task_status to create rows for google-calendar-terminate-fully-declined-events and google-calendar-cap-open-ended-recurring, then toggle them active via the admin / Notion surface:
flask user-lifecycle sync_task_status
  1. Dry-run first, scoped to just these tasks for one alaner (logs "Would delete/cap …", writes nothing):
flask user-lifecycle run_tasks --task-pattern 'google-calendar-*' --alaner-id <id> --dry-run

Once active, they run automatically in the daily run_tasks sweep.

  1. Run the one-off cap (rule 2). --until must be a future date (else UsageError); omit --alaner-id to sweep all active alaners. Work runs on the LOW_PRIORITY RQ queue.
flask user-lifecycle expire_recurring_meetings --until=YYYY-MM-DD [--alaner-id <id>] [--dry-run]

Reporting. Per-task start/stop summaries go to #calendar-lifecycle-cleanup; CLI and task failures go to #alan_home_alerts. Like any UserLifecycleTask, a task auto-deactivates after N consecutive failures — see the framework behavior in the User Lifecycle overview.

Key files

File Contents
apps/eu_tools/user_lifecycle/providers/google_calendar_lifecycle.py Rule 1 & 3 task classes + rule 2 worker (expire_recurring_meetings_for_alaner, process_expire_recurring_meetings_for_alaner)
apps/eu_tools/user_lifecycle/providers/google_calendar_lifecycle_logic.py Pure helpers: cached event listing, RRULE parse/cap, declined-attendee check
apps/eu_tools/user_lifecycle/scheduled_tasks.py Provider import (auto-registers the tasks) + expire_recurring_meetings CLI command
shared/services/google/calendar.py patch_calendar_event_recurrence (silent recurrence patch)