One Calendar to Rule Them All: Sync Your Private Calendars with Google Apps Script

One Calendar to Rule Them All: Sync Your Private Calendars with Google Apps Script

9 minute read

Published:

Are you juggling multiple Google Calendars? Perhaps one for work, one for personal life, and another for a side project or a team you’re on. It’s a common scenario, but it often leads to a fragmented view of your time, making it easy to double-book yourself or miss important commitments. What if you could see all your busy times in one central place, without having to make all your private calendars public?

This guide will walk you through setting up a powerful Google Apps Script that automatically syncs your free/busy information from several private calendars to a single, master calendar.

Why You Need This Solution

Imagine these common scenarios:

  • The Consultant: You work with multiple clients, each with their own Google Workspace. You need to block off time on your primary work calendar when you have a meeting in a client’s calendar, but you can’t share your client’s calendar publicly.
  • The Work-Life Balancer: You want your colleagues to see when you’re busy with a personal appointment (like a doctor’s visit or a parent-teacher conference) without them seeing the actual details of the event. You need to block off the time on your work calendar as simply “Busy”.
  • The Team Player: You’re part of a project team, and you need to share your availability with the project manager. Instead of giving them access to your detailed personal and work calendars, you can provide them with a single calendar that only shows when you are free or busy.

This script solves these problems by creating “shadow” events. It looks at your source calendars and, for every event it finds, it creates a corresponding event on your master calendar with a generic title like “Busy”. This way, your availability is accurately reflected everywhere, but the sensitive details of your appointments remain private.

The Synchronization Script

This is the complete Google Apps Script you will be using. You don’t need to understand every line to use it, but it’s provided here for transparency and for those who wish to customize it further.

/**
 * @fileoverview Syncs multiple Google Calendars to a single calendar.
 *
 * This script is designed to sync events from multiple source calendars
 * (including private, unshared calendars via iCal URLs) to a single 
 * destination calendar. It only syncs the free/busy status, creating 
 * "Busy" events on the destination calendar.
 *
 * @version 2.0
 */

// --- START OF CONFIGURATION ---

const SOURCE_CALENDARS = [
  // Example for a shared calendar:
  // { id: 'work_calendar@your_domain.com', type: 'id' }, 
  
  // Example for a private calendar from another account:
  // { id: 'SECRET_ICAL_URL_FROM_ANOTHER_ACCOUNT', type: 'ical' }, 
];

const DESTINATION_CALENDAR_ID = '[email protected]';
const DAYS_IN_PAST = 2;
const DAYS_IN_FUTURE = 30;
const EVENT_TITLE = 'Busy';
const SYNC_TAG = '#gcal_sync';

// --- END OF CONFIGURATION ---

function getSourceCalendar(source) {
  try {
    let calendar = CalendarApp.getCalendarById(source.id);
    if (!calendar && source.type === 'ical') {
      Logger.log(`Subscribing to iCal calendar: ${source.id}`);
      calendar = CalendarApp.subscribeToCalendar(source.id, { hidden: true, selected: false });
    }
    if (!calendar) {
      Logger.log(`Could not find or subscribe to calendar with ID: ${source.id}`);
      return null;
    }
    return calendar;
  } catch (e) {
    Logger.log(`Error accessing or subscribing to calendar ${source.id}: ${e.toString()}`);
    return null;
  }
}

function syncCalendars() {
  const now = new Date();
  const startDate = new Date(now.getTime() - DAYS_IN_PAST * 24 * 60 * 60 * 1000);
  const endDate = new Date(now.getTime() + DAYS_IN_FUTURE * 24 * 60 * 60 * 1000);
  const destinationCalendar = CalendarApp.getCalendarById(DESTINATION_CALENDAR_ID);

  if (!destinationCalendar) {
    Logger.log('Destination calendar not found.');
    return;
  }

  deleteSyncedEvents(destinationCalendar, startDate, endDate);

  SOURCE_CALENDARS.forEach(source => {
    const sourceCalendar = getSourceCalendar(source);
    if (sourceCalendar) {
      syncSingleCalendar(sourceCalendar, destinationCalendar, startDate, endDate);
    } else {
      Logger.log(`Skipping source calendar: ${source.id}`);
    }
  });

  Logger.log('Calendar sync complete.');
}

function deleteSyncedEvents(calendar, startDate, endDate) {
  const syncedEvents = calendar.getEvents(startDate, endDate, { search: SYNC_TAG });
  syncedEvents.forEach(event => {
    try {
      event.deleteEvent();
    } catch (e) {
      Logger.log(`Error deleting event: ${e.toString()}`);
    }
  });
}

