```html

Diagnosing a Stalled Funeral Home Outreach Campaign: Why Two Implementations Failed and What We Learned

We discovered that a funeral home email outreach system—designed to contact 25+ prospects across San Diego—had never sent a single email in weeks. Two separate implementations existed in the codebase, each partially built and neither operational. This post walks through the diagnostic process, the root causes, and the architectural lessons that emerged.

The Problem: Zero Sends, Two Implementations

A standing rule called for daily outreach to 10 new funeral home prospects via outreach@burialsatseasandiego.com. When we audited the system, we found:

  • No emails sent: Gmail audit across 180 days showed zero sends to any of the 25 funeral home addresses and zero inbound replies.
  • No effectiveness data: A Google Sheet at tab Contacts in workbook 1ADx_0L6...rg38 contained 25 prospects with empty InitialSent, F1Sent, F2Sent, and Replied columns—all untouched.
  • Two competing codebases: A Google Apps Script implementation and an EC2-based Python daemon, neither firing.

Implementation #1: Google Apps Script (Incomplete)

The first approach used Google Apps Script at /sites/queenofsandiego.com/FuneralOutreach.gs to read the prospect sheet and send via AWS SES.

What was built:

  • Sheet integration: Reads from Google Sheet 1ADx_0L6...rg38, tab Contacts, columns A–I (prospect name, email, company, etc.).
  • SES integration: Uses SigV4 signing to call email.us-west-2.amazonaws.com from within the script, sending from admin@queenofsandiego.com.
  • Cadence logic: Defines a widening follow-up sequence: Day 1 (initial), Day 4 (followup 1), Day 10 (followup 2), Day 19 (followup 3).
  • Bounce and OOO detection: Includes logic to parse bounces and out-of-office replies from Gmail headers, updating the sheet accordingly.

Why it never ran:

  • No trigger installed: The script defines trigger specifications (weekly Wednesday 9 AM PT) but the installTrigger() function was never called. Apps Script UI triggers were not created.
  • Schema mismatch: The script header documents a 14-column schema (including bounce tracking, F3, OOO columns), but funeralOutreachSetup()—which would expand the sheet to this width—was never executed. The sheet remains at 9 columns.
  • Wrong sender domain: Uses admin@queenofsandiego.com instead of the standing rule's outreach@burialsatseasandiego.com. This suggests the script predates the domain shift.
  • No verification: Zero rows show InitialSent timestamps, confirming the function never fired.

Implementation #2: EC2 Cron Daemon (One-Shot, Failed)

The second approach was a cron-based email blast on our EC2 instance, designed to send in bulk to a CSV roster.

What was built:

  • Cron entry: 5 16 24 4 * in the instance crontab—scheduled for 4:05 PM UTC on April 24 only (single-shot, not recurring).
  • Shell wrapper: /home/ubuntu/repos/tools/send_funeral_blast.sh invokes jada_blast.py send --campaign funeral-outreach-2026.
  • CSV roster: /home/ubuntu/repos/tools/contacts/funeral-homes-sd.csv contains 8 hand-curated prospect rows (greenwoodsd, lafuneralt, etc.) plus one self-test address.
  • Email template: /home/ubuntu/repos/tools/templates/funeral-outreach.html holds the campaign message.
  • Campaign ledger: s3://progress.queenofsandiego.com/blast-campaigns.json tracks all sends by campaign ID.

Why it never sent:

  • No approval gate pass: The blast system requires a Jira-style approval task (e.g., m-2df1bcb8 in the "done" lane). The campaign ledger shows 2,649 emails across 9 campaigns, but funeral-outreach-2026 is absent—indicating the send was blocked or failed before reaching the ledger.
  • No logs: No funeral_blast_*.log file exists in /home/ubuntu/repos/tools/logs/. Either the cron job never executed (possible if the instance was rebooted), or it exited silently before logging.
  • One-shot design: Even if the April 24 send had succeeded, the cron entry would not repeat. There is no daily loop or prospect harvester.
  • Missing harvester: Neither implementation includes a Google Maps or directory scraper to find and add 10 new prospects daily. Both rely on static rosters (one Google Sheet, one CSV).

Root Cause Analysis

Two independent issues cascaded into zero sends:

  • Partial implementation + lack of follow-through: Both codebases were started but not finished. GAS script lacks trigger installation and schema setup; EC2 cron was a one-off test that never completed the approval flow.
  • No integration between implementations: Different rosters (25 prospects in Sheets vs. 8 in CSV), different senders (admin@ vs. outreach@), different cadences (weekly vs. single-shot). No clear owner or mandate to unify them.
  • Silent failures: The approval gate for the EC2 send probably rejected the campaign, but the shell script did not surface the error to cron logs. The GAS script had no installed trigger, so there was nothing to fail.
  • No harvest loop: The standing rule calls for finding 10 new prospects daily. Neither implementation has this capability; both assume a pre-built roster.

Key Decisions for the Fix

When rebuilding, we face two paths:

  • Option A (Quick start): Install the GAS trigger and let it fire Wednesday 9 AM PT from admin@queenofsandiego.com, generating data immediately. Risk: uses wrong sender domain and weekly cadence.
  • Option B (Clean slate): Set up outreach@burialsatseasandiego.com in SES, switch the GAS script sender, complete funeralOutreachSetup()