```html

Automating Boat Cleaning Dispatch and Calendar Synchronization Across Multiple Platforms

This session focused on solving a critical operational gap: when a primary service vendor (FancyHands) became unavailable, we needed a rapid pivot to an alternative boat cleaning platform with integrated calendar management. The challenge wasn't just finding a replacement service—it was building tooling to automate dispatch scheduling across GetMyBoat and Boatsetter platforms while keeping Google Calendar synchronized in real-time.

Problem Statement

The team was relying on FancyHands for boat cleaning coordination. When that became unavailable, we faced a manual dispatch bottleneck: cleaning requests would come in through multiple channels (email, phone, platform inboxes) but had no automated way to:

  • Scrape cleaning requests from platform inboxes (GetMyBoat, Boatsetter)
  • Create dispatch tasks with consistent metadata
  • Sync those tasks to Google Calendar for visibility across team calendars
  • Maintain calendar state during polling cycles without duplicates

Manual tracking meant delays, missed appointments, and no single source of truth for cleaning schedules.

Technical Architecture

Dispatch Pipeline Components

We built a three-layer solution:

  • Layer 1: Inbox Scraper/Users/cb/Documents/repos/tools/platform_inbox_scraper.py polls GetMyBoat and Boatsetter APIs for new messages containing cleaning requests. The scraper extracts structured data: boat ID, requested date, customer contact info, and cleaning type (interior, exterior, full).
  • Layer 2: Dispatch Coordinator/Users/cb/Documents/repos/tools/dispatch_boat_cleaner.py transforms inbox scrapes into dispatch tasks. It validates against existing calendar holds (to prevent double-booking), assigns priority based on boat class and customer history, and writes tasks to a job queue.
  • Layer 3: Calendar Sync/Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/CalendarSync.gs is a Google Apps Script that receives dispatch tasks via webhook and creates calendar events, maintaining a mapping of event IDs to prevent duplication across polling cycles.

Campaign Scheduler for Bulk Operations

We also created a campaign scheduling system for bulk communications (e.g., sending cleaning confirmations to multiple boat owners). This consists of:

  • /Users/cb/Documents/repos/tools/campaign_scheduler.py — reads campaign_schedule.json and triggers batch email sends via Amazon SES
  • HTML templates stored in /Users/cb/Documents/repos/tools/templates/ (e.g., rady_shell_blast1.html, rady_shell_blast2.html) with customer-specific variable interpolation
  • Deployment script at /Users/cb/Documents/repos/tools/deploy_campaign_scheduler.sh that validates template syntax and pushes to Lambda for scheduled execution

Infrastructure Details

AWS Lambda Integration

The calendar sync endpoint runs on AWS Lambda behind API Gateway. The Lambda function exposes multiple actions via query parameter routing:

GET /calendar-api?action=add-calendar-event
POST /calendar-api?action=add-calendar-event
<payload> {
  "title": "Boat Cleaning - Vessel X",
  "startTime": "2024-04-28T09:00:00Z",
  "endTime": "2024-04-28T11:00:00Z",
  "description": "Interior + exterior cleaning",
  "calendarId": "primary"
}

The Lambda function invokes Google Calendar API v3 using OAuth2 credentials stored in the function's environment. During this session, we identified the action names available in the deployed function and tested the add-calendar-event action with batch requests (7 Sea Scout Wednesday holds were successfully added to validate the pipeline).

Google Apps Script Deployment

The CalendarSync.gs file underwent multiple iterations to handle edge cases:

  • Idempotency: We added an event ID cache to prevent duplicate calendar entries when the polling interval triggers multiple syncs. Events are keyed by boat ID + date + cleaning type.
  • Error Handling: Added retry logic with exponential backoff for transient API failures. Failed events are logged to a separate spreadsheet for manual review.
  • Polling Frequency: Updated the time-based trigger from checking every 15 minutes to every hour to reduce API quota consumption while maintaining responsiveness.

The updated CalendarSync.gs was pushed to the Google Apps Script project using clasp push after confirming the project ID from the .clasp.json` mapping file.

Amazon SES for Dispatch Notifications

All dispatch confirmations and bulk communications are sent via Amazon SES. Email templates are stored in version control at predictable paths:

/Users/cb/Documents/repos/tools/templates/rady_shell_blast1.html
/Users/cb/Documents/repos/tools/templates/rady_shell_blast2.html
/Users/cb/Documents/repos/sites/quickdumpnow.com/marketing/templates/qdn_blast1.html

Templates use Jinja2 syntax for variable interpolation (customer name, boat name, cleaning date, etc.). The campaign scheduler validates template rendering before queuing emails to SES.

Site-Specific Deployment

Unsubscribe pages were created for compliance with SES sending policies:

  • /Users/cb/Documents/repos/sites/quickdumpnow.com/unsubscribe/index.html handles opt-out requests for the QDN property
  • The carole.dangerouscentaur.com unsubscribe logic was integrated into existing site infrastructure

These pages handle GET requests with email address parameters, validate the sender against allowed domains, and update a DynamoDB suppression list that the campaign scheduler checks before sending.

Key Technical Decisions

Why Lambda + Google Apps Script instead of a single service? Google Apps Script provides native Calendar API access without credential rotation concerns, but it lacks scalability for high-volume inbox scraping. By separating concerns—Apps Script for calendar writes, Lambda for polling and dispatch logic—we get the best of both: native GCal integration plus horizontal scalability.

Why Jinja2 templates instead of hard-coded HTML? As the number of campaigns grows, template reuse becomes critical. Jinja2 is standard in Python tooling and easy to version-control. It also allows non-engineers to modify email content without touching code.

Why hourly polling instead of real-time webhooks? GetMyBoat and Boatsetter don't offer reliable webhook APIs for message ingestion. Hourly polling is a pragmatic tradeoff: it's within API rate limits, introduces minimal latency (most boat owners book 24+ hours in advance), and is easier to debug than webhook delivery failures.

What's Next

The immediate rollout plan:

  • Deploy platform_inbox_scraper.py to a scheduled Lambda function (CloudWatch Events, hourly trigger)
  • Validate that GetMyBoat and Boatsetter