Diagnosing a Stalled Funeral Home Outreach Campaign: Why Two Partial Implementations Never Shipped
During a recent infrastructure audit of the Burial at Sea San Diego marketing stack, we uncovered a critical gap: a funeral home prospect outreach campaign that was partially built across two codebases but never actually executed. This post walks through the diagnostic process, the architectural decisions that led to the split implementation, and why neither path reached production.
The Setup: What Was Supposed to Happen
According to standing campaign rules, the system should:
- Discover 10 new funeral home prospects daily via automated harvesting
- Add them to a managed contact list
- Send an initial outreach email from a dedicated
outreach@burialsatseasandiego.comsender - Track opens, replies, and bounce/OOO signals
- Execute a drip sequence (Day 1 → Day 4 → Day 10 → Day 19)
Reality: 25 prospects had been manually loaded into a Google Sheet weeks prior, all columns for send timestamps and replies remained empty, and exactly zero emails had been transmitted to any funeral home.
Implementation #1: Google Apps Script in sites/queenofsandiego.com/
The first attempt lives in FuneralOutreach.gs, a Google Apps Script designed to read from a managed Google Sheet (ID: 1ADx_0L6...rg38, tab: Contacts) and send via Amazon SES.
Architecture:
- Data source: Google Sheet with 9 active columns: Name, Domain, Contact Email, Phone, Notes, InitialSent, F1Sent, F2Sent, Replied
- Send mechanism: AWS SES via SigV4 signed requests directly in the script
- Sender:
admin@queenofsandiego.com(not the mandatedoutreach@burialsatseasandiego.com) - Cadence: Weekly trigger on Wednesday 9 AM PT, not the daily 10-per-day required by spec
- Sequencing: Widening gaps (Day 1 → Day 4 → Day 10 → Day 19) with OOO and bounce detection logic
Why it never ran:
The Apps Script header declares a 14-column schema (including OOO tracking, bounce handling, F3 sequence, and metadata), but the sheet itself only has 9 columns. The initialization function funeralOutreachSetup() was designed to reconcile this mismatch—adding missing columns, populating headers, and initializing tracking state—but was never executed. Without those columns, any attempt to write InitialSent timestamps would fail or write to the wrong cell. The Apps Script trigger itself was also never installed, so the weekly Wednesday 9 AM call never fired.
Code check:
// From FuneralOutreach.gs header
const FUNERAL_SHEET_ID = "1ADx_0L6...rg38";
const FUNERAL_CONTACTS_TAB = "Contacts";
const EXPECTED_COLUMNS = 14; // name, domain, email, phone, notes, InitialSent, F1Sent, F2Sent, F3Sent, Replied, OOO, Bounce, LastAction, Notes
// Sheet reality: 9 columns
// Result: setup() never run → schema mismatch → no trigger installed
Implementation #2: EC2 Cron + Python Daemon in tools/
A second, independent approach was staged on an EC2 instance with a shell wrapper calling jada_blast.py.
Architecture:
- Entry point:
/home/ubuntu/repos/tools/send_funeral_blast.sh - Cron spec:
5 16 24 4 *(April 24 only, single execution—clearly a test run) - CSV source:
tools/contacts/funeral-homes-sd.csv(8 hand-entered prospects; one row was a self-test toc.b.ladd@gmail.com) - Send engine:
jada_blast.py send --campaign funeral-outreach-2026 - Template:
tools/templates/funeral-outreach.html - Campaign ledger: S3 at
s3://progress.queenofsandiego.com/blast-campaigns.json
Why it never shipped:
The cron job was configured for a single date (April 24) as a dry run. Audit of the campaign ledger at s3://progress.queenofsandiego.com/blast-campaigns.json shows 2,649 emails across 9 successfully executed campaigns, but funeral-outreach-2026 does not appear. No corresponding task ID (format: m-2df1bcb8 or similar) exists in the approval workflow, and no log file (tools/logs/funeral_blast_*.log) was created—indicating the cron either silently failed the approval gate or never executed. The one-time cron also violates the daily-10-prospects spec entirely.
Root Cause: Architectural Mismatch and Incomplete Handoff
Two engineers built two solutions in parallel without a completed handoff:
- Solution #1 (GAS): Designed for automated, repeating cadence with rich tracking but never initialized and never triggered.
- Solution #2 (EC2 + Python): Built for one-time testing; config remains frozen to a past date; no automation to discover new prospects.
Neither implementation includes the prospect harvester—the missing piece that should feed 10 new funeral home contact records daily into whichever send mechanism ultimately wins.
Key Decisions Going Forward
Option A (Minimal): Activate GAS as-is
- Install the Apps Script trigger for weekly Wednesday 9 AM
- Run
funeralOutreachSetup()once to align sheet schema - Begin collecting open/reply/bounce data from the existing 25 prospects
- Accept that it sends weekly (not daily) and from
admin@queenofsandiego.comuntil SES sender identity is upgraded - Time to signal: ~10 minutes
Option B (Correct): Full spec alignment before launch
- Verify
outreach@burialsatseasandiego.comis added to SES verified identities - Update GAS sender field to
outreach@burialsatseasandiego.com - Run
funeralOutreachSetup()to establish 14-column schema - Install daily trigger (or modify to cron/Cloud Scheduler) to call