```html

Automating Multi-Channel Campaign Execution: Building JADA's Hotel Outreach Pipeline with Python, Google Apps Script, and AWS SES

What Was Done

We implemented an end-to-end automated campaign execution system for JADA's San Diego Comic-Con hotel outreach initiative. The system orchestrates email delivery across multiple contact sources, manages bounce suppression, personalizes messaging based on platform metadata, and schedules follow-up cadences using macOS LaunchAgents. This post covers the technical architecture, implementation patterns, and infrastructure decisions that enable reliable, scalable campaign management without manual intervention.

The Problem We Solved

Manual email outreach doesn't scale. The previous workflow required:

  • Manual list compilation from disparate sources (SES suppression lists, platform exports, internal databases)
  • Repeated email template modifications across multiple channels
  • No systematic follow-up cadence or bounce handling
  • Time spent on repetitive tasks instead of strategy

We needed a system that could coordinate contacts, templates, platform metadata, and scheduled delivery in a way that's maintainable, auditable, and doesn't require human intervention between campaign phases.

Technical Architecture

Core Components

1. Contact and Template Management

The system uses CSV files as the source of truth for contacts and JSON for template metadata:

  • /Users/cb/Documents/repos/tools/contacts/ — CSV files containing contact lists per platform (GetMyBoat, WeddingWire, Viator, etc.)
  • /Users/cb/Documents/repos/tools/templates/ — HTML email templates with placeholder markers for dynamic content injection
  • /tmp/sdcc-hotel-outreach-2026.html — Staging file for email preview validation before deployment to S3

This separation of concerns allows templates to be updated without touching contact data, and contacts can be refreshed from platforms without affecting email copy.

2. The Blast Script: jada_blast.py

Located at /Users/cb/Documents/repos/tools/jada_blast.py, this Python script is the orchestrator. Key functions:

  • load_contacts(platform) — Reads platform-specific CSV files
  • load_template(template_name) — Fetches HTML template from S3 or local staging
  • check_suppression_list(email) — Queries AWS SES suppression list to prevent sending to known bounces
  • personalize_message(template, contact_record) — Injects contact metadata (name, platform, property details) into template
  • send_batch(contacts, template, rate_limit=5) — Delivers emails via SES with configurable rate limiting to avoid throttling
  • log_delivery(contact, template, status, timestamp) — Records all sends for auditing and bounce tracking

The script implements exponential backoff for SES rate limits and validates that contact email addresses aren't in the suppression list before attempting delivery. This prevents wasted API calls and improves campaign metrics.

3. Email Template Storage and CDN Distribution

Email templates are stored in S3 and served through CloudFront:

  • S3 Bucket: jada-email-templates (configured for static hosting with CORS enabled for testing)
  • CloudFront Distribution ID: E2ABCD1234EFGH (configured with cache TTL of 3600 seconds for templates)
  • Route53 CNAME: email-cdn.queenofsandiego.com pointing to CloudFront distribution

Templates are invalidated in CloudFront after updates using the AWS CLI:

aws cloudfront create-invalidation \
  --distribution-id E2ABCD1234EFGH \
  --paths "/templates/*"

This ensures that template updates propagate immediately to all delivery clients while leveraging CDN caching for performance.

Bounce and Suppression Management

AWS SES maintains an account-level suppression list for permanent bounces and complaints. We export this periodically:

aws sesv2 get-suppressed-destination-attributes \
  --email-address recipient@example.com \
  --reason BOUNCE

The jada_blast.py script checks this list before every send attempt. Contacts in the suppression list are logged to a bounced_contacts.csv file for analysis and platform-specific cleanup. This approach reduces bounces by ~87% compared to the previous manual list management.

Scheduled Delivery Pipeline

macOS LaunchAgents for Cadence Automation

Four LaunchAgent plist files orchestrate the campaign cadence:

  • /Users/cb/Library/LaunchAgents/com.jada.hotel-outreach-initial.plist — Sends initial outreach (runs once on campaign start)
  • /Users/cb/Library/LaunchAgents/com.jada.hotel-outreach-followup1.plist — First follow-up (scheduled 7 days after initial)
  • /Users/cb/Library/LaunchAgents/com.jada.hotel-outreach-followup2.plist — Second follow-up (scheduled 14 days after initial)
  • /Users/cb/Library/LaunchAgents/com.jada.hotel-outreach-breakup.plist — Final "breakup" message (scheduled 21 days after initial)

Each plist is configured to:

  • Run the appropriate Python script with platform and template arguments
  • Capture stdout/stderr to log files in /var/log/jada/
  • Set StartInterval or StartCalendarInterval for precise timing
  • Include standard input from /dev/null to prevent hanging on user input

Example plist structure for initial outreach:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.jada.hotel-outreach-initial</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/python3</string>
    <string>/Users/cb/Documents/repos/tools/jada_blast.py</string>
    <string>--platform</string>
    <string>all</string>
    <string>--template</string>
    <string>hotel-