Christine Phu

Google AppScript: Automated OOO Calendar Sync

Instructions and script written by Al.
Reviewed, edited, and tested by Christine.

Overview

This script automatically mirrors your personal Out of Office (OOO) events to a shared team calendar so you don't have to manually create duplicate entries.

Key Features

Limitations & Behavior

To ensure the script runs reliably, please keep the following in mind:

Setup Instructions

1. Create the Script

  1. Go to script.google.com.
  2. Click New Project and name it "OOO Sync Tool".
  3. Delete any existing code in the editor and paste the script provided below.

2. Enable the Calendar API (Required)

  1. On the left sidebar, click the + icon next to Services.
  2. Find and select Google Calendar API.
  3. Click Add.

3. Personalize the Script

Edit the variables at the top of the script:

4. Run & Authorize

  1. Click Run at the top.
  2. Review and Allow permissions.
  3. Check the Executions tab on the left to confirm the sync was successful.

5. Set the Hourly Automator

  1. Click the Triggers (Clock icon) on the left sidebar -> + Add Trigger.
  2. Function: syncOOO | Source: Time-driven | Type: Hour timer | Interval: Every hour.

The Script

(Remember to test it!)


// --- CONFIGURATION
const SHARED_CALENDAR_ID = 'YOUR_SHARED_CALENDAR_ID';
const MY_UNIQUE_TAG = 'YOUR_UNIQUE_TAG_HERE';
const USER_NAME = 'YOUR_NAME_HERE';

function syncOOO() {
  const primaryCalId = 'primary';
  const now = new Date();
  const futureLimit = new Date();
  futureLimit.setMonth(now.getMonth() + 3);

  console.info("Starting sync for period: " + now.toLocaleDateString() + " to " + futureLimit.toLocaleDateString());

  // 1. Fetch OOO entries from your personal calendar
  const oooEntries = Calendar.Events.list(primaryCalId, {
    timeMin: now.toISOString(),
    timeMax: futureLimit.toISOString(),
    eventTypes: ['outOfOffice'],
    singleEvents: true
  }).items || [];

  const sharedCal = CalendarApp.getCalendarById(SHARED_CALENDAR_ID);
  if (!sharedCal) {
    console.error("Could not find Shared Calendar. Check the SHARED_CALENDAR_ID.");
    return;
  }

  const sharedEvents = sharedCal.getEvents(now, futureLimit);
  const mySyncedEvents = sharedEvents.filter(e => e.getTag('sync_source') === MY_UNIQUE_TAG);

  // 2. UPDATE OR DELETE EXISTING SYNCED EVENTS
  mySyncedEvents.forEach(syncedEvent => {
    const matchingOOO = oooEntries.find(ooo => {
      const oooStart = new Date(ooo.start.dateTime || ooo.start.date).getTime();
      return oooStart === syncedEvent.getStartTime().getTime();
    });

    if (!matchingOOO) {
      console.warn("DELETING: " + syncedEvent.getTitle() + " (Event removed or start time changed on personal cal)");
      syncedEvent.deleteEvent();
    } else {
      const newTitle = USER_NAME + " " + matchingOOO.summary;
      const newEnd = new Date(matchingOOO.end.dateTime || matchingOOO.end.date);
      let changed = false;

      if (syncedEvent.getTitle() !== newTitle) {
        console.info("UPDATING TITLE: " + syncedEvent.getTitle() + " -> " + newTitle);
        syncedEvent.setTitle(newTitle);
        changed = true;
      }

      if (syncedEvent.getEndTime().getTime() !== newEnd.getTime()) {
        console.info("UPDATING TIME: Duration changed for " + newTitle);
        syncedEvent.setTime(syncedEvent.getStartTime(), newEnd);
        changed = true;
      }

      if (!changed) {
        console.log("NO CHANGE: " + newTitle + " is already up to date.");
      }
    }
  });

  // 3. CREATE NEW SYNCED EVENTS
  oooEntries.forEach(ooo => {
    const start = new Date(ooo.start.dateTime || ooo.start.date);
    const alreadyExists = mySyncedEvents.some(e => e.getStartTime().getTime() === start.getTime());

    if (!alreadyExists) {
      const title = USER_NAME + " " + ooo.summary;
      const end = new Date(ooo.end.dateTime || ooo.end.date);
      const newEvent = sharedCal.createEvent(title, start, end);
      newEvent.setTag('sync_source', MY_UNIQUE_TAG);
      console.info("CREATED: New event synced -> " + title);
    }
  });

  console.info("Sync process complete.");
}

↑ Back to top

#work