Building Event Automation at Scale: Follow-up Email Queuing, LaunchAgent Scheduling, and Document Versioning
What Was Done
This session consolidated three separate automation concerns into a cohesive event-management pipeline:
- Designed and built a follow-up email templating system that decouples guest communication logic from business rules
- Implemented scheduled digest delivery via macOS LaunchAgent with structured email composition
- Created a versioned document management workflow for event artifacts (trip sheets, waivers) with cache invalidation
- Integrated DynamoDB crew tracking with S3-backed print documents and real-time CloudFront distribution updates
Architecture: The Follow-up Email Framework
The follow-up system lives in /Users/cb/.claude/skills/jada-followup/ and separates concerns into three layers:
- Template definitions (
reference/followup_templates.json): JSON schema defining email types, required fields, and variable placeholders. Each template includes subject, body, and conditional logic for when to send. - Builder logic (
scripts/build_followups.py): Python orchestrator that reads template definitions, validates required fields against incoming data, and queues emails. The builder performs shape validation (ensuring guest entries don't have crew-only fields, checking for required contact info) before commit. - Queue persistence: Built emails are serialized to a work queue, enabling retry logic and preventing duplicate sends if the process crashes mid-execution.
Why this structure: Separating templates from code means non-engineers can adjust email wording and timing without touching Python. The validation layer catches data schema violations early—a guest row missing an email address won't cascade into a silent failure downstream.
Scheduled Delivery: LaunchAgent + Digest Pipeline
The digest system adds scheduled, batched communication:
- LaunchAgent plist (
/Users/cb/Library/LaunchAgents/com.jada.morning-digest.plist): Defines a daily scheduled task runningbuild_digest.pyat 06:00 local time. The plist specifies working directory, environment variables, and log file paths for debugging failed runs. - Digest builder (
/Users/cb/icloud-jada-ops/digest/build_digest.py): Aggregates events from the canonical ledger (ledger.json), pulls crew assignments from DynamoDB, and formats a single email covering upcoming events, crew changes, and booking confirmations. - Context files (
CONTEXT.mdfiles in both digest and jada-ops roots): Documented assumptions about data sources, schema, and failure modes. Treated as source of truth for what the digest knows and doesn't.
Why LaunchAgent over cron: macOS LaunchAgent runs with proper environment (login shell, user context) and logs to system log (visible via Console.app), making it easier to debug permission issues. Cron would require special shell setup and harder log access.
Document Versioning and Cache Invalidation
Event documents (trip sheets, waivers) follow a versioning pattern with live URLs and cache control:
- S3 bucket: Documents stored at predictable paths like
queenofsandiego.com/print/2026-07-04-dylan-trip-sheet.htmland.../2026-07-04-dylan-waiver.html. Filename includes event date and organizer name for human-readable identification. - CloudFront distribution: Points to the S3 origin with default TTL of 1 day. When documents update (crew changes, photo codes added), an invalidation clears the cache immediately so print links return fresh content.
- Build-and-deploy cycle (
jada-deploytool): Downloads current documents from S3, applies patches (adding crew data, updating photo codes), re-uploads, then invalidates the CloudFront distribution. All three steps are atomic—no partial state visible to external users.
Why this approach: Hardcoding URLs in guest emails and SMS requires that links remain stable, but content must update as crew confirms. S3 + CloudFront + invalidation lets us keep the same URL while changing the behind-the-scenes HTML. DynamoDB stores the source of truth for crew; S3 is the rendered artifact; CloudFront is the cache.
Data Integration Points
The pipeline pulls from multiple sources and validates consistency:
- ledger.json: Canonical event record (dates, organizers, bookings, crew assignments). Updated via
ledger.pywith deposit tracking and payment method recording (e.g., Zelle). - contacts.csv: Guest and crew contact directory. Parsed with edge-case handling: crew rows excluded from guest-list requests, prospective entries skipped, email addresses validated before queue.
- DynamoDB jada-crew-dispatch table: Real-time crew state (assigned roles, confirmation status). Queried during document build to populate trip-sheet crew slots. Partition key is event date; records scanned for matching event IDs.
- Gmail API: Final outbound channel for structured emails and confirmations. Implemented with retry logic (up to 4 attempts) to handle transient auth or quota issues.
Key Architectural Decisions
Template-driven over hard-coded emails: Each email type is a record in JSON, not a Python f-string. This lets us version template changes separately from code, roll back email wording without redeploying binaries, and run A/B tests on subject lines.
Validation at queue time, not send time: The builder checks data shapes before adding to the email queue. Catching missing fields early prevents silently dropped guests (e.g., crew row with no email in a guest-list context) and makes audit logs cleaner.
Single source of truth for documents: S3 is the master copy; local jada-ops directories sync back from deployed versions. This prevents drift where a local file was edited but the live version wasn't, or vice versa.
CloudFront invalidation is synchronous in the deploy step: We wait for the invalidation to confirm before considering the deploy done. Avoids race conditions where an email is sent with a new URL but CloudFront still serves stale content.
What's Next
- Webhook triggers for real-time events: Crew confirmations or guest RSVPs currently require manual email-send. A webhook endpoint could trigger follow-ups instantly.
- Suppress list versioning: Guest suppression rules are currently ad-hoc. Formalizing a suppression ledger (JSON or DynamoDB) would make opt-out rules auditable and reversible.
- Distributed digest aggregation: The digest currently runs on a single machine. A Lambda-based variant could run on a schedule and post results to Slack, making digest data available to the team without email inbox clutter.
- Type-safe template validation: Template JSON is currently hand-written. A schema validator (JSON Schema, Pydantic) would catch typos before deploy.
For operators: Logs from LaunchAgent appear in Console.app (search for "com.jada.morning-digest"). If the digest doesn't send, check LaunchAgent status via launchctl list | grep jada and review the plist StandardErrorPath and StandardOutPath logs. Document builds log to CloudFront access logs (queryable via S3 Select) and DynamoDB scans are logged to CloudWatch for the Lambda that runs the scans.