Implementing Autonomous Email Campaign Management with Scheduled Blast Workflows
Over the past development session, we refactored the JADA email marketing infrastructure to support autonomous, scheduled campaign delivery with built-in suppression list management and multi-platform outreach orchestration. This post details the technical implementation, infrastructure decisions, and architectural patterns that enable reliable, hands-off campaign execution across multiple contact sources.
The Problem: Manual Campaign Bottlenecks
Previously, email campaigns required manual intervention at multiple stages: preparing contact lists, managing bounce/complaint suppression, coordinating delivery timing, and monitoring delivery status across disparate platforms. This created operational friction and missed opportunities for time-sensitive outreach (like San Diego Comic-Con hotel coordination).
The solution required three integrated components: a refactored blast script with suppression awareness, scheduled launch agents for cadence-based delivery, and a unified dashboard for tracking campaign state.
Technical Architecture
Core Components
- jada_blast.py (
/Users/cb/Documents/repos/tools/jada_blast.py): The primary campaign delivery engine, refactored to accept suppression lists and implement widening-gap cadence scheduling - send_hotel_outreach.py (
/Users/cb/Documents/repos/tools/send_hotel_outreach.py): Campaign-specific wrapper that manages contact deduplication, template rendering, and SES delivery coordination - LaunchAgent plist files (
/Users/cb/Library/LaunchAgents/): Four scheduled jobs managing initial outreach and three follow-up waves - SES Suppression List Integration: Direct querying of AWS SES bounce/complaint lists to prevent sending to invalid addresses
- Dashboard Integration: Real-time campaign state tracking via
update_dashboard.pycommand execution
Suppression List Management
The blast script now queries AWS SES's suppression list before sending to any contact:
boto3_client = boto3.client('sesv2', region_name='us-west-2')
suppression_list = boto3_client.list_suppressed_destinations(
Reason='BOUNCE' | 'COMPLAINT',
PageSize=100
)
# Filter contacts against suppression_list before sending
This prevents bounces that trigger SES complaint metrics and keeps the sending reputation clean. Suppression data is exported nightly and merged into the contact deduplication logic in send_hotel_outreach.py.
Widening-Gap Cadence Pattern
Rather than blast-and-hope, campaigns now follow a widening-gap delivery schedule:
- Wave 1 (Initial): Day 0, full contact list
- Wave 2 (Follow-up 1): Day 3, non-responders only (filtered via tracking pixel or reply detection)
- Wave 3 (Follow-up 2): Day 7, secondary non-responders
- Wave 4 (Breakup): Day 14, final attempt, then mark as unengaged
Each wave is independent—contacts are removed from subsequent waves if they've engaged. This reduces unsubscribe fatigue while maintaining campaign cadence.
LaunchAgent Scheduling
Four plist files in /Users/cb/Library/LaunchAgents/ define the scheduled execution pattern:
com.jada.hotel-outreach-initial.plist # Day 0, 09:00 UTC
com.jada.hotel-outreach-followup1.plist # Day 3, 14:00 UTC
com.jada.hotel-outreach-followup2.plist # Day 7, 10:00 UTC
com.jada.hotel-outreach-breakup.plist # Day 14, 16:00 UTC
Each plist follows standard macOS launch agent format, executing the wrapper script with campaign-specific parameters. The timing is staggered to avoid SES rate-limiting and to give recipient inboxes time to settle between waves.
Infrastructure & Deployment
Email Template Storage & CDN
Campaign HTML templates are versioned in the repository at /tmp/sdcc-hotel-outreach-2026.html, then uploaded to S3 for external reference:
- S3 Bucket: Campaign assets stored with version control
- CloudFront Distribution: Edge-cached delivery with cache invalidation after template updates
- Inline Images: All marketing images embedded via Base64 or hosted on S3 with CloudFront CDN for consistent delivery
Template updates trigger CloudFront invalidation to ensure all recipients see the latest version, even if initial sends encounter delays.
Contact Data Pipeline
Contact sources are merged and deduplicated before sending:
- Google Sheets (via Apps Script in
CrewDispatch.gs,FuneralOutreach.gs) - Platform APIs (GetMyBoat, WeddingWire, Viator integration in progress)
- Manual lists imported via CSV
- Suppression list filtering (SES bounces/complaints)
The send_hotel_outreach.py script orchestrates this pipeline, ensuring no duplicate sends and respecting suppression constraints.
Key Technical Decisions
Why LaunchAgent Over Cron?
macOS LaunchAgent provides:
- Native process management and restart on failure
- Standardized logging to system log (queryable via
log stream) - Automatic dependency tracking (won't run if system is asleep, queues if unavailable)
- Cleaner integration with Python virtual environments and environment variable injection
Cron would require external monitoring; LaunchAgent is built for this use case.
SES Over Third-Party ESP
AWS SES was chosen for:
- Direct suppression list control (no external system to sync)
- Cost efficiency at scale (contact-driven pricing, not per-email)
- Custom headers for campaign tracking (UTM parameters, list IDs)
- Bounce/complaint webhooks for real-time feedback loops
Suppression-First Design
Rather than reactive bounce handling, the system proactively filters contacts against SES suppression data before sending. This prevents:
- Hard bounces that degrade sender reputation
- Repeated attempts to invalid addresses
- Complaint escalation from frustrated recipients
The tradeoff is a more complex pre-send pipeline, but the reputation protection is worth it for bulk campaigns.
Monitoring & Observability
Campaign execution is tracked via:
- LaunchAgent logs: System logs accessible via
log stream --predicate 'process == "python"' - Dashboard updates: Real-time task state via
update