```html

Fixing Calendar Sync Auto-Triggers in Google Apps Script: OAuth Re-auth + Scheduled Trigger Setup

What Was Done

The Boatsetter iCal integration was written and deployed to CalendarSync.gs, but the automated sync triggers were never activated. Additionally, Gmail and Calendar OAuth tokens had expired, which would have caused write failures even if triggers were running. This post walks through the exact steps to re-authorize GAS permissions and activate time-based triggers so that multi-platform booking feeds sync automatically to the ops calendar.

The Three Blockers

  • No active triggers: The function calendarSyncSetup() (line 355 in sites/queenofsandiego.com/CalendarSync.gs) was never executed, so no time-based triggers were registered in the GAS environment.
  • Expired Gmail OAuth: The service account token used by GAS to send daily reconciliation emails via Gmail API had expired.
  • Revoked Calendar OAuth: The Calendar service authorization had been revoked, preventing any write operations to the booking ledger calendar.

Technical Details: OAuth Re-authorization Flow

GAS manages OAuth tokens differently than Lambda or standalone Python scripts. When you call a GAS function that touches Gmail or Calendar APIs, the GAS runtime checks for valid tokens. If tokens are missing or expired, it halts execution.

The re-auth process is straightforward:

  1. Open the GAS editor at the project URL: https://script.google.com/d/1HiEgjBrCGrnOIvr27nIk1E1qoR1pwKmWLigl191Tz0xq7LautJrIp9Ii/edit
  2. Select testSync() from the function dropdown (line 563 in CalendarSync.gs)
  3. Click Run
  4. Google prompts: "This app needs access to your Google Account" — click Allow
  5. GAS may prompt twice (once for Gmail scope, once for Calendar scope) — allow both

Why run testSync() first instead of calendarSyncSetup()? Because testSync() actually exercises the Gmail and Calendar services. The setup function only touches SpreadsheetApp and ScriptApp (the trigger registration APIs), which don't require these OAuth scopes. By running testSync(), you force GAS to request the scopes upfront.

Trigger Registration: The Setup Function

Once OAuth is re-authorized, run calendarSyncSetup() (line 355). This function does three things:

// Simplified version of what calendarSyncSetup() does:
function calendarSyncSetup() {
  // 1. Create BookingLedger tab in the ops sheet
  const sheet = SpreadsheetApp.openById(OPS_SHEET_ID);
  if (!sheet.getSheetByName('BookingLedger')) {
    sheet.insertSheet('BookingLedger');
  }
  
  // 2. Register syncAllChannels to run every 30 minutes
  ScriptApp.newTrigger('syncAllChannels')
    .timeBased()
    .everyMinutes(30)
    .create();
  
  // 3. Register sendDailyReconciliation to run daily at 7:30am PT
  ScriptApp.newTrigger('sendDailyReconciliation')
    .timeBased()
    .atHour(7)
    .nearMinute(30)
    .inTimezone('America/Los_Angeles')
    .everyDays(1)
    .create();
}

The key decision here: two separate triggers instead of one. Why? syncAllChannels() needs to run frequently (every 30 minutes) to catch new Boatsetter, Airbnb, and other platform bookings. sendDailyReconciliation() should run once daily at 7:30am PT to email the reconciliation report to the ops team. Separating them prevents the heavy lifting of the sync from blocking the lightweight email send.

Infrastructure: Google Apps Script Project Configuration

The GAS project is bound to a Google Sheet (OPS_SHEET_ID) and uses the default GAS service account for API calls. The project configuration is stored in .clasp.json (not committed; exists locally in the project root).

Key resources:

  • GAS Script ID: 1HiEgjBrCGrnOIvr27nIk1E1qoR1pwKmWLigl191Tz0xq7LautJrIp9Ii
  • Bound Spreadsheet (ops sheet): Referenced via OPS_SHEET_ID constant (line 42 in CalendarSync.gs)
  • Trigger Storage: GAS stores trigger metadata in its own database; you view/delete via the clock icon in the left sidebar of the editor

The Boatsetter iCal URL is hardcoded in the ICAL_FEEDS array (line 58):

const ICAL_FEEDS = {
  'Boatsetter': 'https://ical.boatsetter.com/...',
  // Additional platforms added as integrations go live
};

Verification: The Test Execution

After running calendarSyncSetup(), verify that triggers were created by opening the clock icon in the left sidebar. You should see:

  • syncAllChannels — Time-based → Every 30 minutes
  • sendDailyReconciliation — Time-based → Every day (7:30am PT)

Then run testSync() again to exercise the entire sync pipeline. Watch the Execution Log for output like:

Info: Fetching Boatsetter iCal...
Info: Parsed 3 events from Boatsetter
Info: CalendarSync complete. New bookings: 3

If you see Exception: You do not have permission, OAuth re-auth didn't complete or a scope is still missing. Go back to step 1 and run testSync() again.

Separate Integration: Viator Email Scanner Setup

There is a second GAS project that handles Viator bookings via email scanning. This requires a separate setup call:

  • File: sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/JadaCalendarDashboard.gs
  • Function: jadaCalendarScanSetup() (line 364)
  • GAS editor URL: https://script.google.com/d/1dDpSK8JZda7XUpKIGlyyAX19KLL4JqFjYVtpcunB5ZE3