Auditing a Stalled Funeral Home Outreach Campaign: Why Two Implementations Failed to Launch
We inherited a funeral home prospecting system with two partial implementations, neither operational. After a week-long audit across Google Apps Script, EC2 infrastructure, SES, and S3 logging, we discovered why zero emails have shipped despite weeks of setup work—and what needs to happen to fix it.
The Problem Statement
A standing rule mandates daily outreach to 10 new funeral home prospects via email, with a drip sequence tracking replies and engagement. The Google Sheet showed 25 manually-loaded prospects with zero sends recorded. Gmail audit (180 days) showed zero outbound mail to any funeral home domain. The question: why isn't this working, and what's the minimum viable fix?
Implementation #1: Google Apps Script — Designed Well, Never Triggered
The primary implementation lives in /Users/cb/Documents/repos/sites/queenofsandiego.com/FuneralOutreach.gs. It's architecturally sound:
- Data source: Google Sheet
1ADx_0L6...rg38, tabContacts(25 rows, 9 columns: Name, Email, Phone, Address, City, State, Zip, Notes, Source). - Send mechanism: Inline SES SigV4 signing. The script constructs MIME messages, computes AWS Signature Version 4, and POSTs directly to
email.us-west-2.amazonaws.com. - Cadence: Weekly trigger scheduled for Wednesday 9 AM PT, then per-recipient delay widening (Day 1 → Day 4 → Day 10 → Day 19).
- Reply detection: Logic to scan Gmail inbox for replies from each prospect domain, flag OOO responses, detect bounces.
- Sender identity:
admin@queenofsandiego.com(not SES-verified asoutreach@burialsatseasandiego.com, per the standing rule).
The blocker: The Apps Script trigger was never installed. No time-driven trigger appears in the GAS project bindings. The initialization function funeralOutreachSetup() was never called, so the sheet schema was never extended from 9 to 14 columns (missing InitialSent, F1Sent, F2Sent, Replied, OOO flags, bounce flags). Every row's InitialSent column is empty—the script can't tell whether a prospect has been contacted.
Implementation #2: EC2 Cron Daemon — Single-Shot, Approval-Gated, Failed
A second implementation on EC2 uses a hand-rolled cron job:
- Cron spec:
5 16 24 4 *— fires only on April 24th at 4:05 PM UTC. This was intended as a one-off test, not the daily harvester. - Command:
/home/ubuntu/repos/tools/send_funeral_blast.sh, which invokesjada_blast.py send --campaign funeral-outreach-2026againsttools/contacts/funeral-homes-sd.csv(8 hand-entered prospects). - Template:
tools/templates/funeral-outreach.html(basic pitch for burial-at-sea services). - Approval gate: The blast tool requires a Kanban task in the
approvedlane (task ID formatm-XXXXXXXX) before sending. If missing, the tool logs a warning and exits silently.
What happened: On April 24, the cron job likely fired, but the approval task was never created or moved. The campaign ledger at s3://progress.queenofsandiego.com/blast-campaigns.json contains 2,649 emails across 9 campaigns—but funeral-outreach-2026 is not listed. No log files exist in tools/logs/funeral_blast_*.log. The send was gated out.
Infrastructure & Data Audit Details
SES & Domain Verification: admin@queenofsandiego.com is verified in SES; outreach@burialsatseasandiego.com is not. The standing rule mandates use of the burialsatseasandiego.com address for funeral outreach (separate from the core brand domain), but no SES identity was ever set up for it.
S3 & Campaign Ledger: The blast tool logs all sends to s3://progress.queenofsandiego.com/blast-campaigns.json. A tally script queries this ledger to count sends per campaign. The funeral campaign is simply absent, confirming zero sends.
Gmail Audit: 180-day search across jadasailing@gmail.com (admin account) found:
- 0 messages sent to any of the 25 funeral home addresses (e.g.,
@greenwoodsd.com,@lavistamemorialpark.com,@neptunesandiego.com). - 0 messages sent from
outreach@orinfo@burialsatseasandiego.com. - 0 inbound replies from any prospect domain.
Root Cause Analysis
Two independent implementations exist, but neither is operational:
- GAS script: Code is correct, but the trigger was never wired up. The sheet schema is incomplete, so even if the trigger fired, the script would fail trying to write to missing columns.
- EC2 cron: Designed as a one-off test, not a daily harvester. The approval gate prevented the single send. No mechanism exists to discover and enroll 10 new prospects daily.
Neither implementation does the core job: discovering new funeral home prospects programmatically. Both rely on manual CSV/Sheet population. The standing rule calls for an automated scraper (Google Maps API, local directories, etc.) to feed the pipeline. That tool does not exist.
Key Decision Points Going Forward
Option A: Quick-Start with GAS (20 min)
- Install the Apps Script time-driven trigger (Wednesday 9 AM PT).
- Run
funeralOutreachSetup()to extend sheet schema. - Accept sending from
admin@queenofsandiego.comfor now (deviates from standing rule, but data is better than silence). - Pro: Immediate drip starts; we get engagement data within 7 days.
- Con: Domain reputation grows under the wrong sender; standing rule is violated.
Option B: Clean Implementation (60 min)
- Verify
outreach@burialsatseasandiego.comidentity