function syncSingleCalendar(sourceCalendar, destinationCalendar, startDate, endDate) {
  const events = sourceCalendar.getEvents(startDate, endDate);
  events.forEach(event => {
    if (event.getMyStatus() === CalendarApp.GuestStatus.NO || event.isAllDayEvent()) {
      return;
    }
    try {
      destinationCalendar.createEvent(
        EVENT_TITLE,
        event.getStartTime(),
        event.getEndTime(),
        { description: `Synced from ${sourceCalendar.getName()}\n${SYNC_TAG}` }
      ).setVisibility(CalendarApp.Visibility.PRIVATE);
    } catch (e) {
      Logger.log(`Error creating event: ${e.toString()}`);
    }
  });
}

function createTrigger() {
  const allTriggers = ScriptApp.getProjectTriggers();
  allTriggers.forEach(trigger => {
    if (trigger.getHandlerFunction() === 'syncCalendars') {
      ScriptApp.deleteTrigger(trigger);
    }
  });
  ScriptApp.newTrigger('syncCalendars').timeBased().everyHours(1).create();
  Logger.log('Trigger created successfully.');
}

Step-by-Step Configuration Guide

Here’s how to set up your automated calendar synchronization.

Step 1: Open Google Apps Script and Paste the Code

  1. Navigate to script.google.com and click New project.
  2. Delete any placeholder code in the Code.gs file.
  3. Copy the entire script from the section above and paste it into the editor.
  4. Click the save icon and give your project a name, such as “Master Calendar Sync”.

Step 2: Share Your Source Calendars

For the script to read events from calendars you own (or have direct access to), you must share them with the Google account that will run the script. You only need to do this for calendars you configure with type: 'id'. You can skip this for calendars you are syncing via the secret iCal URL.

  1. Open Google Calendar.
  2. On the left, find a source calendar you want to sync. Hover over it and click the three-dot menu (â‹®).
  3. Select Settings and sharing.
  4. Under the Share with specific people or groups section, click Add people and groups.
  5. Enter the email address of the Google account where you are setting up this script.
  6. In the permissions dropdown, select See all event details.
  7. Click Send. On you destination account, open received Email and click Join shared calendar. Add calendar in new window.
  8. Repeat this process for all source calendars that you are syncing by their ID.

Step 3: Configure Your Calendars in the Script

This is the most crucial part. You need to tell the script which calendars to read from and which one to write to by editing the CONFIGURATION section at the top of the script.

Locate the SOURCE_CALENDARS array. This is where you’ll list all the calendars you want to sync from. You can add two types:

Calendars You Own or Have Access To (id): For your primary calendar or any calendar shared with your Google account as described in Step 2.

{ id: '[email protected]', type: 'id' }

Private, Unshared Calendars (ical): For calendars from other accounts (like a client’s or a separate personal account).

How to get the iCal URL:

  1. In the Google account that owns the private calendar, open Google Calendar.
  2. Find the calendar on the left, click the three-dot menu (â‹®), and select Settings and sharing.
  3. Scroll to Integrate calendar and copy the URL under Secret address in iCal format.
  4. Add it to the script like this:
{ id: 'https://calendar.google.com/calendar/ical/..../private-..../basic.ics', type: 'ical' }

Set Your Destination Calendar:

  1. Find the DESTINATION_CALENDAR_ID variable.
  2. Replace the placeholder value with the ID of the calendar you want all the “Busy” events to be created on. It’s recommended to create a new, separate calendar for this purpose.

Step 4: For Google Workspace Users (Administrator Steps)

If you are using a Google Workspace account (e.g., [email protected]) and can’t find the “Secret address in iCal format” for a private calendar, your administrator has likely disabled external sharing. You will need to ask your Google Workspace administrator to perform the following steps:

  1. Log in to the Google Admin console at admin.google.com.
  2. Navigate to Apps > Google Workspace > Calendar.
  3. Click on Sharing settings.
  4. In the External Sharing options section, choose a setting that allows sharing information. The recommended setting for this use case is “Share all information, but outsiders cannot change calendars”.
  5. Click Save.

After the administrator makes this change (it may take a few minutes to apply), the “Secret address in iCal format” will become available in your calendar’s settings page.

Step 5: First Run and Authorization

  1. In the Apps Script editor toolbar, ensure the syncCalendars function is selected in the dropdown menu.
  2. Click Run.
  3. A popup will appear asking for authorization. Click Review permissions.
  4. Choose your Google account.
  5. You’ll likely see a screen saying “Google hasn’t verified this app”. This is normal for personal scripts. Click Advanced, and then click Go to (your project name) (unsafe).
  6. Click Allow to grant the script the necessary permissions to manage your calendars.
  7. The script will now run for the first time, syncing your calendars.

Step 6: Set Up Automatic Syncing

To keep your master calendar up-to-date automatically, you need to create a trigger.

  1. In the Apps Script editor, select the createTrigger function from the dropdown menu.
  2. Click Run. You’ll see a confirmation log once the trigger is created.

That’s it! The script will now run every hour, deleting the old “Busy” events and creating new ones to perfectly reflect the current state of all your source calendars. You now have a single, reliable view of your true availability across all your commitments.