Skip to content

Instantly share code, notes, and snippets.

@cr0ybot
Last active May 14, 2026 16:52
Show Gist options
  • Select an option

  • Save cr0ybot/c4d21271b740bc098e79c135ba4ed766 to your computer and use it in GitHub Desktop.

Select an option

Save cr0ybot/c4d21271b740bc098e79c135ba4ed766 to your computer and use it in GitHub Desktop.
SyncPersonalCalendar
{
"timeZone": "America/Chicago",
"dependencies": {
"enabledAdvancedServices": [
{
"userSymbol": "Calendar",
"version": "v3",
"serviceId": "calendar"
}
]
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/calendar"
]
}
/**
* Sync personal events with your work calendar.
*
* Uses sync tokens (via the advanced Calendar API) for efficient incremental updates.
* Tracks a personalEventId → workEventId map in PropertiesService so that work
* calendar events are deleted or updated when the originating personal event changes.
*
* Setup:
* 1. From your *personal* calendar, add your work account in Calendar Settings > Share with specific people, your work cal needs at least "See all event details" access to your personal cal.
* 2. Create an Apps Script project in your *work* Google account: https://script.google.com/u/0/home
* 3. Add this file and the appsscript.json file.
* 4. In the Apps Script project, go to Project Settings > Script Properties and add the following:
* - fromEmail (required) — email address of the personal calendar to sync from
* - daysOut (optional, default: 30) — how many days ahead to monitor on the initial full sync
* - skipWeekends (optional, default: true) — set to "false" to also sync weekend events
* - officeHoursStart (optional, default: 8) — work day start hour (24-hour)
* - officeHoursEnd (optional, default: 16) — work day end hour (24-hour)
* - label (optional, default: "Out of Office") — title prefix for copied events
* - acceptCalendars (optional) — comma-separated list of *additional* calendar IDs (beyond fromEmail)
* whose events should be synced, e.g. "friend@example.com,shared@example.com"
* 5. Add a trigger for the `sync` function:
* - Event source: From calendar
* - Event calendar details: Calendar updated
* - Calendar owner email: [your personal email address]
*
* @version 2.0.0
* @author Cory Hughart <cory@coryhughart.com>
* @copyright 2026 Cory Hughart
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL-3.0-or-later
* @link https://gist.github.com/cr0ybot/c4d21271b740bc098e79c135ba4ed766
* @ref https://medium.com/@willroman/auto-block-time-on-your-work-google-calendar-for-your-personal-events-2a752ae91dab
*/
function getConfig() {
const props = PropertiesService.getScriptProperties().getProperties();
const toEmail = CalendarApp.getDefaultCalendar().getId();
const fromEmail = props['fromEmail'];
if (!fromEmail) throw new Error('Script property "fromEmail" is required.');
const daysOut = parseInt(props['daysOut'] || '30', 10);
const skipWeekends = (props['skipWeekends'] ?? 'true') !== 'false';
const officeHours = {
start: parseInt(props['officeHoursStart'] || '8', 10),
end: parseInt(props['officeHoursEnd'] || '16', 10),
};
const label = props['label'] || 'Out of Office';
const extraCalendars = (props['acceptCalendars'] || '')
.split(',')
.map(s => s.trim())
.filter(Boolean);
const acceptCalendars = [fromEmail, ...extraCalendars];
return { toEmail, fromEmail, daysOut, skipWeekends, officeHours, label, acceptCalendars };
}
/**
* Trigger entry point. Uses the stored sync token for an incremental update, or
* falls back to a full sync over the next `daysOut` days when no token exists.
*
* @ref https://developers.google.com/apps-script/advanced/calendar#synchronizing_events
*/
function sync() {
const config = getConfig();
const properties = PropertiesService.getUserProperties();
const syncToken = properties.getProperty('syncToken');
const eventMap = JSON.parse(properties.getProperty('eventMap') || '{}');
const toCal = CalendarApp.getDefaultCalendar();
const options = { maxResults: 100, singleEvents: true };
if (syncToken) {
options.syncToken = syncToken;
} else {
// No stored token: fetch the upcoming window for the initial full sync
const now = new Date();
const endDate = new Date();
endDate.setDate(now.getDate() + config.daysOut);
options.timeMin = now.toISOString();
options.timeMax = endDate.toISOString();
}
let pageToken;
let nextSyncToken;
do {
options.pageToken = pageToken;
let response;
try {
response = Calendar.Events.list(config.fromEmail, options);
} catch (e) {
if (e.message === 'Sync token is no longer valid, a full sync is required.') {
// Token was invalidated server-side; drop it and redo as a full sync
console.log('Sync token expired, falling back to full sync.');
properties.deleteProperty('syncToken');
sync();
return;
}
throw e;
}
for (const ev of (response.items || [])) {
processEvent(ev, toCal, eventMap, config);
}
pageToken = response.nextPageToken;
nextSyncToken = response.nextSyncToken;
} while (pageToken);
if (nextSyncToken) {
properties.setProperty('syncToken', nextSyncToken);
}
properties.setProperty('eventMap', JSON.stringify(eventMap));
}
// Evaluate a single personal calendar event and create, update, or remove its
// corresponding work calendar OOO block as needed.
function processEvent(ev, toCal, eventMap, config) {
const { toEmail, fromEmail, skipWeekends, officeHours, label, acceptCalendars } = config;
const workEventId = eventMap[ev.id];
// Remove the work event if the personal event was deleted or marked "free"
// (transparency === 'transparent'). This replaces the Freebusy API check.
if (ev.status === 'cancelled' || ev.transparency === 'transparent') {
if (workEventId) removeWorkEvent(workEventId, ev.id, eventMap);
return;
}
// Only process events whose organizer is in the accepted list (prevents spam)
const organizer = ev.organizer?.email;
if (!acceptCalendars.includes(organizer)) {
if (workEventId) removeWorkEvent(workEventId, ev.id, eventMap);
return;
}
// Skip events the personal account declined or hasn't responded to
if (ev.attendees?.length) {
const isOrganizer = organizer === fromEmail || organizer === toEmail;
const me = ev.attendees.find(a => a.email === fromEmail || a.email === toEmail);
if (!isOrganizer && (!me || me.responseStatus !== 'accepted')) {
if (workEventId) removeWorkEvent(workEventId, ev.id, eventMap);
return;
}
}
// Ignore all-day events (for now); they use ev.start.date instead of ev.start.dateTime
if (!ev.start?.dateTime) {
if (workEventId) removeWorkEvent(workEventId, ev.id, eventMap);
return;
}
const startTime = new Date(ev.start.dateTime);
const endTime = new Date(ev.end.dateTime);
// Skip weekends
const dow = startTime.getDay();
if (skipWeekends && (dow < 1 || dow > 5)) {
if (workEventId) removeWorkEvent(workEventId, ev.id, eventMap);
return;
}
// Skip events outside work hours
if (startTime.getHours() >= officeHours.end || endTime.getHours() <= officeHours.start) {
if (workEventId) removeWorkEvent(workEventId, ev.id, eventMap);
return;
}
const isPublic = ev.visibility === 'public';
const title = isPublic && ev.summary ? `${label} (${ev.summary})` : label;
// If we already have a work event for this personal event, update it in place
if (workEventId) {
const workEvent = CalendarApp.getEventById(workEventId);
if (workEvent) {
const timeChanged = workEvent.getStartTime().getTime() !== startTime.getTime()
|| workEvent.getEndTime().getTime() !== endTime.getTime();
const titleChanged = workEvent.getTitle() !== title;
if (timeChanged || titleChanged) {
console.log(`Updating work event for personal event ${ev.id}: ${title}`);
if (timeChanged) workEvent.setTime(startTime, endTime);
if (titleChanged) workEvent.setTitle(title);
}
return;
}
// Work event was externally deleted; fall through to recreate it
delete eventMap[ev.id];
}
// Create a new OOO block on the work calendar
console.log(`Creating work event for personal event ${ev.id}: ${title}`);
const newEvent = toCal.createEvent(title, startTime, endTime, { eventType: 'outOfOffice' });
newEvent.removeAllReminders();
eventMap[ev.id] = newEvent.getId();
}
// Delete the work calendar event and remove its entry from the map.
function removeWorkEvent(workEventId, personalEventId, eventMap) {
const workEvent = CalendarApp.getEventById(workEventId);
if (workEvent) {
console.log(`Removing work event ${workEventId} (personal event ${personalEventId} deleted/changed)`);
workEvent.deleteEvent();
}
delete eventMap[personalEventId];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment