Building an Attended SMS Confirmation System for Charter Lead Management: Architecture, Tooling, and Safety by Design
This post documents the technical foundation we've built for managing warm leads and charter confirmations at scale, with a focus on the architectural decision to favor attended, human-gated SMS sends over fully automated outbound messaging. The reasoning here matters as much as the implementation—it's a pattern that applies well beyond this single use case.
What Was Done
We established a charter lead confirmation workflow that:
- Monitors incoming messages on GetMyBoat via Playwright browser automation
- Drafts SMS acknowledgments using cached charter and proposal data
- Gates every outbound SMS through human review before send (no unattended automation)
- Respects quiet hours (no SMS between 17:00–08:00 local time)
- Maintains a local cache of charter roles, dates, and call times to avoid re-fetching during send
- Uses the native
send-smscommand-line tool for message delivery
The system is not a fire-and-forget automation. It's a human-in-the-loop confirmation engine where the AI drafts, the operator reviews, and the operator sends. This is intentional.
Technical Details: The Architecture
Layer 1: Data Sources and State
Lead and charter data lives in three places:
- DynamoDB (production): Charter metadata, crew roster, revenue records. Queried at session start to populate cache.
- iCloud jada-ops/ directory: Proposals in flight (Dylan, Noelle, Molly charters). Kept locally for fast lookup during draft.
- GetMyBoat web interface: Live incoming messages. Scraped via Playwright when the monitor runs.
Rather than building a complex sync mechanism, we cache aggressively at session start. This trades freshness for simplicity: during a working session, the cache is good enough; at session end, it flushes. A new session means a fresh cache pull from authoritative sources.
Layer 2: Playwright-Based Message Monitoring
GetMyBoat's notification settings were reconfigured to route notifications to jadasailing@gmail.com. Playwright handles the browser automation:
# Pseudo-command flow (actual implementation is session-specific)
playwright launch-browser \
--headless false \
--profile /Users/cb/.playwright/getmyboat-profile
# Navigate to GetMyBoat messages
goto https://www.getmyboat.com/messages
# Wait for new message badge
wait-for-selector .message-unread
# Extract sender, timestamp, message body
extract-message-data > /tmp/warm-lead.json
The Playwright profile includes saved authentication for GetMyBoat. We verified credentials exist in creds.txt (labels logged, values masked) before starting any session work.
Why Playwright instead of polling an API? GetMyBoat's public API is rate-limited and doesn't expose message webhooks. Browser automation is slower but reliable and doesn't require API access negotiation.
Layer 3: Proposal Synthesis and Draft Generation
When a warm lead arrives, the system:
- Identifies the charter type from the message context
- Looks up prior proposals from
~/jada-ops/proposals/(Dylan, Noelle, Molly examples) - Extracts: pricing structure, included amenities, call-time language, cancellation terms
- Synthesizes a new draft proposal tailored to the new lead
- Generates an SMS acknowledgment text that references the upcoming proposal
This synthesis happens in-session, synchronously. We do not write files to disk until you explicitly approve each draft.
Layer 4: SMS Gating and Quiet Hours
The send-sms tool is available and executable. Before any SMS leaves, we check:
# Pseudo-validation
current_time=$(date +%H:%M)
if is_between($current_time, 17:00, 08:00):
print "SMS blocked: quiet hours (17:00–08:00). Queue for morning send."
return
if not operator_approval_received:
print "SMS blocked: awaiting one-tap send confirmation from operator."
return
send_sms --to $recipient --message $draft_text
This is not a technical limitation; it's an operational rule enforced in session. If you close the terminal, the rule doesn't carry forward. A new session requires a fresh human decision.
Infrastructure and File Layout
On your local Mac (/Users/cb/):
~/.claude/projects/-Users-cb/memory/jada-getmyboat-lead-automation.md— Session memory. Tracks which leads have been acknowledged, cached charter data, prior proposal paths.~/jada-ops/(iCloud sync) — Live proposals for Dylan, Noelle, Molly charters. Used as templates.~/.playwright/getmyboat-profile/— Authenticated browser profile. Persisted across sessions.creds.txt— Labels for GMB login credentials (values masked in logs).
On EC2 (for context, not actively used in this flow):
~/repos/jada-heritage-research.md— Long-form narrative of the business; not queried during confirmations.- DynamoDB table (Charter metadata, crew roster) — Queried once per session start to populate cache.
Key Decisions: Why This Design?
Attended vs. Fully Automated
We chose attended send (draft → your review → one-tap send) over fully automated unattended send (fire overnight, you review in the morning if something goes wrong). Here's why:
- SMS is irreversible. An email can be unsent or corrected; a text to a real person's phone is permanent. Wrong tone, wrong date, wrong name—it's now a reputation hit.
- Speed is the same for you. Morning-you says "send Travis's confirmation," I hand you the exact text, you tap send. Same outcome as a scheduled agent, but with your eyes on it.
- Context matters. You might know Travis got a better offer, or the date changed overnight. A human glance catches that; an unattended scheduler doesn't.
- Scope creep risk is lower. Fully automated systems tempt expansion ("why not auto-draft the proposal too? why not auto-send it?"). Attended systems have a natural boundary: you're in the loop, so you see when complexity is growing.
Session-Based State, Not Persistent Agents
This system does not run a background daemon. When you close the terminal, nothing persists. When you open a new terminal tomorrow, you run start the crew monitor and we rebuild state from scratch.
This is safer. A