Diagnosing a Stalled Funeral Home Outreach Campaign: Why Two Implementations Never Shipped
We discovered that a multi-week funeral home prospecting initiative—designed to find and contact 10 new funeral directors daily via automated email—has generated zero sends and zero effectiveness data. This post documents the forensic analysis that revealed why two separate implementations both failed silently, and the architectural decisions needed to actually launch the campaign.
The Problem: No Data, No Sends
A standing rule mandated daily outreach to funeral homes in the San Diego region: identify 10 new prospects daily, send them templated emails from outreach@burialsatseasandiego.com, and track reply patterns to measure conversion. After several weeks, the effectiveness spreadsheet showed all contact records with empty InitialSent, F1Sent, F2Sent, and Replied columns—zero activity across the board.
An audit of Gmail (jadasailing@gmail.com, 180-day window) confirmed: zero sends to any of the 25 prospect addresses on file, zero sends from any burialsatseasandiego.com address, and zero inbound replies from prospect domains (greenwoodsd.com, lavistamemorialpark.com, neptunesandiego.com, cafuneralt.com, cortezcremations.com, etc.). The outreach drip campaign had never actually started.
Forensic Analysis: Two Partial Implementations
Implementation #1: Google Apps Script (GAS)
The primary implementation lives in sites/queenofsandiego.com/FuneralOutreach.gs. This is a Google Apps Script that:
- Reads prospect data from a Google Sheet (ID:
1ADx_0L6...rg38, tabContacts) containing 25 manually-entered funeral home records - Sends emails via AWS SES using SigV4 request signing (computed in-script) from sender address
admin@queenofsandiego.com - Implements intelligent drip scheduling: Day 1 initial send, then follow-ups on Days 4, 10, and 19
- Detects out-of-office auto-replies, hard bounces, and prospect replies using Gmail API filters
Why it never ran: The trigger was never installed. The code defines a doWeekly() function meant to execute on Wednesday mornings at 9 AM PT, but no Apps Script trigger actually invokes it. Additionally, the sheet schema is incomplete: the script header documents a 14-column schema (including OOO, Bounce, F3Sent tracking), but the actual sheet only has 9 columns. The setup function funeralOutreachSetup()—which would extend the sheet and initialize tracking fields—was never executed.
Implementation #2: EC2 Cron Job
A second implementation exists on the production EC2 instance at /home/ubuntu/repos/tools/send_funeral_blast.sh. This shell script:
- Invokes a Python campaign sender:
jada_blast.py send --campaign funeral-outreach-2026 - Reads prospect CSV from
tools/contacts/funeral-homes-sd.csv(8 hand-entered records, including a self-test address atc.b.ladd@gmail.com) - Uses email template at
tools/templates/funeral-outreach.html - Writes send logs to
tools/logs/funeral_blast_*.log
The cron entry schedules execution for a single date: 5 16 24 4 * (4:05 PM UTC on April 24th). This is a one-off send, not the mandated daily cadence.
Why it never sent: Checking the S3 campaign ledger at s3://progress.queenofsandiego.com/blast-campaigns.json reveals 2,649 total sends across 9 campaigns, but funeral-outreach-2026 does not appear in the ledger. No corresponding log file exists in tools/logs/. The most likely explanation: the send encountered a validation gate (possibly an approval workflow requiring an m-2df1bcb8 task to move to the done lane in an internal workflow system) and exited silently without logging.
Architectural Gaps
Beyond implementation incompleteness, the standing rule's core requirement—find 10 new prospects daily—has no implementation anywhere in the codebase. Neither the GAS script nor the EC2 job contains prospect discovery logic. There is no integration with Google Maps API, funeral home directory scrapers, or any prospecting tool. Both implementations assume manually-curated CSV/sheet data, which is fundamentally non-scalable for daily discovery.
Additionally, neither implementation uses the correct sender identity. The standing rule mandates outreach@burialsatseasandiego.com, but:
- The GAS script sends from
admin@queenofsandiego.com - The EC2 script likely uses whatever sender is configured in the campaign template or SES identity settings
This sender mismatch introduces deliverability risk and violates the stated outreach policy.
Path Forward: Two Options
Option A (Quick Start): Install the weekly GAS trigger as-is, accept admin@queenofsandiego.com as sender, and begin collecting effectiveness data despite the non-compliance. Time: 5 minutes. This generates baseline data but violates the standing rule.
Option B (Compliant Launch): Before firing any campaign:
- Verify
outreach@burialsatseasandiego.comis registered in AWS SES as a verified sending identity - Update the GAS script to use
outreach@burialsatseasandiego.comas the sender in the SES SigV4 request - Extend the Google Sheet schema from 9 to 14 columns to match the script's tracking fields, then run
funeralOutreachSetup() - Install a daily trigger (not weekly) that discovers new prospects and queues them for outreach
- Install the weekly GAS trigger to handle initial sends and follow-up drips
- Time: ~20 minutes, produces auditable, policy-compliant data
Option B is recommended. Launching a campaign from an unauthorized sender address risks deliverability penalties and audit findings if the initiative scales.
Key Takeaway
Two parallel implementations both stalled at the deployment stage, leaving 25+ prospects cold for weeks. The root causes were: missing trigger installation, incomplete schema migration, a one-off cron (not daily automation), missing prospect discovery logic, and sender identity mismatch. The fix requires ~20 minutes of infrastructure work and tactical decisions about sender identity and discovery automation before the campaign is operationalized.