A shared team calendar that's been in use for years accumulates thousands of past events, and deleteOldCalendarEvents keeps that history from growing forever by removing single events that fall outside a defined retention window.
Manually scrolling back through years of calendar history to delete old appointments one at a time isn't just tedious, it's also risky - it's easy to accidentally delete something recent while trying to navigate that far back.
This example defines a retention window using two dates - the cutoff for how recent an event needs to be to survive, and a second, older boundary limiting how far back the search itself looks - and deletes every non-recurring event found in between.
You'll finish with a self-maintaining calendar cleanup that runs on a schedule, skips recurring events entirely to avoid breaking an ongoing series, and logs exactly how many events it removed each time.
Need this built? Hire a Google Apps Script developer →
Sheet / project setup
| Setting | Location | Purpose |
|---|---|---|
| retentionDays | Script constant | How many days back an event must be before it's eligible for deletion |
| rangeStart / rangeEnd | Computed in script | The two-year lookback window searched for events to delete |
| Cleanup Log | Sheet | Records the date and count of every cleanup run |
What it does
Every scheduled run computes a rangeEnd equal to today minus the retention period, and a rangeStart two years further back than that, then fetches every event falling between those two dates. Each single (non-recurring) event found gets deleted, and the total count is logged for later review.
- Recurring events are explicitly skipped so an active weekly meeting series is never accidentally broken
- The search window itself is capped at two years back, avoiding an unbounded and slow query
- Every run appends a summary line to a Cleanup Log sheet
Prerequisites
The script's account needs edit access to the calendar in order to call deleteEvent, and the retention period should be chosen carefully since deleted events cannot be recovered through the script.
- A shared calendar ID the script account can edit, not just view
- A Cleanup Log sheet ready to receive appendRow calls
- A time-driven trigger calling deleteOldCalendarEvents on a schedule, such as weekly or monthly
- Agreement within the team on what retention period is appropriate before this runs unattended
Walkthrough
rangeEnd is computed by subtracting retentionDays from the current date, which defines the newest event date still eligible for deletion; anything more recent than that is left untouched no matter what.
isRecurringEvent() is what protects an ongoing weekly or monthly series from being deleted just because one of its historical occurrences falls inside the cleanup window - recurring event instances are skipped entirely rather than deleted one occurrence at a time.
Edge cases
Events older than the two-year rangeStart boundary are never even considered by this function, so an unusually long-running cleanup schedule (say, running for the first time after years of no cleanup) would need multiple runs with an adjusted rangeStart to fully catch up.
- A single event that's part of a series but was individually modified may still report isRecurringEvent() as true depending on how it was edited
- Events with guests still get deleted along with any pending RSVPs, which is a one-way, irreversible action
- Calendar events created by other calendar systems synced into Google Calendar may behave differently around deletion permissions
Testing
Before running this against a real calendar, test with a very short retentionDays value against a disposable test calendar populated with a handful of known past events, confirming exactly which ones get removed.
- Create a test recurring event overlapping the deletion window and confirm it's correctly skipped
- Verify the Cleanup Log records an accurate count after a test run
- Confirm events newer than the retention cutoff are left completely untouched
Hardening
Because deleteEvent is irreversible, adding a dry-run mode that only logs what would be deleted, without actually calling deleteEvent, is worth having available before trusting this on an unattended schedule against a calendar people rely on.
- Add a dryRun flag that logs candidate events without deleting them, for a safe first look at what a given retention window would remove
- Export a backup of event details to a sheet immediately before deletion, as a lightweight recovery record
- Extend rangeStart further back and re-run in batches if this is the very first cleanup on a calendar with many years of history
Variations
The same retention-window deletion pattern applies to cleaning up old tasks, form responses, or Drive files just as easily as calendar events, by swapping CalendarApp calls for the equivalent API on whichever resource needs periodic cleanup.
- Archive event details to a sheet before deleting instead of only logging a count
- Apply a different retention period to different calendars by looping over an array of calendar IDs and windows
- Skip deletion for events matching a specific keyword or label, such as 'Legal Hold', regardless of age
Full code: deleteOldCalendarEvents()
The function bounds its own search window explicitly, both to keep the query fast and to make the retention policy's boundaries easy to reason about and adjust.
function deleteOldCalendarEvents() {
var calendar = CalendarApp.getCalendarById('team-calendar@group.calendar.google.com');
var retentionDays = 90;
var rangeEnd = new Date();
rangeEnd.setDate(rangeEnd.getDate() - retentionDays);
var rangeStart = new Date(rangeEnd.getTime());
rangeStart.setFullYear(rangeStart.getFullYear() - 2);
var events = calendar.getEvents(rangeStart, rangeEnd);
var deletedCount = 0;
for (var i = 0; i < events.length; i++) {
var event = events[i];
if (event.isRecurringEvent()) continue;
event.deleteEvent();
deletedCount++;
}
var logSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Cleanup Log');
logSheet.appendRow([new Date(), deletedCount + ' events older than ' + retentionDays + ' days deleted']);
}- Line 2: Targets a specific shared calendar by ID for the cleanup, rather than the default personal calendar.
- Line 6: Computes the newest date still eligible for deletion by subtracting the retention period from today.
- Line 8: Bounds how far back the search itself goes, preventing an unbounded historical query.
- Line 10: Fetches every event falling inside the computed two-boundary window in one call.
- Line 15: Skips any event that's part of a recurring series, protecting ongoing meetings from deletion.
- Line 17: Permanently deletes the event once it's confirmed to be both old enough and non-recurring.
- Line 22: Logs a summary of the run, including the exact count of events removed.
Deploy this example
- 01
Open Apps Script
In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.
- 02
Paste and save
Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.
- 03
Authorize once
Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.
- 04
Add the trigger
Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.
Before scheduling calendar cleanup
- 1Retention period agreed upon by the team before enabling this unattended
- 2Recurring-event skip logic tested against a real recurring series
- 3Cleanup Log sheet ready to record run history
- 4Time-driven trigger frequency chosen to match how fast the calendar accumulates old events
- 5Considered a dry-run mode before the first real deletion pass
- 6Backup or export strategy decided for events about to be permanently deleted
Frequently asked questions
Not through this script - CalendarApp's deleteEvent call is permanent from Apps Script's perspective; Google Calendar's own trash retention in the web UI may offer a short recovery window for manually deleted events, but scripted deletions should be treated as final.
Deleting individual occurrences of a recurring series from a script risks breaking the series' internal recurrence rule or leaving orphaned exceptions; it's much safer to leave the entire series alone and handle recurring event cleanup as a separate, more careful process if truly needed.
Run the function multiple times with rangeStart pushed progressively further back on each pass, since the two-year cap exists specifically to keep each individual query fast and bounded rather than scanning an entire calendar's history at once.
Any guests who had the event on their own calendars will typically see it disappear or show as cancelled the next time their calendar syncs, since deleteEvent removes the event for all attendees, not just the calendar it was deleted from.
Not directly through getEvents, but you can check each event's description or a custom marker (like a specific string added when the event was created) before deciding to delete it, filtering out events that weren't created by your own automation.
Calendar API calls are inherently rate-limited per event, so for tens of thousands of events, batching runs across multiple scheduled executions or using the Calendar API's batch endpoints directly (outside of CalendarApp) can be considerably faster.