Skip to content

Instantly share code, notes, and snippets.

@quad
Last active May 1, 2026 00:56
Show Gist options
  • Select an option

  • Save quad/35577aaca8880f6245b929cadbda2fbf to your computer and use it in GitHub Desktop.

Select an option

Save quad/35577aaca8880f6245b929cadbda2fbf to your computer and use it in GitHub Desktop.
Block out work calendar from personal calendar

Steps

  1. Create a new Apps Script project
  2. Add Google Calendar API as a Service in the left sidebar
  3. Change timezone to UTC in Project Settings
  4. Copy-paste in constants.gs
    • Change SRC_ID to the address for your source calendar
  5. Copy-paste in code.gs
  6. (Optional) If you're migrating from an older version of this script…
    1. Copy-paste in migrate.gs
    2. Run the migrate function
  7. Add a Trigger via the left sidebar:
    • Function to run: sync
    • Event source: time-driven, hour timer, every hour
    • Notify me of failures: daily
const SRC_ID = "you@example.com";
const DST_ID = "primary";
const MIRROR_TAG = "mirror=block";
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);
}
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