```html

Diagnosing a Stalled Funeral Home Outreach Campaign: Infrastructure, Code State, and Recovery Path

We inherited a funeral home prospecting system that was supposed to be running automatically—discovering 10 new funeral home contacts daily, enriching them into a tracking sheet, and sending a timed drip sequence. Instead, after weeks of supposed operation, zero emails had been sent and zero replies received. This post walks through how we diagnosed the failure across three loosely-coupled systems and what we learned about distributed marketing automation.

The Three Implementations (None Fully Working)

The system was built in pieces, and no single implementation covered the full spec:

  • Google Apps Script (GAS) in sites/queenofsandiego.com/FuneralOutreach.gs: Handles drip sequencing and reply detection. Reads/writes a Google Sheet (1ADx_0L6...rg38, tab Contacts). Sends via SES SigV4 from admin@queenofsandiego.com.
  • EC2 cron job (/home/ubuntu/repos/tools/send_funeral_blast.sh): One-time scheduled send for Apr 24, 2026. Uses jada_blast.py to read a CSV and dispatch via SES campaign ledger.
  • Missing: prospect harvester. No code exists to query Google Maps, scrape directories, or auto-populate the contact sheet.

What the Audit Found

Google Sheet state: 25 funeral home prospects were manually loaded into the Contacts tab, but all tracking columns (InitialSent, F1Sent, F2Sent, Replied) are empty. The sheet has 9 columns; the GAS script header declares 14, implying funeralOutreachSetup() was never executed to populate the schema.

Gmail audit (180-day window, jadasailing@gmail.com): Zero sends to any of the 25 funeral home addresses. Zero sends from outreach@ or info@burialsatseasandiego.com (the domain the standing rule mandates). Zero inbound replies.

GAS trigger state: The script specifies a weekly Wednesday 9 AM PT trigger, not the daily 10-per-day cadence in the spec. But the trigger itself was never installed in the Apps Script project.

EC2 campaign: The cron was set for Apr 24, 2026, and calls jada_blast.py send --campaign funeral-outreach-2026. The campaign ledger at s3://progress.queenofsandiego.com/blast-campaigns.json contains 2,649 emails across 9 campaigns, but funeral-outreach-2026 is not among them. No log file exists at tools/logs/funeral_blast_*.log, so the send either never fired or failed silently before writing.

Technical Breakdown: Why Nothing Ran

GAS implementation gaps:

  • Trigger never installed. The script is defined but Apps Script never scheduled it.
  • Sender mismatch: script uses admin@queenofsandiego.com, but the standing rule requires outreach@burialsatseasandiego.com.
  • Schema mismatch: sheet has 9 columns, script expects 14. The setup function that bridges this gap never ran.
  • No harvester. The drip can sequence 25 contacts, but there's no mechanism to find new ones.

EC2 cron gaps:

  • One-time schedule (Apr 24 only). Not a recurring job.
  • Silent failure. If the approval gate rejected the send (missing task in done lane), the shell script exited without logging the error visibly.
  • Hardcoded campaign name and CSV path. Scaling to 10 new contacts daily would require rewrites.

Infrastructure and Architecture Decisions

Why two implementations? The GAS version is pull-based (reads from a sheet on demand), while the EC2 version is push-based (runs on a schedule, writes logs to disk and S3). GAS is more maintainable for small lists; EC2 scales better for large volumes and integrates with existing SES infrastructure.

Why SES over Gmail API? Gmail has stricter rate limits (e.g., 100 msgs/day by default). SES allows higher throughput and is already set up with dedicated sender domains and bounce/complaint handling via SNS.

Why a separate outreach@burialsatseasandiego.com Sender reputation is domain-specific. Using admin@ muddles deliverability signals. A dedicated outreach@ address isolates this campaign's bounce/complaint rate, so a bad list doesn't tank admin email.

The Recovery Path

Two options emerged:

Option A: Quick start (20 min)
Install the weekly GAS trigger now, using admin@queenofsandiego.com` as-is. Begin generating data from the 25 existing prospects. Trade-off: sender domain reputation may suffer if bounces occur.

Option B: Clean start (40 min)
Before sending anything:

  • Set up SES sender identity for outreach@burialsatseasandiego.com.
  • Extend the Google Sheet to 14 columns and run funeralOutreachSetup()` to populate defaults.
  • Update the GAS script to send from the new domain.
  • Switch cadence from weekly to daily, and cap at 10 sends per run (respecting SES limits).
  • Install the trigger and let it fire.

Option B ensures clean metrics and reduces blacklist risk.

What a 7-Figure Marketer Would Do

Recognize that data quality beats speed. The 25 prospects are cold. Without a working harvester, you're not discovering new ones daily. Without a working send, you have no reply data. The first move is to get one complete loop working end-to-end: harvest → enrich → send → track → reply. Then iterate.

Second: isolate sender domains by campaign. Funeral homes are a distinct vertical; give them their own reputation track.

Third: instrument everything. Log every decision—harvests, enrichments, sends, bounces, replies. The current GAS script has reply detection code but no way to see if it ever fired. Logs are the debugging backbone.

Next Steps

We're implementing Option B:

  1. Verify SES sender identity for outreach@burialsatseasandiego.com (check CloudWatch metrics for delivery status).
  2. Extend the Google Sheet schema and run the setup function.
  3. Update FuneralOutreach.gs to use the new sender and cadence.
  4. Install the trigger and monitor the first three runs for errors.