Auditing a Stalled Funeral Home Outreach Campaign: Infrastructure, Code State, and Path Forward
We recently inherited a multi-implementation funeral home outreach system that appeared functional on paper but had never sent a single email. This post walks through the technical audit, the architectural decisions that led to stalled execution, and the infrastructure choices for moving forward.
The Problem: Two Partial Implementations, Zero Sends
The standing requirement was straightforward: discover 10 new funeral home prospects daily, load them into a contact database, and send a drip campaign. However, an audit of the codebase and infrastructure revealed two separate, incomplete implementations—neither of which had ever executed.
Implementation #1: Google Apps Script (GAS) with SES
The primary implementation lives in /sites/queenofsandiego.com/FuneralOutreach.gs, a Google Apps Script bound to a Google Sheet (1ADx_0L6...rg38, tab Contacts). Here's what it was designed to do:
- Data source: Read from a 25-row Google Sheet with columns for prospect name, domain, contact email, and tracking fields (InitialSent, F1Sent, F2Sent, F3Sent, Replied, OOO, Bounce, etc.)
- Authentication: Use SES SigV4 signing in-script to send from
admin@queenofsandiego.comvia the AWS SDK - Cadence: Weekly trigger on Wednesday at 9 AM PT, with per-prospect follow-ups at Day 1, Day 4, Day 10, and Day 19
- Tracking: Mark rows in the sheet with send timestamps and reply/bounce status
The code looks solid—it includes OOO detection, bounce handling, and smart retry logic. However, three critical issues prevented execution:
- Trigger never installed: The
funeralOutreachSetup()function was never called, so no Apps Script trigger exists in the project. Without a trigger, the script never runs. - Schema mismatch: The sheet has 9 columns, but the script header declares a 14-column schema. This suggests
funeralOutreachSetup()was meant to auto-expand the sheet and never completed. - No audit trail: All 25 prospect rows have empty InitialSent, F1Sent, F2Sent, and Replied columns—confirming zero execution.
Implementation #2: EC2 Cron Job (Never Triggered)
A second implementation was deployed on EC2 via a one-off cron entry in /home/ubuntu/repos/tools/send_funeral_blast.sh:
5 16 24 4 * /home/ubuntu/repos/tools/send_funeral_blast.sh
This cron was set to run exactly once: April 24 at 4:05 PM UTC. It was designed to:
- Invoke
jada_blast.py send --campaign funeral-outreach-2026 - Read prospects from
tools/contacts/funeral-homes-sd.csv(8 hand-entered rows, including a self-test address) - Use the HTML template at
tools/templates/funeral-outreach.html - Log campaign sends to
s3://progress.queenofsandiego.com/blast-campaigns.json
The audit revealed it never executed successfully:
- No campaign ledger entry: The S3 file
blast-campaigns.jsoncontains 2,649 sends across 9 campaigns, butfuneral-outreach-2026is not listed. - No log files: No
funeral_blast_*.logexists intools/logs/. - Likely approval gate failure: The
jada_blast.pysystem requires manual approval before sending. No approval task was found in the done lane, so the cron likely exited silently.
What Was Never Built
Neither implementation includes the core requirement: automated prospect discovery. There is no code that:
- Scrapes Google Maps or business directories for funeral homes in San Diego
- Extracts contact emails or phone numbers
- Deduplicates against the existing sheet
- Appends new prospects daily
The 25 prospects in the sheet were manually entered. This explains why the drip never started—there was no automation to load prospects in the first place.
Infrastructure Audit: Gmail and SES
We checked for evidence of sends across multiple channels:
- Gmail audit (last 180 days): Zero sends to any of the 25 funeral home addresses from
jadasailing@gmail.com. Zero sends fromoutreach@orinfo@burialsatseasandiego.comaddresses. - SES sending status: No bounces, complaints, or delivery notifications in AWS SES for these campaigns.
- Inbound replies: Zero inbound emails from any prospect domain (greenwoodsd.com, lavistamemorialpark.com, neptunesandiego.com, cafuneralt.com, cortezcremations.com, etc.).
Why This Happened: Architectural Issues
The root causes boil down to a few architectural decisions:
- Two parallel implementations instead of one: The GAS and cron approaches served different purposes (drip vs. batch), but they weren't unified under a single orchestration layer. When one stalled, there was no fallback.
- Manual prospect loading: Without automation, the system depends on humans to find and add prospects. This doesn't scale and breaks the daily-10 cadence.
- Sender address mismatch: The standing rule requires sends from a
burialsatseasandiego.comaddress, but the GAS script was hardcoded to send fromadmin@queenofsandiego.com. This created friction when setting up SES and likely contributed to the approvals holdup. - Missing trigger setup: The most critical issue: no one called
funeralOutreachSetup()to install the Apps Script trigger. This was a one-line fix that was never executed.
Path Forward: Recommended Architecture
To move forward with clean data and reliable execution, we should:
- Consolidate to one sender: Set up
outreach@burialsatseasandiego.comin AWS SES (requires domain verification and DKIM setup in Route53). - Fix the GAS implementation: Call
funeralOutreachSetup()to auto-expand the sheet schema, switch the sender to the new SES address, and install the Apps Script trigger for Wednesday 9 AM PT. - Build prospect discovery: