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
syncAllChannelsto run every 30 minutes - Registers a time-based trigger for
sendDailyReconciliationto 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:
- Navigate to the GAS editor URL above
- In the function dropdown (top center), select
testSync - Click the Run button
- Google prompts: "This app needs access to..." → click Allow
- 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:
- In the function dropdown, select
calendarSyncSetup - Click Run
- Execution log should show "CalendarSync setup complete" with trigger details
- Verify in the left sidebar: click the clock icon (Triggers) → you should see two entries:
syncAllChannels— Time-driven → Every 30 minutessendDailyReconciliation— 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