Reactivating Boatsetter Calendar Sync: Resolving OAuth Expiry and Missing Trigger Setup in Google Apps Script

The Boatsetter booking integration was fully implemented but silently broken in production — the iCal feed was wired into CalendarSync.gs, but the automation trigger was never activated, and OAuth tokens had expired. This post walks through the diagnosis, the three-step fix sequence, and why this failure mode is worth documenting for future integrations.

The Problem: Three Interdependent Blockers

Ticket m-91325edb was marked "needs-you" because the Boatsetter sync required manual intervention at multiple layers:

  • Missing trigger setup: calendarSyncSetup() in CalendarSync.gs had never been executed, so the time-based triggers that should poll Boatsetter's iCal feed every 30 minutes were never created.
  • Expired Gmail OAuth: Ticket t-8d86d5ba indicated the OAuth token used by GAS for Gmail scope had expired, preventing the script from sending reconciliation emails even if sync data existed.
  • Revoked Calendar OAuth: The Calendar API write permission had been revoked (likely during a security audit or account maintenance), blocking all CalendarApp calls that insert Boatsetter events.

Any one of these would fail the sync; all three being present meant the integration was completely non-functional despite being code-complete.

Step 1: Re-authorizing GAS and Re-consenting OAuth Scopes

Google Apps Script maintains OAuth consent at the project level, not the individual script. When you execute any function in the GAS editor that touches a protected API (Gmail, Calendar, Sheets, etc.), the first execution triggers Google's consent dialog.

Why this works: The GAS runtime binds the script execution to your Google account and prompts re-consent for any scopes the script declares. This is distinct from standalone OAuth tokens — it's part of GAS's built-in OAuth flow.

Implementation steps:

  1. Navigate to the CalendarSync.gs project: https://script.google.com/d/1HiEgjBrCGrnOIvr27nIk1E1qoR1pwKmWLigl191Tz0xq7LautJrIp9Ii/edit
  2. In the function dropdown menu (top center), select testSync — a lightweight read-only function that validates both Gmail and Calendar scopes without mutating data.
  3. Click Run
  4. Google displays an authorization prompt: "This app needs access to your Google account. Review the permissions requested."
  5. Click Allow for the first scope (Gmail: send emails, read draft metadata).
  6. A second prompt may appear for Calendar scope (read/write calendar events). Click Allow again.
  7. Check the Execution Log (bottom of editor) for any errors. You should see clean output with no permission exceptions.

This single operation re-establishes trust between the GAS project and your Google account for both Gmail and Calendar APIs, clearing both t-8d86d5ba and the revoked Calendar OAuth issue.

Step 2: Activating Time-Based Triggers via calendarSyncSetup()

The ticket originally referenced calendarDashboardSetup(), but the actual function name in CalendarSync.gs is calendarSyncSetup(). Both exist in the Jada ecosystem, but they serve different purposes — the naming matters because running the wrong one won't create the Boatsetter sync triggers.

What calendarSyncSetup() does:

// Pseudo-code structure (actual implementation in CalendarSync.gs)
function calendarSyncSetup() {
  // Delete any existing triggers to avoid duplicates
  deleteExistingTriggers_();
  
  // Create time-based trigger: syncAllChannels every 30 minutes
  ScriptApp.newTrigger('syncAllChannels')
    .timeBased()
    .everyMinutes(30)
    .create();
  
  // Create daily trigger: sendDailyReconciliation at 7:30am PT
  ScriptApp.newTrigger('sendDailyReconciliation')
    .timeBased()
    .atHour(7)
    .atMinute(30)
    .onWeekDay(ScriptApp.WeekDay.MONDAY, ScriptApp.WeekDay.TUESDAY, ...)
    .create();
}

Why idempotency matters: If you run calendarSyncSetup() multiple times, you'll create duplicate triggers. The function handles this by deleting existing triggers before creating new ones, but it's still good to verify.

Activation steps:

  1. In the same GAS editor, function dropdown → select calendarSyncSetup
  2. Click Run
  3. Watch the Execution Log for completion (should be instantaneous)
  4. Verify trigger creation by clicking the clock icon (⏰) in the left sidebar under Triggers
  5. You should see two new entries:
    • syncAllChannels — Time-based, every 30 minutes
    • sendDailyReconciliation — Time-based, daily at 7:30 AM (America/Los_Angeles)

These triggers now run automatically without further intervention. The Boatsetter iCal feed will be polled every 30 minutes via the syncAllChannels function, which internally calls the Boatsetter-specific sync logic.

Step 3: Verification and Validation

After both OAuth re-consent and trigger activation, validate the entire pipeline:

  1. Function dropdown → select testSync
  2. Click Run
  3. Examine the Execution Log for the following successful output pattern:
Fetching Boatsetter iCal...
  [N] events from Boatsetter
CalendarSync complete. New bookings: [M]
Updating calendar entries...
✓ Calendar events synchronized
✓ Reconciliation email sent

What each line means:

  • Fetching Boatsetter iCal... — The script successfully retrieved the iCal feed from Boatsetter's public URL (hardcoded in CalendarSync.gs). No network errors or 403/404 responses.
  • [N] events from Boatsetter — Parsed N events from the iCal format. If 0, either Boatsetter has no upcoming bookings or the feed URL is stale.
  • New bookings: [M] — M of those N events were new (not previously in the Jada calendar). This is the reconciliation count.
  • Calendar events synchronized — The script successfully wrote to Google Calendar API. No permission errors (which would show as Exception: You do not have permission to perform this action).
  • Reconciliation email sent — The Gmail scope