Flow — Cold Outreach (Funeral)
Status: DRAFTED Owner: john@funeralwiseai.com Last verified against prod: 2026-05-04 (initial draft — not yet run end-to-end) Related skill: none (new vertical — no skill exists yet)
1. Purpose
Drive funeral home prospects from first awareness to demo booking to paid setup. Source: scraped funeral home listings (741 prospects as of 2026-05-04, 0 conversions). This is the first commercial non-church vertical. The pipeline mirrors the church outreach pipeline but uses funeral-coded copy, secular tone, and routes bookings to the funeral-specific Cal.com event type.
2. Trigger
Triggered by the weekly scrape cron or a manual founder action.
- Type: cron (weekly) + manual override
- Source:
/api/cron/outreach-scrape/route.ts(withvertical = 'funeral'param) - Schedule: Scrape Mondays 6am ET; send Wednesdays 9am ET (same pipeline, different vertical)
- Founder UI:
/founder/[token]/outreach-engine→ filter by vertical=funeral
3. Preconditions
-
outreach_contactsrow exists withvertical = 'funeral',status = 'provisioned' -
preview_urlpopulated (must use funeral preview template — NOT church template) -
outreach_contacts.email_do_not_contact = false(checked at row level, notchurchestable) -
outreach_contacts.status != 'dnc'and!= 'bounced'and!= 'converted' -
tenant_voice_agentsFK constraint DROPPED (Stream A — must be resolved before funeral provisioning works) - Funeral preview templates deployed (Stream A —
funeraltemplate variant in/preview/[slug]) - RESEND_API_KEY active and Resend domain verified for
funeralwiseai.com - Physical address confirmed in email footer template — BLOCKER for first send
- Cal.com
funeralwiseai-demoevent type exists (founder creates manually)
4. Steps
Step 1 — Scrape
What happens: Funeral home listings (scraped from public directories — not PewSearch) are loaded into outreach_contacts with vertical = 'funeral'.
Where: Scrape source TBD — likely custom scraper targeting NFDA directory, Yellow Pages funeral category, or Google Places API. See founder for approved scrape source.
Verifications:
- db:
SELECT count(*) FROM outreach_contacts WHERE vertical='funeral' AND created_at >= now() - interval '7 days'→ >0 after scrape run - db:
SELECT name, email, website FROM outreach_contacts WHERE vertical='funeral' LIMIT 5→ spot-check quality (no churches, no empty emails)
Failure signal: Count doesn't grow → scraper blocked or rate-limited. Email field empty on all rows → source doesn't expose emails (need LinkedIn Sales Nav or manual enrichment).
Step 2 — Provision (Personalized Demo Preview)
What happens: provision.ts scrapes the funeral home website, generates funeral-specific Q&A via Haiku API, creates a demo tenant row, and populates preview_url on outreach_contacts.
Where: churchwiseai-web/src/lib/outreach/provision.ts — requires funeral vertical path (currently blocked by church_voice_agents_church_id_fkey — Stream A must fix this first)
Verifications:
- db:
SELECT preview_url, status FROM outreach_contacts WHERE id = '<prospect_id>'→preview_urlNOT NULL,status = 'provisioned' - render: GET
{preview_url}→ HTTP 200, uses funeral template (NOT Protestant/Catholic/Community church template), page title contains funeral home name - db: Demo tenant row created with correct
vertical = 'funeral'
Failure signal: status = 'provision_failed' → check for FK error in provision logs (Stream A issue). preview_url uses church template → template dispatcher not checking vertical.
Step 3 — Send Email 1 (Day 0)
What happens: Send Email 1 ("What happens when a family calls [Funeral Home Name] at 2am?") via Resend. FROM address: hello@funeralwiseai.com (not churchwiseai.com).
Where: /api/cron/outreach-send/route.ts → cold-outreach-emails-funeral.md templates
Verifications:
- db:
SELECT status, email_1_sent_at FROM outreach_contacts WHERE id = '<prospect_id>'→status = 'email_1_sent',email_1_sent_atNOT NULL - delivered: Gmail search
to:<prospect_email> subject:"What happens when a family calls"within 15 min - render: Email from address is
hello@funeralwiseai.com, NOThello@churchwiseai.com - render: No theological language in email body (no "pastor," "congregation," "church")
- render: CASL footer present with physical address and unsubscribe link
Failure signal: FROM address wrong → template not using FUNERAL_FROM constant. Email contains church language → copy not using funeral template.
Step 4 — Click → Preview Page
What happens: Prospect clicks demo link → /p/[token] stamps pro_website_clicked_at → 302 to /preview/[slug]?ref=outreach_{token}.
Where: churchwiseai-web/src/app/p/[token]/route.ts
Verifications:
- db:
SELECT pro_website_clicked_at, status FROM outreach_contacts WHERE id = '<prospect_id>'→ NOT NULL,status = 'clicked' - render: Preview page loads funeral template — NOT church template. Verify: no "Your congregation" language, uses "families" not "members," no cross/steeple imagery
Failure signal: Preview shows church template → vertical routing broken in PreviewClient.tsx.
Step 5 — Chatbot Interaction (Demo)
What happens: Funeral director types questions into the demo chatbot. AI responds with funeral-home-appropriate information.
Where: /api/chatbot/stream → demo tenant row → funeral knowledge base
Verifications:
- render: Type "What happens after someone passes away?" → chatbot describes intake process, not a church response
- render: Type "Do you offer direct cremation?" → chatbot responds with appropriate funeral-context answer (not "we don't offer that at our church")
- render: Type "I need grief support" → AI responds with composure and appropriate resources, does NOT use HEAR protocol theological language
Failure signal: Chatbot responds with church-coded language → vertical not isolated in chatbot stream route.
Step 6 — Demo Voice Call (Optional)
What happens: Prospect dials demo number, hears funeral-coded voice agent greeting.
Where: LiveKit voice agent → tenant_voice_agents demo row with vertical = 'funeral'
Verifications:
- code: Voice agent greeting: "Thank you for calling [Funeral Home Name]" — NOT "Thank you for calling [Funeral Home Church]"
- code: Agent tone is composed and dignified — no upbeat/enthusiastic tone
- db:
voice_call_logsrow created after call with correcttenant_id
Failure signal: Wrong vertical in voice greeting → routing broken. Voice agent is energetic/enthusiastic → tone prompt not applied for funeral vertical.
Step 7 — Email 2 (Day 3) and Email 3 (Day 7)
What happens: Follow-up emails sent per schedule. All use funeral-coded copy.
Where: /api/cron/outreach-followup/route.ts
Verifications:
- db:
SELECT email_2_sent_at, email_3_sent_at FROM outreach_contacts WHERE id = '<prospect_id>' - render: Email 2 subject: "The call that came in at 11:47pm" — verify no church language
- code: DNC gate:
outreach_contacts.email_do_not_contact = trueblocks all follow-ups
Step 8 — Reply / Book / Convert
What happens: Prospect clicks "Book a 20-min call" → cal.com/john-moelker/funeralwiseai-demo. Webhook records booking. Converted prospect moves to manual sales process (no self-serve checkout without setup call for funeral vertical).
Where: Cal.com webhook → outreach_contacts.status = 'booked'
Verifications:
- db:
SELECT status, booked_at FROM outreach_contacts WHERE id = '<prospect_id>'→status = 'booked',booked_atNOT NULL - code: Cal.com event type exists:
funeralwiseai-demo— verify at cal.com/john-moelker before first campaign
Failure signal: Booking not recorded → Cal.com webhook not configured. Self-serve checkout at /funeralwiseai/checkout is a parallel path — verify it routes correctly.
Step 9 — Email 4 Break-Up (Day 14)
What happens: Break-up email sent. Reply "remove me" → outreach_contacts.email_do_not_contact = true.
Where: /api/cron/outreach-followup/route.ts
Verifications:
- db:
SELECT email_4_sent_at FROM outreach_contacts WHERE id = '<prospect_id>'→ NOT NULL at day 14 - render: Email 4 subject: "Last note from me" — confirm no church language, secular close
5. What the Recipient Sees
Email 1: Subject: "What happens when a family calls [Funeral Home Name] at 2am?"
- From: hello@funeralwiseai.com
- Dignified, secular tone throughout
- CTA: "[HEAR [FUNERAL HOME NAME]'S DEMO →]" → preview page with funeral template
- Footer: CASL footer with physical address, unsubscribe link, source disclosure
Preview page: /preview/[slug] — funeral branded (no cross, no theological language), demo chatbot, demo voice number, "Book a Demo" CTA to Cal.com
6. Compliance & Unsubscribe
- Regime: CASL (Canadian prospects) + CAN-SPAM (US prospects)
- Unsubscribe link: Every email footer →
funeralwiseai.com/unsubscribe?token={{token}} - DNC gate:
outreach_contacts.email_do_not_contact = true— checked before every send - Physical address footer: Required. Must be confirmed with founder before first send.
- Source disclosure: "You're receiving this because [Funeral Home Name] appears in public business listings"
- Note: This vertical does NOT use
churches.email_do_not_contact— it has its own DNC state onoutreach_contacts
7. Failure Modes
| Failure | Signal | Alerting path |
|---|---|---|
| FK constraint on tenant_voice_agents | Provision fails at step 2 | Logged in provision.ts — Stream A blocker |
| Preview uses church template | Visual verification fails | Fix vertical routing in PreviewClient.tsx |
| FROM address is churchwiseai.com | Email reputation risk | Fix FUNERAL_FROM constant in send route |
| Theological language in email | Brand/tone violation | Copy review before first send |
| Cal.com event type missing | Booking link 404s | Founder creates event type manually |
| Physical address missing | CASL non-compliance | BLOCKER — do not send without address |
8. Verification Manifest
flow: cold-outreach-funeral
verifications:
- step: 1
verb: db
command: SELECT count(*) FROM outreach_contacts WHERE vertical='funeral' AND created_at >= now() - interval '7 days'
expect: ">0 after scrape run"
- step: 2
verb: render
command: "GET {preview_url}"
expect: "HTTP 200, funeral template, home name in title, no church imagery"
- step: 3
verb: db
command: SELECT status, email_1_sent_at FROM outreach_contacts WHERE id = '<id>'
expect: "email_1_sent, timestamp NOT NULL"
- step: 3
verb: render
command: "Inspect email source — FROM, body, footer"
expect: "FROM = hello@funeralwiseai.com, no theological language, CASL footer present"
- step: 5
verb: render
command: "Type 'What happens after someone passes away?' in demo chatbot"
expect: "Funeral-appropriate response, no church language"
- step: 8
verb: db
command: SELECT status, booked_at FROM outreach_contacts WHERE id = '<id>'
expect: "booked, booked_at NOT NULL after Cal.com booking"
9. Open Questions / Known Gaps
- Physical address for CASL footer not confirmed — BLOCKER
- Funeral scrape source not finalized — where are the 741 existing prospects scraped from? Confirm with founder
-
tenant_voice_agentsFK constraint — Stream A must resolve before funeral provisioning works - Funeral preview template — Stream A must ship before this flow can run end-to-end
- Cal.com
funeralwiseai-demoevent type — founder creates manually (seeknowledge/marketing/cal-com-event-types.md) - Resend domain verification for
funeralwiseai.com— check Resend dashboard -
outreach_contacts.email_do_not_contactcolumn — verify it exists (rule #18: check before using)