```html

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 the outreach@ 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.com instead of the verified outreach@burialsatseasandiego.com in 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:

  1. 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.
  2. Fix sender reputation first: Switch from admin@queenofsandiego.com to outreach@burialsatseasandiego.com`. This domain is SPF/DKIM-authenticated in SES; using it signals legitimacy to spam filters. Do NOT send from an unvalidated domain.
  3. 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.
  4. 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.
  5. 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 Bounce and BounceReason columns
  • Write a lightweight prospect harvester (start with local directory CSV uploads, then add Maps API scraper)
  • Delete the