Skip to content

Instantly share code, notes, and snippets.

@jpoutrin
Last active February 20, 2026 09:08
Show Gist options
  • Select an option

  • Save jpoutrin/e028db163b73b8deedd61399d642460d to your computer and use it in GitHub Desktop.

Select an option

Save jpoutrin/e028db163b73b8deedd61399d642460d to your computer and use it in GitHub Desktop.
Calendar Sync
/**
* Calendar Sync Script
* Syncs events from multiple source calendars to your main pro calendar as private blockers.
*
* CONFIGURATION: Edit the CONFIG object below with your calendar IDs
*/
const CONFIG = {
// Your main professional calendar (where all events will be synced TO)
// Use 'primary' for your default calendar, or the calendar ID
targetCalendarId: 'CAL_ID_TARGET',
// Source calendars to sync FROM (add as many as needed)
// You can find calendar IDs in Google Calendar > Settings > Calendar settings
sourceCalendars: [
{
id: 'CAL ID',
name: 'Perso',
color: '2' // Optional: CalendarApp.EventColor (1-11)
},
{
id: 'CAR ID',
name: 'CAL NAME',
color: '9'
},
// {
// id: 'client2-calendar-id@group.calendar.google.com',
// name: 'Client 2',
// color: '3'
// }
// Add more calendars as needed
],
// How many days in the past and future to sync
daysInPast: 7,
daysInFuture: 60,
// Prefix for synced events (helps identify them)
syncPrefix: '🔒 ',
// Unique identifier stored in event description to track synced events
syncTag: '[CALENDAR-SYNC-ID:',
excludeTag: '#nosync'
};
/**
* Main sync function - run this manually or via trigger
*/
function syncAllCalendars() {
console.log('Starting calendar sync...');
const targetCalendar = CalendarApp.getCalendarById(CONFIG.targetCalendarId)
|| CalendarApp.getDefaultCalendar();
if (!targetCalendar) {
console.error('Target calendar not found!');
return;
}
const now = new Date();
const startDate = new Date(now.getTime() - (CONFIG.daysInPast * 24 * 60 * 60 * 1000));
const endDate = new Date(now.getTime() + (CONFIG.daysInFuture * 24 * 60 * 60 * 1000));
// Get all existing synced events in target calendar
const existingSyncedEvents = getExistingSyncedEvents(targetCalendar, startDate, endDate);
const processedEventIds = new Set();
// Process each source calendar
for (const sourceConfig of CONFIG.sourceCalendars) {
console.log(`Processing: ${sourceConfig.name}`);
try {
const sourceCalendar = CalendarApp.getCalendarById(sourceConfig.id);
if (!sourceCalendar) {
console.warn(`Calendar not found or not accessible: ${sourceConfig.name} (${sourceConfig.id})`);
continue;
}
const sourceEvents = sourceCalendar.getEvents(startDate, endDate);
console.log(`Found ${sourceEvents.length} events in ${sourceConfig.name}`);
for (const sourceEvent of sourceEvents) {
// const sourceTitleDebug = sourceEvent.getTitle();
// const sourceEventId = sourceEvent.getId();
const syncId = generateSyncId(sourceConfig.id, sourceEvent);
processedEventIds.add(syncId);
syncEvent(sourceEvent, targetCalendar, sourceConfig, syncId, existingSyncedEvents);
}
} catch (error) {
console.error(`Error processing ${sourceConfig.name}: ${error.message}`);
}
}
// Clean up orphaned events (deleted from source)
cleanupOrphanedEvents(existingSyncedEvents, processedEventIds);
console.log('Calendar sync completed!');
}
/**
* Get all events in target calendar that were created by this sync
*/
function getExistingSyncedEvents(calendar, startDate, endDate) {
const events = calendar.getEvents(startDate, endDate);
const syncedEvents = new Map();
for (const event of events) {
// const title = event.getTitle();
const description = event.getDescription() || '';
const syncIdMatch = description.match(/\[CALENDAR-SYNC-ID:([^\]]+)\]/);
if (syncIdMatch) {
syncedEvents.set(syncIdMatch[1], event);
}
}
return syncedEvents;
}
/**
* Generate a unique sync ID for tracking events
*/
function generateSyncId(calendarId, event) {
const eventId = event.getId();
const startTime = event.getStartTime().getTime();
const combined = `${calendarId}|${eventId}|${startTime}`;
const hash = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, combined);
return hash.map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join('');
}
/**
* Sync a single event to the target calendar
*/
function syncEvent(sourceEvent, targetCalendar, sourceConfig, syncId, existingSyncedEvents) {
// Skip if the source event is already a synced event (avoid loops)
const sourceDescription = sourceEvent.getDescription() || '';
if (sourceDescription.includes(CONFIG.syncTag)) {
return;
}
const eventDay = sourceEvent.getStartTime().getDay();
if (eventDay === 0 || eventDay === 6) {
return;
}
const existingEvent = existingSyncedEvents.get(syncId);
// Prepare event details
const originalTitle = sourceEvent.getTitle();
const originalDescription = sourceEvent.getDescription();
const title = `${CONFIG.syncPrefix}${sourceConfig.name}: ${originalTitle}`;
const description = `Synced from: ${sourceConfig.name}\n${CONFIG.syncTag}${syncId}]`;
if (originalTitle.includes(CONFIG.excludeTag) || originalDescription.includes(CONFIG.excludeTag)) {
return;
}
const isAllDay = sourceEvent.isAllDayEvent();
const startTime = sourceEvent.getStartTime();
const endTime = sourceEvent.getEndTime();
const status = sourceEvent.getMyStatus();
// Determine if should show as busy or free
// If declined or the original is set to "free", mark as free
const guestsCanSee = sourceEvent.guestsCanSeeGuests();
const eventType = sourceEvent.getEventType().toString();
if (existingEvent) {
// Update existing synced event
if (needsUpdate(existingEvent, sourceEvent, title)) {
try {
if (isAllDay) {
// For all-day events, we need to delete and recreate
// because you can't change an event from timed to all-day
existingEvent.deleteEvent();
createSyncedEvent(targetCalendar, sourceEvent, title, description, sourceConfig, isAllDay);
} else {
existingEvent.setTime(startTime, endTime);
existingEvent.setTitle(title);
existingEvent.setDescription(description);
// existingEvent.setVisibility(CalendarApp.Visibility.PRIVATE);
}
console.log(`Updated: ${title}`);
} catch (error) {
console.error(`Error updating event: ${error.message}`);
}
}
} else {
// Create new synced event
createSyncedEvent(targetCalendar, sourceEvent, title, description, sourceConfig, isAllDay);
}
}
/**
* Create a new synced event in the target calendar
*/
function createSyncedEvent(targetCalendar, sourceEvent, title, description, sourceConfig, isAllDay) {
try {
let newEvent;
if (isAllDay) {
const startDate = sourceEvent.getAllDayStartDate();
const endDate = sourceEvent.getAllDayEndDate();
// Check if multi-day
const daysDiff = Math.round((endDate - startDate) / (24 * 60 * 60 * 1000));
if (daysDiff > 1) {
newEvent = targetCalendar.createAllDayEvent(title, startDate, endDate);
} else {
newEvent = targetCalendar.createAllDayEvent(title, startDate);
}
} else {
newEvent = targetCalendar.createEvent(title, sourceEvent.getStartTime(), sourceEvent.getEndTime());
}
newEvent.setDescription(description);
newEvent.setVisibility(CalendarApp.Visibility.PRIVATE);
if (sourceEvent.getEventType() === CalendarApp.EventType.WORKING_LOCATION) {
newEvent.setVisibility(CalendarApp.Visibility.PUBLIC);
} else {
newEvent.setVisibility(CalendarApp.Visibility.PRIVATE);
}
// Set color if specified
if (sourceConfig.color) {
newEvent.setColor(sourceConfig.color);
}
// Remove any default notifications (optional - keeps it clean)
newEvent.removeAllReminders();
console.log(`Created: ${title}`);
} catch (error) {
console.error(`Error creating event: ${error.message}`);
}
}
/**
* Check if an existing synced event needs to be updated
*/
function needsUpdate(existingEvent, sourceEvent, newTitle) {
const existingTitle = existingEvent.getTitle();
if (existingTitle !== newTitle) return true;
if (sourceEvent.isAllDayEvent() !== existingEvent.isAllDayEvent()) return true;
if (!sourceEvent.isAllDayEvent()) {
if (existingEvent.getStartTime().getTime() !== sourceEvent.getStartTime().getTime()) return true;
if (existingEvent.getEndTime().getTime() !== sourceEvent.getEndTime().getTime()) return true;
} else {
if (existingEvent.getAllDayStartDate().getTime() !== sourceEvent.getAllDayStartDate().getTime()) return true;
}
return false;
}
/**
* Remove synced events that no longer exist in source calendars
*/
function cleanupOrphanedEvents(existingSyncedEvents, processedEventIds) {
let cleaned = 0;
for (const [syncId, event] of existingSyncedEvents) {
if (!processedEventIds.has(syncId)) {
try {
console.log(`Removing orphaned event: ${event.getTitle()}`);
event.deleteEvent();
cleaned++;
} catch (error) {
console.error(`Error deleting orphaned event: ${error.message}`);
}
}
}
if (cleaned > 0) {
console.log(`Cleaned up ${cleaned} orphaned events`);
}
}
/**
* Setup automatic trigger - run this once manually
*/
function setupTrigger() {
// Remove existing triggers for this function
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
if (trigger.getHandlerFunction() === 'syncAllCalendars') {
ScriptApp.deleteTrigger(trigger);
}
}
// Create new trigger - runs every 15 minutes
ScriptApp.newTrigger('syncAllCalendars')
.timeBased()
.everyMinutes(15)
.create();
console.log('Trigger created! Sync will run every 15 minutes.');
}
/**
* Remove all triggers - run if you want to stop syncing
*/
function removeTriggers() {
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
if (trigger.getHandlerFunction() === 'syncAllCalendars') {
ScriptApp.deleteTrigger(trigger);
}
}
console.log('All sync triggers removed.');
}
/**
* Manual cleanup - removes ALL synced events from target calendar
* Use with caution!
*/
function removeAllSyncedEvents() {
const targetCalendar = CalendarApp.getCalendarById(CONFIG.targetCalendarId)
|| CalendarApp.getDefaultCalendar();
const now = new Date();
const startDate = new Date(now.getTime() - (CONFIG.daysInPast * 24 * 60 * 60 * 1000));
const endDate = new Date(now.getTime() + (CONFIG.daysInFuture * 24 * 60 * 60 * 1000));
const events = targetCalendar.getEvents(startDate, endDate);
let removed = 0;
for (const event of events) {
const description = event.getDescription() || '';
if (description.includes(CONFIG.syncTag)) {
event.deleteEvent();
removed++;
}
}
console.log(`Removed ${removed} synced events.`);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment