Auditing a Stalled Funeral Home Outreach Campaign: Why Two Implementations Never Shipped
Last week we discovered that a funeral home prospecting campaign—fully designed, partially implemented across two separate code paths, and backed by 25 qualified leads—has sent exactly zero emails in production. This post walks through what we found, why both implementations failed silently, and the architectural decisions that led us here.
The Discovery: Two Half-Built Systems
Our audit uncovered two separate outreach implementations, neither functional:
- Google Apps Script (GAS) implementation: Lives in
sites/queenofsandiego.com/FuneralOutreach.gs, scheduled but never triggered, with incomplete schema setup. - EC2 cron implementation: Lives in
/home/ubuntu/repos/tools/send_funeral_blast.sh, set to run once on April 24, but the send never completed and left no trace in logs or the campaign ledger.
Both point to the same Google Sheet (1ADx_0L6...rg38, tab Contacts) containing 25 manually-entered funeral home prospects. The sheet's tracking columns—InitialSent, F1Sent, F2Sent, Replied—are completely empty. Gmail audit across 180 days shows zero outbound sends to any funeral home domain (greenwoodsd.com, lavistamemorialpark.org, neptunesandiego.com, cafuneralt.com, cortezcremations.com, etc.) and zero inbound replies.
Implementation #1: Google Apps Script — Design vs. Reality
FuneralOutreach.gs was built with the right architecture but never activated. Here's what exists:
- Sender:
admin@queenofsandiego.comvia AWS SES with SigV4 signing in-script (not the mandatedoutreach@burialsatseasandiego.com). - Schedule: Weekly Wednesday 9 AM PT, not the standing rule's daily 10-prospect cadence.
- Drip logic: Per-row delay widening (Day 1 → Day 4 → Day 10 → Day 19).
- Features: OOO detection, bounce handling, reply tracking.
- Schema mismatch: Sheet has 9 columns; the script's header documents a 14-column schema including OOO/bounce/F3 tracking. The setup function
funeralOutreachSetup()was never called, so the sheet was never restructured.
The trigger was never installed. Without installTrigger() being run manually or as part of initialization, the script never executed. The InitialSent column remains empty across all 25 rows—a telltale sign that runFuneralOutreach() never fired even once.
Implementation #2: EC2 Cron — One-Shot That Failed Silently
The second implementation uses the existing funeral blast toolchain:
- Cron entry:
5 16 24 4 *(April 24 only, single execution). - Command:
jada_blast.py send --campaign funeral-outreach-2026 - Prospect file:
tools/contacts/funeral-homes-sd.csv(8 hand-entered addresses, including one self-testc.b.ladd@gmail.com). - Template:
tools/templates/funeral-outreach.html
Checking the campaign send ledger at s3://progress.queenofsandiego.com/blast-campaigns.json, we found 2,649 total emails across 9 campaigns—but funeral-outreach-2026 does not appear. No funeral_blast_*.log file exists in tools/logs/. The cron likely hit an approval gate (a missing task ID in the done lane) and exited without error, leaving no audit trail.
Why This Happened: Architecture & Decision Points
Reason #1: Dual implementation without ownership clarity. The GAS script and EC2 cron were built independently. Neither had a clear owner responsible for validation and activation. GAS assumed weekly sends; the EC2 path assumed daily 10-prospect harvesting (which doesn't exist anywhere in the codebase).
Reason #2: Schema mismatch indicates incomplete handoff. The GAS script's header documents a 14-column schema; the actual sheet has 9. This suggests funeralOutreachSetup() was designed but never executed. This is a classic sign of incomplete migration or testing—the developer finished the send logic but didn't finalize the data layer.
Reason #3: Sender address mismatch with standing rule. The standing rule mandates outreach@burialsatseasandiego.com, but GAS sends from admin@queenofsandiego.com. This wasn't caught during code review, suggesting the standing rule wasn't consulted during implementation.
Reason #4: Silent failure on EC2. The cron command likely succeeded (no non-zero exit), but the approval gate rejected the send and the script had no logging to surface why. Approval gates should fail loud, not silent.
What Should Happen Next
We have two paths forward:
- Path A (Quick but dirty): Install the GAS trigger and let it run Wednesday at 9 AM from
admin@queenofsandiego.com. This generates data immediately but violates the standing rule. - Path B (Correct but takes time): Stand up
outreach@burialsatseasandiego.comin SES, update the GAS script to use that sender, restructure the sheet to 14 columns, install the trigger, and shift to a daily 10-prospect cadence. This is ~20 minutes of work but produces clean, compliant data.
Path B is the right choice. Compliance violations and technical debt compound; sending from the wrong address risks deliverability and creates audit confusion. The prospect list is cold—another 20 minutes of setup won't hurt, but starting with debt will.
The EC2 path should be shelved until a prospect harvester exists. Right now it's a dead code path with no feeder mechanism.