```html

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.com via 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.json contains 2,649 sends across 9 campaigns, but funeral-outreach-2026 is not listed.
  • No log files: No funeral_blast_*.log exists in tools/logs/.
  • Likely approval gate failure: The jada_blast.py system 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 from outreach@ or info@burialsatseasandiego.com addresses.
  • 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.com address, but the GAS script was hardcoded to send from admin@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:

  1. Consolidate to one sender: Set up outreach@burialsatseasandiego.com in AWS SES (requires domain verification and DKIM setup in Route53).
  2. 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.
  3. Build prospect discovery: