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.pypolls 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.pytransforms 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.gsis 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— readscampaign_schedule.jsonand 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.shthat 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.htmlhandles 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.pyto a scheduled Lambda function (CloudWatch Events, hourly trigger) - Validate that GetMyBoat and Boatsetter