Diagnosing a Stalled Funeral Home Outreach Campaign: Why Two Partial Implementations Never Shipped

During a recent infrastructure audit of the Burial at Sea San Diego marketing stack, we uncovered a critical gap: a funeral home prospect outreach campaign that was partially built across two codebases but never actually executed. This post walks through the diagnostic process, the architectural decisions that led to the split implementation, and why neither path reached production.

The Setup: What Was Supposed to Happen

According to standing campaign rules, the system should:

  • Discover 10 new funeral home prospects daily via automated harvesting
  • Add them to a managed contact list
  • Send an initial outreach email from a dedicated outreach@burialsatseasandiego.com sender
  • Track opens, replies, and bounce/OOO signals
  • Execute a drip sequence (Day 1 → Day 4 → Day 10 → Day 19)

Reality: 25 prospects had been manually loaded into a Google Sheet weeks prior, all columns for send timestamps and replies remained empty, and exactly zero emails had been transmitted to any funeral home.

Implementation #1: Google Apps Script in sites/queenofsandiego.com/

The first attempt lives in FuneralOutreach.gs, a Google Apps Script designed to read from a managed Google Sheet (ID: 1ADx_0L6...rg38, tab: Contacts) and send via Amazon SES.

Architecture:

  • Data source: Google Sheet with 9 active columns: Name, Domain, Contact Email, Phone, Notes, InitialSent, F1Sent, F2Sent, Replied
  • Send mechanism: AWS SES via SigV4 signed requests directly in the script
  • Sender: admin@queenofsandiego.com (not the mandated outreach@burialsatseasandiego.com)
  • Cadence: Weekly trigger on Wednesday 9 AM PT, not the daily 10-per-day required by spec
  • Sequencing: Widening gaps (Day 1 → Day 4 → Day 10 → Day 19) with OOO and bounce detection logic

Why it never ran:

The Apps Script header declares a 14-column schema (including OOO tracking, bounce handling, F3 sequence, and metadata), but the sheet itself only has 9 columns. The initialization function funeralOutreachSetup() was designed to reconcile this mismatch—adding missing columns, populating headers, and initializing tracking state—but was never executed. Without those columns, any attempt to write InitialSent timestamps would fail or write to the wrong cell. The Apps Script trigger itself was also never installed, so the weekly Wednesday 9 AM call never fired.

Code check:

// From FuneralOutreach.gs header
const FUNERAL_SHEET_ID = "1ADx_0L6...rg38";
const FUNERAL_CONTACTS_TAB = "Contacts";
const EXPECTED_COLUMNS = 14; // name, domain, email, phone, notes, InitialSent, F1Sent, F2Sent, F3Sent, Replied, OOO, Bounce, LastAction, Notes
// Sheet reality: 9 columns
// Result: setup() never run → schema mismatch → no trigger installed

Implementation #2: EC2 Cron + Python Daemon in tools/

A second, independent approach was staged on an EC2 instance with a shell wrapper calling jada_blast.py.

Architecture:

  • Entry point: /home/ubuntu/repos/tools/send_funeral_blast.sh
  • Cron spec: 5 16 24 4 * (April 24 only, single execution—clearly a test run)
  • CSV source: tools/contacts/funeral-homes-sd.csv (8 hand-entered prospects; one row was a self-test to c.b.ladd@gmail.com)
  • Send engine: jada_blast.py send --campaign funeral-outreach-2026
  • Template: tools/templates/funeral-outreach.html
  • Campaign ledger: S3 at s3://progress.queenofsandiego.com/blast-campaigns.json

Why it never shipped:

The cron job was configured for a single date (April 24) as a dry run. Audit of the campaign ledger at s3://progress.queenofsandiego.com/blast-campaigns.json shows 2,649 emails across 9 successfully executed campaigns, but funeral-outreach-2026 does not appear. No corresponding task ID (format: m-2df1bcb8 or similar) exists in the approval workflow, and no log file (tools/logs/funeral_blast_*.log) was created—indicating the cron either silently failed the approval gate or never executed. The one-time cron also violates the daily-10-prospects spec entirely.

Root Cause: Architectural Mismatch and Incomplete Handoff

Two engineers built two solutions in parallel without a completed handoff:

  • Solution #1 (GAS): Designed for automated, repeating cadence with rich tracking but never initialized and never triggered.
  • Solution #2 (EC2 + Python): Built for one-time testing; config remains frozen to a past date; no automation to discover new prospects.

Neither implementation includes the prospect harvester—the missing piece that should feed 10 new funeral home contact records daily into whichever send mechanism ultimately wins.

Key Decisions Going Forward

Option A (Minimal): Activate GAS as-is

  • Install the Apps Script trigger for weekly Wednesday 9 AM
  • Run funeralOutreachSetup() once to align sheet schema
  • Begin collecting open/reply/bounce data from the existing 25 prospects
  • Accept that it sends weekly (not daily) and from admin@queenofsandiego.com until SES sender identity is upgraded
  • Time to signal: ~10 minutes

Option B (Correct): Full spec alignment before launch

  • Verify outreach@burialsatseasandiego.com is added to SES verified identities
  • Update GAS sender field to outreach@burialsatseasandiego.com
  • Run funeralOutreachSetup() to establish 14-column schema
  • Install daily trigger (or modify to cron/Cloud Scheduler) to call