Funeral Home Outreach Campaign: Diagnosing Two Stalled Implementations and the Path Forward
We recently conducted a deep audit of the funeral home outreach initiative after discovering that despite weeks of setup work, zero emails have been sent to prospects. This post documents what we found, why both implementations stalled, and the architectural decisions needed to unblock the campaign.
The Current State: Two Partial Systems, Zero Sends
Our investigation revealed two separate outreach implementations in different stages of incompleteness:
- Google Apps Script implementation (`sites/queenofsandiego.com/FuneralOutreach.gs`): Fully written but never triggered, with foundational setup incomplete
- EC2 cron-based implementation (`/home/ubuntu/repos/tools/send_funeral_blast.sh`): Single-shot attempt on April 24 that failed silently at the approval gate
The Google Sheet tracking prospect interactions (`1ADx_0L6...rg38`, tab `Contacts`) contains 25 manually-loaded funeral home records with empty response tracking columns—`InitialSent`, `F1Sent`, `F2Sent`, `Replied`—indicating the drip campaign never executed. A Gmail audit across 180 days showed zero sends to any prospect domain (greenwoodsd.com, lavistamemorialpark.com, neptunesandiego.com, etc.) from either the intended `outreach@burialsatseasandiego.com` or the fallback sender address.
Implementation #1: Google Apps Script—The Design Artifact
The GAS implementation in `FuneralOutreach.gs` is architecturally sound but mechanically incomplete:
- Sheet schema mismatch: The script header documents a 14-column tracking schema (including OOO detection, bounce tracking, and three follow-up send columns), but the actual `Contacts` sheet is only 9 columns wide. This indicates the initialization function `funeralOutreachSetup()` was never executed.
- Sender configuration drift: The code sends from `admin@queenofsandiego.com` via SES, but the standing rule requires all funeral outreach to originate from `outreach@burialsatseasandiego.com`—a domain we own but haven't provisioned in SES.
- Missing time trigger: The script contains logic for a weekly Wednesday 9 AM PT cadence with per-row delays (Day 1, Day 4, Day 10, Day 19), but the Apps Script trigger was never created. Without the trigger, the time-driven execution never starts.
- Cadence mismatch: The standing rule specifies daily discovery and sending of 10 new prospect emails, but this implementation only re-engages existing prospects on a fixed weekly schedule—it has no prospect discovery mechanism.
The script does include solid defensive logic: it checks for out-of-office replies via Gmail API, handles bounce notifications, and logs all send attempts back to the sheet. But this infrastructure sits dormant without a trigger and without the foundational sheet columns initialized.
Implementation #2: EC2 Cron—The One-Shot That Failed
The second approach attempted a single bulk send via `send_funeral_blast.sh` on April 24:
5 16 24 4 * /home/ubuntu/repos/tools/send_funeral_blast.sh
This cron line invokes:
jada_blast.py send --campaign funeral-outreach-2026 \
--csv /home/ubuntu/repos/tools/contacts/funeral-homes-sd.csv
The hardcoded CSV contains 8 prospect addresses, including one self-test email (`c.b.ladd@gmail.com`). The send command references template `tools/templates/funeral-outreach.html` and routes through the SES-backed campaign ledger system.
Why it failed: The campaign ledger in `s3://progress.queenofsandiego.com/blast-campaigns.json` contains records for 9 campaigns totaling 2,649 sends, but `funeral-outreach-2026` is absent. No corresponding log file exists in `tools/logs/`, and the approval workflow likely rejected the send without persisting an error state—the cron exited silently without writing to the ledger or log directory.
Infrastructure Gaps: No Prospect Discovery
Neither implementation includes a prospect harvester. The standing rule calls for automated discovery of 10 new funeral home contacts daily, but the codebase contains no:
- Google Maps API integration to search for funeral homes in San Diego County
- Web scraping logic to extract contact information from business directories
- Deduplication logic against existing sheet records
- Validation step to filter out invalid or suspended domains
Both systems depend entirely on manual CSV loading or Google Sheet population, creating a dependency on human data entry that breaks the automated daily cadence requirement.
Path Forward: Architectural Decisions
To unblock this campaign, we must decide between two approaches:
Option A: Quick Start (20 minutes)
Install the Apps Script trigger on `FuneralOutreach.gs` as-is and run the first send from `admin@queenofsandiego.com` to the 25 existing prospects. This generates baseline effectiveness data immediately but violates the standing rule sender requirement and ignores the daily-10 discovery mandate. Use case: rapid MVP feedback loop.
Option B: Compliant Build (4–6 hours)
- Provision SES sender: Verify the `burialsatseasandiego.com` domain in SES and create the `outreach@` address with DKIM/SPF alignment.
- Extend the Google Sheet: Run `funeralOutreachSetup()` to expand `Contacts` from 9 to 14 columns, initializing tracking fields for send timestamps, bounce codes, and reply indicators.
- Update sender in GAS: Change the hardcoded `admin@queenofsandiego.com` to `outreach@burialsatseasandiego.com` in the SES signature block.
- Implement prospect discovery: Build a daily Lambda function (or cron task on EC2) that queries Google Maps Places API for funeral homes in San Diego County, extracts contact info, deduplicates against existing sheet rows, and appends new prospects to the sheet.
- Adjust cadence: Modify the Apps Script trigger to daily at 10 AM PT (rather than weekly Wednesday), and configure the per-row delay logic to handle 10+ new prospects arriving each morning.
- Install trigger and monitor: Create the time-driven trigger via Apps Script UI, run a test send to an internal address, and monitor sheet updates and Gmail logs for 48 hours.
Option B produces data aligned with the standing rule and creates a sustainable prospect pipeline, but requires coordinating SES domain verification and building the discovery layer.
Next Steps
The decision on which path to take should be made by the campaign owner. Option A lets us answer "Are funeral home decision-makers responding to our outreach?" within hours. Option B ensures we're compliant with brand sender requirements and can scale to discover new prospects indefinitely.
Once approved, we'll document the implementation, add monitoring dashboards to track send success rates and reply metrics, and establish a weekly effectiveness review process tied to the sheet's `Replied` column.