Fixing Calendar Sync Triggers and OAuth Re-authorization in Google Apps Script
Overview
The Boatsetter iCal integration was written and wired into the codebase but non-functional because two critical setup steps were never executed: the time-based sync triggers were never activated, and the OAuth tokens for Gmail and Google Calendar had expired or been revoked. This post walks through the diagnosis, the fix sequence, and the infrastructure decisions that prevent this class of problem in the future.
What Was Done
Three blockers required remediation in sequence:
- Re-authorize OAuth scopes for Gmail and Google Calendar at the GAS project level
- Activate time-based triggers for the sync and reconciliation flows
- Verify end-to-end execution by running a test sync function
Technical Details
File Locations and Function Names
The primary work lives in one GAS project:
- File:
sites/queenofsandiego.com/CalendarSync.gs - GAS Editor URL:
https://script.google.com/d/1HiEgjBrCGrnOIvr27nIk1E1qoR1pwKmWLigl191Tz0xq7LautJrIp9Ii/edit - Setup function:
calendarSyncSetup()at line 355 - Test function:
testSync()at line 563
A separate GAS project handles email-based booking ingestion and requires its own trigger activation:
- File:
sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/JadaCalendarDashboard.gs - GAS Editor URL:
https://script.google.com/d/1dDpSK8JZda7XUpKIGlyyAX19KLL4JqFjYVtpcunB5ZE3-NMX_9v0lQJ5/edit - Setup function:
jadaCalendarScanSetup()at line 364
Step 1: Re-authorize OAuth Tokens
GAS maintains separate OAuth tokens for each API scope (Gmail, Calendar, Sheets, etc.). When tokens expire or are revoked, the GAS runtime will reject API calls with Exception: You do not have permission errors. The fix is to trigger a re-consent flow by executing any function that touches those scopes.
Execution sequence:
- Open the GAS editor for CalendarSync.gs (URL above)
- In the function dropdown menu, select
testSync - Click the Run button
- Google will prompt with a consent screen: "This app needs access to your Gmail, Calendar, and Sheets" — click Allow
- The OAuth flow may prompt multiple times (one per scope) — allow all of them
Why testSync first? The calendarSyncSetup() function only touches ScriptApp (for creating triggers) and SpreadsheetApp (for creating the BookingLedger tab). It won't trigger Calendar or Gmail OAuth prompts. Running testSync first exercises the full API surface — it fetches from the Boatsetter iCal URL, writes to Google Calendar, and sends reconciliation emails via Gmail. This guarantees the OAuth consent dialogs fire before we activate the time-based triggers.
Step 2: Activate Time-Based Triggers
Once OAuth is re-authorized, the setup function can safely create triggers:
- In the GAS editor, select
calendarSyncSetupfrom the function dropdown - Click Run
- Watch the Execution log for the success message
The expected output:
Notice Execution started
Info CalendarSync setup complete:
Info - BookingLedger tab created in ops sheet
Info - syncAllChannels: every 30 minutes
Info - sendDailyReconciliation: daily at 7:30am PT
Notice Execution completed
What this function does:
- Creates or idempotently returns the "BookingLedger" tab in the ops spreadsheet (source of truth for all bookings across platforms)
- Registers a
syncAllChannelstime-based trigger that fires every 30 minutes - Registers a
sendDailyReconciliationtime-based trigger that fires daily at 7:30 AM PT
To verify triggers were created, click the clock icon in the left sidebar of the GAS editor. You should see exactly two entries with the functions and schedules listed above.
Step 3: Verify End-to-End Sync
Run testSync again and monitor the Execution log. You should see output like:
Fetching Boatsetter iCal from: [URL masked]
Parsed 5 events from Boatsetter
Writing 3 new bookings to ops sheet BookingLedger tab
Writing 3 events to "Boatsetter" calendar
Sending daily reconciliation to operations@...
CalendarSync complete. New bookings: 3
Any error with You do not have permission indicates OAuth is still invalid and step 1 needs to be repeated.
Architecture and Design Decisions
Why GAS and Not a Standalone Service?
Google Apps Script is the integration point for this system because:
- It has native OAuth integration with Gmail and Google Calendar — no external token management required
- It executes within Google's ecosystem, so writes to Google Calendar are atomic and auditable
- Time-based triggers (via
ScriptApp.newTrigger()) are serverless — no Lambda, cron, or external scheduler to manage - It can read from external iCal feeds (Boatsetter, Sailo, Viator) via
UrlFetchApp
Boatsetter iCal Feed Integration
The iCal URL for Boatsetter is stored in the repo's repos.env file and referenced in CalendarSync.gs as part of the ICAL_FEEDS array. The sync function:
- Fetches the iCal file via HTTPS
- Parses events and extracts bookings (date, duration, rental asset, customer)
- Writes to the BookingLedger tab in the ops spreadsheet (canonical ledger)
- Creates corresponding events in the "Boatsetter" Google Calendar
This pattern scales: as other platforms (Sailo, Viator) go live, their iCal feeds are added to the array and