- Create a new Apps Script project
- Add
Google Calendar APIas aServicein the left sidebar - Change timezone to UTC in Project Settings
- Copy-paste in constants.gs
- Change
SRC_IDto the address for your source calendar
- Change
- Copy-paste in code.gs
- (Optional) If you're migrating from an older version of this script…
- Copy-paste in migrate.gs
- Run the
migratefunction
- Add a
Triggervia the left sidebar:- Function to run:
sync - Event source: time-driven, hour timer, every hour
- Notify me of failures:
daily
- Function to run:
Last active
May 1, 2026 00:56
-
-
Save quad/35577aaca8880f6245b929cadbda2fbf to your computer and use it in GitHub Desktop.
Block out work calendar from personal calendar
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const SRC_ID = "you@example.com"; | |
| const DST_ID = "primary"; | |
| const MIRROR_TAG = "mirror=block"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const LEGACY_TAG_KEY = "block.id"; | |
| const MIGRATION_WINDOW_DAYS = 60; | |
| function migrate() { | |
| const now = new Date(); | |
| const timeMin = new Date(now); timeMin.setDate(now.getDate() - MIGRATION_WINDOW_DAYS); | |
| const timeMax = new Date(now); timeMax.setDate(now.getDate() + MIGRATION_WINDOW_DAYS); | |
| const window = { timeMin: timeMin.toISOString(), timeMax: timeMax.toISOString() }; | |
| // Map legacy IDs to current API ids. Legacy format: | |
| // non-recurring: iCalUID | |
| // recurring instance: `${iCalUID}-${startUTC}-${endUTC}` | |
| const legacyToNew = new Map(); | |
| for (const e of listAll(SRC_ID, { ...window, eventTypes: ["default"] })) { | |
| if (!e.start.dateTime) continue; | |
| const legacy = e.recurringEventId | |
| ? `${e.iCalUID}-${new Date(e.start.dateTime).toISOString()}-${new Date(e.end.dateTime).toISOString()}` | |
| : e.iCalUID; | |
| legacyToNew.set(legacy, e.id); | |
| } | |
| const dstCal = CalendarApp.getCalendarById(DST_ID); | |
| let migrated = 0, alreadyDone = 0, orphaned = 0; | |
| for (const e of listAll(DST_ID, window)) { | |
| if (e.extendedProperties?.private?.mirror === "block") { | |
| alreadyDone++; | |
| continue; | |
| } | |
| const legacyId = dstCal.getEventById(e.iCalUID)?.getTag(LEGACY_TAG_KEY); | |
| if (!legacyId) continue; | |
| const newId = legacyToNew.get(legacyId); | |
| if (!newId) { | |
| Logger.log("Orphaned (no matching source): %s [%s]", e.summary, legacyId); | |
| orphaned++; | |
| continue; | |
| } | |
| Calendar.Events.patch({ | |
| extendedProperties: { private: { mirror: "block", sourceId: newId } }, | |
| }, DST_ID, e.id); | |
| Logger.log("Migrated: %s -> %s", legacyId, newId); | |
| migrated++; | |
| } | |
| Logger.log("Done. migrated=%d alreadyDone=%d orphaned=%d", migrated, alreadyDone, orphaned); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const SYNC_WINDOW_DAYS = 7; | |
| function sync() { | |
| const now = new Date(); | |
| const timeMin = new Date(now); timeMin.setDate(now.getDate() - SYNC_WINDOW_DAYS); | |
| const timeMax = new Date(now); timeMax.setDate(now.getDate() + SYNC_WINDOW_DAYS); | |
| const window = { timeMin: timeMin.toISOString(), timeMax: timeMax.toISOString() }; | |
| const sourceEvents = new Map( | |
| listAll(SRC_ID, { ...window, eventTypes: ["default"] }) | |
| .filter(e => e.start.dateTime) // skip all-day events | |
| .filter(blocksTime) | |
| .map(e => [e.id, e]) | |
| ); | |
| const existing = new Map(); | |
| for (const e of listAll(DST_ID, { ...window, privateExtendedProperty: [MIRROR_TAG] })) { | |
| const id = e.extendedProperties?.private?.sourceId; | |
| if (!id) continue; | |
| if (!sourceEvents.has(id)) { | |
| Logger.log("Deleting stale event: %s", id); | |
| Calendar.Events.remove(DST_ID, e.id); | |
| continue; | |
| } | |
| if (existing.has(id)) { | |
| Logger.log("Deleting duplicate event: %s", id); | |
| Calendar.Events.remove(DST_ID, e.id); | |
| continue; | |
| } | |
| existing.set(id, e); | |
| } | |
| for (const [id, src] of sourceEvents) { | |
| const dst = existing.get(id); | |
| if (!dst) { | |
| Logger.log("Creating event: %s [id=%s]", src.summary, id); | |
| Calendar.Events.insert({ | |
| summary: "Blocked", | |
| start: src.start, | |
| end: src.end, | |
| visibility: "private", | |
| colorId: "8", // graphite | |
| transparency: "opaque", | |
| reminders: { useDefault: false }, | |
| extendedProperties: { private: { mirror: "block", sourceId: id } }, | |
| }, DST_ID); | |
| continue; | |
| } | |
| if (+new Date(dst.start.dateTime) !== +new Date(src.start.dateTime) || | |
| +new Date(dst.end.dateTime) !== +new Date(src.end.dateTime)) { | |
| Logger.log("Correcting event: %s [id=%s]", src.summary, id); | |
| Calendar.Events.patch({ start: src.start, end: src.end }, DST_ID, dst.id); | |
| } | |
| } | |
| } | |
| // Block source event unless we explicitly declined or it's marked free. | |
| function blocksTime(e) { | |
| if (e.transparency === "transparent") return false; | |
| const me = e.attendees?.find(a => a.self); | |
| return !me || me.responseStatus !== "declined"; | |
| } | |
| function listAll(calendarId, params) { | |
| const items = []; | |
| let pageToken; | |
| do { | |
| const resp = Calendar.Events.list(calendarId, { | |
| singleEvents: true, | |
| showDeleted: false, | |
| maxResults: 2500, | |
| ...params, | |
| pageToken, | |
| }); | |
| items.push(...resp.items); | |
| pageToken = resp.nextPageToken; | |
| } while (pageToken); | |
| return items; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment