```html

Fixing Boatsetter Auto-Sync: Resolving OAuth Expiration and Trigger Activation in Google Apps Script

What Was Done

The Boatsetter booking platform had been wired into our calendar sync pipeline for weeks, but auto-sync never activated. Three blockers prevented it from working: (1) the time-based triggers were never created, (2) Gmail OAuth credentials expired, and (3) Calendar API permissions were revoked. This post walks through the diagnosis and fix sequence, with exact file paths and function names to avoid the "which file?" problem that slowed us down.

The Architecture: Where Boatsetter Lives

Calendar sync lives in a Google Apps Script project deployed to manage multiple booking platforms. The project structure is:

  • Primary GAS Project ID: 1HiEgjBrCGrnOIvr27nIk1E1qoR1pwKmWLigl191Tz0xq7LautJrIp9Ii
  • File path (repo): sites/queenofsandiego.com/CalendarSync.gs
  • GAS Editor URL: https://script.google.com/d/1HiEgjBrCGrnOIvr27nIk1E1qoR1pwKmWLigl191Tz0xq7LautJrIp9Ii/edit

The Boatsetter iCal feed URL is hardcoded in the ICAL_FEEDS array (around line 45) and referenced by the main sync function syncAllChannels() at line 287. This function fetches events from all configured iCal sources, parses them, and writes booking entries to the BookingLedger tab in the ops spreadsheet.

Technical Details: The Three Blockers

Blocker 1: Missing Time-Based Triggers

Google Apps Script uses ScriptApp.newTrigger() to schedule recurring functions. The ticket noted that calendarDashboardSetup() needed to run, but the actual function name in CalendarSync.gs is calendarSyncSetup() (line 355). This function does the following:

  • Creates the BookingLedger tab if missing (SpreadsheetApp scope only)
  • Registers a time-based trigger for syncAllChannels to run every 30 minutes
  • Registers a time-based trigger for sendDailyReconciliation to run daily at 7:30 AM PT

Without running this setup function, no triggers existed in the GAS project, so even valid credentials wouldn't help—nothing was scheduled to execute.

Blocker 2: Expired Gmail OAuth Token

The sendDailyReconciliation() function (line 495) sends a summary email via Gmail. GAS OAuth tokens have a lifecycle: they're refreshed automatically on function execution, but if a project hasn't executed any Gmail-scoped function in a long time, the refresh token may expire. The execution log showed "Exception: You do not have permission to access Gmail," confirming this.

Blocker 3: Revoked Calendar API Permissions

The syncAllChannels() function calls CalendarApp.getDefaultCalendar().createEvent() to write parsed bookings to the primary calendar. If the user revokes Calendar API access in their Google account, GAS won't be able to re-request permission automatically—it requires an explicit re-consent flow.

The Fix Sequence

Step 1: Trigger Re-Authorization

Open the GAS editor and run a function that touches both Gmail and Calendar scopes. The testSync() function (line 563) is ideal:


function testSync() {
  var events = fetchIcalEvents("boatsetter"); // triggers Calendar API scope
  sendTestEmail("Test sync executed"); // triggers Gmail scope
  Logger.log("OAuth re-auth complete");
}

Steps to execute:

  1. Navigate to the GAS editor URL above
  2. In the function dropdown (top center), select testSync
  3. Click the Run button
  4. Google prompts: "This app needs access to..." → click Allow
  5. If multiple scopes are missing, you'll see the consent dialog twice (Gmail first, then Calendar)

Watch the Execution log panel at the bottom. If you see warnings like "Authorization required," the re-auth worked—you'll see them because the test is actually running now with fresh tokens.

Step 2: Activate the Triggers

Now that OAuth is refreshed, run the setup function:


function calendarSyncSetup() {
  // Create BookingLedger sheet
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  if (!ss.getSheetByName("BookingLedger")) {
    ss.insertSheet("BookingLedger");
  }
  
  // Register triggers
  ScriptApp.newTrigger("syncAllChannels")
    .timeBased()
    .everyMinutes(30)
    .create();
  
  ScriptApp.newTrigger("sendDailyReconciliation")
    .timeBased()
    .atHour(7)
    .everyDays(1)
    .inTimezone("America/Los_Angeles")
    .create();
  
  Logger.log("CalendarSync setup complete...");
}

Steps:

  1. In the function dropdown, select calendarSyncSetup
  2. Click Run
  3. Execution log should show "CalendarSync setup complete" with trigger details
  4. Verify in the left sidebar: click the clock icon (Triggers) → you should see two entries:
    • syncAllChannels — Time-driven → Every 30 minutes
    • sendDailyReconciliation — Time-driven → Daily 7:30 AM PT

Step 3: Verify End-to-End

Run testSync() one more time to exercise the full pipeline:


// Execution log should show:
// INFO    Fetching Boatsetter iCal...
// INFO    Parsed 5 events from Boatsetter
// INFO    Writing to BookingLedger...
// INFO    Calendar sync complete. New bookings: 5
// INFO    Sending daily reconciliation email...

If you see no permission errors, the fix is complete. If you still see auth failures, check the error type—it will tell you which scope didn't re-auth properly.

Why This Approach

Why re-auth before triggering setup? The calendarSyncSetup() function only touches SpreadsheetApp and ScriptApp (creating a sheet and registering triggers). It doesn't invoke Gmail or Calendar APIs, so it wouldn't trigger the OAuth consent flow. By running testSync() first, we ensure all scopes refresh before the triggers are created