Diagnosing a Stalled Funeral Home Outreach Campaign: Why Zero Emails Sent Despite Complete Code
We inherited a funeral home prospecting system that looked production-ready on paper but had never sent a single email. This post walks through the forensic analysis that uncovered why, and the architectural decisions needed to fix it.
The Symptom: 25 Prospects, Zero Contact
A Google Sheet at 1ADx_0L6...rg38 tab Contacts held 25 funeral home prospects (greenwoodsd.com, lavistamemorialpark.com, neptunesandiego.com, etc.). Every row had empty InitialSent, F1Sent, F2Sent, and Replied columns. The standing rule called for:
- 10 new prospects harvested daily
- Drip sequence: Day 1, Day 4, Day 10, Day 19 after initial contact
- Sender:
outreach@burialsatseasandiego.com(SES-validated) - Cadence: daily sends
Gmail audit over 180 days: zero sends to any prospect domain. Zero inbound replies. No logs of script execution.
Finding #1: Two Incomplete Implementations
GAS Script — Partial Infrastructure
File: /Users/cb/Documents/repos/sites/queenofsandiego.com/FuneralOutreach.gs
This Google Apps Script had the right bones:
- Reads from the Sheet via v4 API
- Builds SES SigV4 signatures in-script (no external auth library)
- Includes OOO and bounce detection logic
- Defines a 14-column schema:
Prospect,Email,InitialSent,F1Sent,F2Sent,F3Sent,Replied,ReplyDate,OOO,Bounce,BounceReason,FollowUpGap,CampaignID,Notes
But the trigger was never installed. More critically, the setup function funeralOutreachSetup() was never executed, meaning:
- Sheet only had 9 columns (stopped at
Notes) - No column validation
- The weekly Wednesday 9 AM PT trigger referenced in code comments never fired
- Sender was hardcoded to
admin@queenofsandiego.com, not theoutreach@domain
EC2 Cron — One-Shot Campaign
File: /home/ubuntu/repos/tools/send_funeral_blast.sh
A single cron entry: 5 16 24 4 * (April 24 only, 4:05 PM UTC). This called:
jada_blast.py send --campaign funeral-outreach-2026 \
--csv tools/contacts/funeral-homes-sd.csv \
--template tools/templates/funeral-outreach.html
The CSV had 8 hand-entered prospects. The S3 campaign ledger at s3://progress.queenofsandiego.com/blast-campaigns.json contains 2,649 total sends across 9 campaigns. funeral-outreach-2026 does not appear in it. No funeral_blast_*.log exists in tools/logs/. The cron job likely failed the approval gate (no m-2df1bcb8 task in the workflow done lane) and exited silently.
Root Causes
- No trigger installed: GAS code existed but wasn't connected to a time-based or manual trigger.
- Schema mismatch: Sheet width (9 cols) didn't match script expectations (14 cols). Setup was incomplete.
- Wrong sender:
admin@queenofsandiego.cominstead of the verifiedoutreach@burialsatseasandiego.comin SES. - No harvester: No prospect acquisition job exists. The 25 prospects were manually loaded once; no daily top-up of 10 new leads ever ran.
- Cadence mismatch: GAS was set to weekly; standing rule calls for daily sends.
- One-shot cron: EC2 script was a single April 24 task, not a recurring job.
What a 7-Figure Digital Marketer Would Do Here
In order of ROI:
- Validate the audience: Before scaling, confirm that 25 hand-entered prospects represent a real market segment. Run a small batch (5–10 addresses) manually via SES and measure open/reply rates. If reply rate is <5%, the template or offer is broken, not the infrastructure.
- Fix sender reputation first: Switch from
admin@queenofsandiego.comtooutreach@burialsatseasandiego.com`. This domain is SPF/DKIM-authenticated in SES; using it signals legitimacy to spam filters. Do NOT send from an unvalidated domain. - Pick one implementation stack: GAS is simpler (no server ops), EC2 is scalable. For funeral homes (low volume, niche), GAS + Sheet is the right call. Retire the EC2 cron.
- Instrument before scale: Add logging to every send attempt. Sheet columns
InitialSent,F1Sent, etc., are your only telemetry. Include SES bounce/complaint webhooks (SNS → Lambda → Sheet append) so you catch hard bounces in real time, not weeks later. - Automate prospect ingest last: Once the drip works on 25, write the harvester (Google Maps scraper, LinkedIn, local directory API). Ingesting 10/day into a broken system just creates 10 daily failures.
The Fix (High Level)
To restart this campaign correctly:
- Extend the Sheet to 14 columns (run
funeralOutreachSetup()in GAS script editor console) - Change sender to
outreach@burialsatseasandiego.com - Install a daily trigger (e.g., 9 AM PT Monday–Friday) instead of weekly
- Add SES bounce/complaint SNS webhooks to update
BounceandBounceReasoncolumns - Write a lightweight prospect harvester (start with local directory CSV uploads, then add Maps API scraper)
- Delete the