Outreach Campaigns — Operator Runbook
What this is
The system behind ChurchWiseAI's first cold email campaign to PewSearch-listed churches that have no website. It targets 739 churches, delivers a free AI Starter Kit via per-church HMAC-tokenized landing pages, and captures intent signals (click → download → Pro Website interest → reply → convert) in Supabase. First batch launches Monday, 2026-04-13. The infrastructure is reusable for every future segment (non-claimers, denominational waves, ITW cross-sell, bounced-welcome recovery) via the church-outreach skill.
Active campaign: no-website-churches-2026-04 (id 5f006309-da0a-4784-9314-17015264fa4e, target 739, status draft as of 2026-04-12).
When to use it vs MailerLite
| Use outreach-campaigns (this system) | Use MailerLite |
|---|---|
| Cold, one-to-one, research-personalized first touch | Newsletter broadcasts to opted-in subscribers |
| Compliance + do-not-contact-driven (implied-consent CASL basis) | Double-opt-in marketing funnels |
| Per-church HMAC tokens, per-church landing pages | Same content to thousands of lead-magnet opt-ins |
| Reply triage into founder inbox | Open-rate / click-rate automations |
One-off admin sends from the PewSearch dashboard go through Resend, not through this system.
System overview
Data flow on a single email:
pull-targets.tsseedsoutreach_contacts(one row per church, unique HMACstarter_kit_token, 30-day expiry)./church-outreach research Nmarksqueued → researching, dispatches N parallel sub-agents, writesresearch_json, moves toresearched(ordo_not_contactif signals present)./church-outreach draft Npicks rich vs sparse template, substitutes variables, runs the lint gate (draft-emails.ts:lintDraft), writesdraft_subject+draft_body, moves todrafted.- Founder reviews; agent updates to
approved. - Week 1: agent formats for Gmail Web UI paste. Week 2+: Gmail API sends from
john@pewsearch.com. Row moves tosent. - Recipient clicks →
pewsearch.com/starter-kit/[token]→outreach_contacts.clicked_atset, statuslink_clicked. - PDF download →
/api/outreach/kit-download/[token]→kit_downloaded_atset, statuskit_downloaded. - "Tell me more" Pro Website click →
/api/outreach/pro-website-click/[token]→pro_website_clicked. - Unsubscribe →
/outreach/unsubscribe/[token]→ POSTconfirmUnsubscribeServer Action →status='unsubscribed'+churches.email_do_not_contact=true.
Database schema
Two owned tables + three columns on churches (applied via pewsearch/migrations/20260412_outreach_tables.sql).
outreach_campaigns: id, name (unique), description, target_query_description, target_count, status (draft|active|paused|complete|cancelled), started_at, paused_at, completed_at, notes, created_at.
outreach_contacts: id, campaign_id, church_id, email, research_json, personalization_json, draft_subject, draft_body, subject_pattern (A|B|C), starter_kit_token (unique), starter_kit_token_expires_at, status (17 values — see below), sent_at, opened_at, clicked_at, kit_downloaded_at, pro_website_clicked_at, replied_at, reply_category, converted_at, unsubscribed_at, bounced_at, founder_notes, created_at, updated_at (trigger). Unique (campaign_id, church_id).
Status enum: queued, researching, researched, drafting, drafted, approved, scheduled, sent, bounced, opened, link_clicked, kit_downloaded, pro_website_clicked, replied, converted, unsubscribed, do_not_contact.
Indexes: campaign, church, status, token.
churches additions: email_do_not_contact (bool, default false, honored by every sender in the codebase), email_do_not_contact_reason, email_do_not_contact_at.
Current state snapshot (2026-04-12): 714 queued, 17 drafted, 8 do_not_contact.
Operational surfaces
| Surface | File | Purpose |
|---|---|---|
| Landing page | pewsearch/web/src/app/starter-kit/[token]/page.tsx | Kit download + below-fold Pro Website mention |
| Kit download | pewsearch/web/src/app/api/outreach/kit-download/[token]/route.ts:10-31 | Redirects to /ai-starter-kit.pdf, sets kit_downloaded_at |
| Pro Website click | pewsearch/web/src/app/api/outreach/pro-website-click/[token]/route.ts | POST, idempotent |
| Unsubscribe (confirm page) | pewsearch/web/src/app/outreach/unsubscribe/[token]/page.tsx | POST-only Server Action to defeat email-scanner pre-clicks |
| Unsubscribe (API) | pewsearch/web/src/app/api/outreach/unsubscribe/[token]/route.ts | Alternate POST endpoint, mirrors DNC flag to churches |
| HMAC helper | pewsearch/web/src/lib/outreach-tokens.ts:8-37 | generateOutreachToken, lookupOutreachContactByToken (with expiry check) |
| Seed script | ~/.claude/skills/church-outreach/scripts/pull-targets.ts | Campaign + 739-row idempotent upsert |
| Research orchestrator | ~/.claude/skills/church-outreach/scripts/dispatch-research.ts | Batch pull + recordResearch() |
| Drafter + linter | ~/.claude/skills/church-outreach/scripts/draft-emails.ts:18-94 | VOICE_BLOCKLIST, REQUIRED_ELEMENTS, saveDraft() |
| Sender (Week 1) | Gmail Web UI via john@pewsearch.com Send-as alias | Manual paste |
| Sender (Week 2+) | Gmail API (Phase 7 — not yet implemented) | Automated with kill-switch monitoring |
Sender identity: john@pewsearch.com is a Google Workspace domain alias of john@churchwiseai.com. MX + SPF + DKIM (google._domainkey, 2048-bit) + domain-verification TXT all live on Vercel DNS as of 2026-04-12 (Appendix C of the design spec).
The church-outreach skill
Location: ~/.claude/skills/church-outreach/. Commands:
| Command | One-liner | Status |
|---|---|---|
plan <segment> | Size the segment, create campaign row, propose calendar | Stub (Phase 7) |
research <batch_size> | Dispatch N parallel sub-agents, write research_json | Live |
draft <batch_size> | Rich/sparse template, lint gate, write draft_* | Live |
review | Founder approves drafts one at a time | Manual (Phase 7 UI deferred) |
send <ids> | Respect caps + time-of-day + kill switches | Week 1: manual paste |
status | Sent/replied/unsub/convert dashboard | Manual SQL (stub) |
review-week | Friday analysis, copy iteration | Manual (stub) |
Templates: templates/email-rich-data.md, templates/email-sparse-data.md, templates/subject-patterns.md. Reply templates (11): templates/reply-templates/*.md — send-me-the-kit, tell-me-more, want-to-talk, send-info-later, what-does-it-cost, skeptical-of-ai, we-have-facebook, we-are-rural, not-interested, wrong-person, plus the README.md. Founder edits these for voice before Monday launch.
References: references/voice-principles.md (HEAR + voice linter regexes), references/compliance-rules.md (CASL + CAN-SPAM), references/cadence-rules.md (caps + kill switches).
Weekly cadence
| Week | Days | Daily cap | Batch | Founder behavior |
|---|---|---|---|---|
| 1 (Apr 13–17) | Mon–Fri | 20 | 100 | Every email researched + approved |
| 2 (Apr 20–24) | Mon–Fri | 50 | 250 | Template locked, sample-review 5, bulk-approve |
| 3 (Apr 27–May 1) | Mon–Fri | 100 | ~389 | Full cadence; list exhausted ~Apr 30 |
Send window: 9:00 AM – 2:00 PM recipient local time (bucket by churches.state_code). Never weekends, never Holy Week. Day-1 throttle: 5 @ 9 AM, 5 @ 10 AM, 10 @ 11 AM to avoid new-sender spike.
Compliance — the 5 musts in every email
Enforced by the drafter's REQUIRED_ELEMENTS lint (draft-emails.ts:31-41):
- Accurate From/Reply-To:
John Moelker <john@pewsearch.com>. - Physical address:
ChurchWiseAI LTD · 125 Concession Street, Ingersoll, ON N5C 1G2. - One-click unsubscribe URL containing
pewsearch.com/outreach/unsubscribe/. - Relationship disclosure:
"You're receiving this because ..."/"You're getting this because ..."naming the PewSearch listing. - Non-misleading subject (no
Re:/FW:on first touch).
CASL basis: conspicuously-published pastoral email on Google Business profile = 24-month implied consent. Prefer role addresses (office@, info@, churchname@) over personal @gmail.com. Every sender in the codebase MUST check churches.email_do_not_contact before sending — grep for the column name when adding a new send path.
Voice principles — HEAR applied to cold outreach
Hear open with one true specific fact from research — never "Hope this finds you well." Empathize without flattery (no compliments about their logo/mission). Advance — offer the gift, let the landing page explain. Respond — invite a reply even if they never click ("Would love to hear back either way"). And: no theology games. Never "the Lord led" / "God put you on my heart." The only honest hook is that they're listed in PewSearch without a website.
Voice linter auto-rejects (from draft-emails.ts:18-29): hope this finds/well, circle back, touch base, jump on a (quick) call, the Lord led/put on my heart, God led/brought/placed, act now/fast/today, limited time, We help churches.
Body 120–220 words. Subject 30–60 chars. Sign-off is John Moelker then Founder, ChurchWiseAI + PewSearch on the next line — no "Cheers,", no "Best,".
How to start a new campaign
/church-outreach plan <segment-name>— agent queries the segment, createsoutreach_campaignsrow, proposes calendar. (Stub today; for now seed manually via a script mirroringpull-targets.ts.)- Founder approves calendar + copy variants.
/church-outreach research 20— 3–5 min wall-clock via parallel sub-agents./church-outreach draft 20— lint gate enforces compliance + voice; failures return to drafter, not founder./church-outreach review— founder approves/edits each draft./church-outreach send <ids>— Week 1 formats for Gmail paste; Week 2+ via Gmail API./church-outreach statusdaily,/church-outreach review-weekeach Friday.
Quick status SQL:
SELECT status, count(*) FROM outreach_contacts
WHERE campaign_id = '5f006309-da0a-4784-9314-17015264fa4e'
GROUP BY status ORDER BY count DESC;
Rolling 24h unsubscribe rate (kill-switch at 3%):
SELECT
count(*) FILTER (WHERE unsubscribed_at > now() - interval '24 hours') AS unsubs_24h,
count(*) FILTER (WHERE sent_at > now() - interval '24 hours') AS sent_24h,
round(100.0 *
count(*) FILTER (WHERE unsubscribed_at > now() - interval '24 hours')
/ NULLIF(count(*) FILTER (WHERE sent_at > now() - interval '24 hours'), 0)
, 2) AS unsub_pct
FROM outreach_contacts
WHERE campaign_id = '5f006309-da0a-4784-9314-17015264fa4e';
How to handle common reply scenarios
Pick the matching template in ~/.claude/skills/church-outreach/templates/reply-templates/ and adapt:
- "Send me the kit / yes please" →
send-me-the-kit.md, re-send the unlock URL, setreply_category='send-kit'. - "Tell me more" →
tell-me-more.md, flag to founder, setreply_category='question'. - "Let's talk / schedule a call" →
want-to-talk.mdwith Cal.com link. - "What's the cost?" →
what-does-it-cost.md— point to pricing page, no hard pitch. - "Not interested" →
not-interested.md, setstatus='do_not_contact', no reply. - "Wrong person / no longer here" →
wrong-person.md, setstatus='do_not_contact', founder note. - Unsubscribe in reply text → honor immediately, same DB write as unsubscribe endpoint.
Every reply: update replied_at + reply_category. Founder-action replies surface in the admin UI (Phase 7).
Metrics to watch + kill switches
Track: delivered, clicked unlock link, kit downloaded, Pro Website clicked, replied, unsubscribed, converted (premium_churches.acquired_via='outreach_2026_04').
Do NOT track: open rate via pixel — Apple Mail Privacy made it meaningless.
Hard kill switches (auto-pause):
- Rolling 24h unsubscribe rate > 3% of sent.
- Any Gmail spam complaint via FBL.
- Gmail disables
john@pewsearch.comsending → pause 48h, resume at 50% prior volume. - DKIM/SPF/DMARC check fails.
Soft stops (flag founder):
- Reply rate < 3% after 100 sends → copy isn't landing, rewrite.
- Kit-download rate < 10% of clicks → landing page issue.
- 30-day conversion = 0% despite healthy replies → CTA or Pro Website pitch weak.
Benchmarks: reply 8–15% good / <3% bad; unsub <1% target / 1–2% acceptable / >2% pause / >3% hard stop; 30-day conversion 1–3% is strong at this ACV.
Known hazards
- Concurrent-agent races. Multiple Claude agents in parallel sessions can overlap on
research Nand double-pick the samequeuedrows.dispatch-research.ts:48-52uses aqueued → researchingUPDATE gate, but it is not transactional across sessions. Mitigation: one outreach agent at a time. When starting, drop a sentinel file likeC:/dev/.outreach-agent-runningand check for it beforeresearch/draftcommands. TODO: wrap the batch-claim in a singleUPDATE ... WHERE status='queued' ... RETURNINGstatement so Postgres serializes claimants. - Slug / state-code mismatches. PewSearch listing URLs in email footers resolve via
pewsearch.com/churches/{slug}. A change to slug-routing 2026-04-12 introduced aslug_redirectstable + route fallback; verify every sent email's listing URL 404-checks before send in Week 1. If a draft references a slug that no longer resolves, fix the slug inchurches, regenerate the draft, re-lint. - Email deliverability / warmup. We are brand-new on
pewsearch.comGmail outbound. DMARC isp=nonefor warmup — review at Week 3 for upgrade top=quarantine. Hold the 20/50/100 ramp; do not accelerate even if Week 1 reply rate looks good. Any Gmail postmaster warning → pause immediately. - Tradition-specific voice landmines. Voice/title conventions vary: Churches of Christ (CoC) use "Minister" not "Pastor"; Catholic/Orthodox priests are "Father"; Episcopalian priests are "The Rev."; some Baptist churches use "Brother"; Quaker/Friends meetings have no pastor. Drafter defaults to
Hi therewhenpastor_name_confidence < high— never gamble. If research returns a denomination-appropriate title, use it verbatim fromresearch_json.pastor_name.
Monday launch readiness checklist
Before first send Monday 2026-04-13 9 AM ET:
-
outreach_campaigns+outreach_contactsmigration applied (status snapshot shows 714 queued — confirmed). -
OUTREACH_TOKEN_SECRETset in production env on bothpewsearch.comandchurchwiseai.comVercel projects (useprintf, notecho). -
/starter-kit/[token]loads, click recordsclicked_at. -
/api/outreach/kit-download/[token]redirects to/ai-starter-kit.pdf(TODO: confirm PDF is uploaded topewsearch/web/public/ai-starter-kit.pdf— design spec open-question #2 not yet resolved). -
/outreach/unsubscribe/[token]renders confirm button, POST writes bothoutreach_contactsandchurches.email_do_not_contact. - Every existing sender in the codebase (Resend welcome, Stripe webhook emails, MailerLite stub) checks
churches.email_do_not_contactbefore sending. (TODO: full audit not yet documented.) - DKIM passes for
pewsearch.com(verified 2026-04-12). - Gmail Send-as
john@pewsearch.comtested end-to-end. - 20 drafts for Monday researched, drafted, lint-passed, founder-approved. (Current DB: 17 drafted — need 3 more before Monday.)
- 10 reply templates edited to founder's voice.
-
ai-starter-kit.pdfhosted and downloadable.
Post-launch: Week 2 transition to Gmail API + admin UI
Spec Phases 7–9 (deferred until Week 1 data is in):
- Gmail API OAuth for
john@pewsearch.comstored in churchwiseai-web env. - Cron-style job reads
status='scheduled', respects daily cap, time-of-day bucket, kill switches, sends, writessent_at. - Gmail push notifications (Pub/Sub) →
/api/outreach/replywebhook → Haiku 4.5 classifier →reply_category, auto-responder via Resend forsend-kit-link-again, founder escalation for substantive replies. - Admin UI at
churchwiseai.com/admin/[token]/outreach— draft review, bulk-approve, random-sample-review, live metrics header. (Spec referenceschurchwiseai-web/src/app/admin/[token]/outreach/page.tsx— not yet created.)
Verify Gmail Workspace daily-send quota supports the Week 3 ceiling of 100/day. Standard Workspace = 2000/day, so we are safe at 100.
Appendix — sample emails
Rich-data (Greater Nazaree Baptist, Mobile AL, Pattern A subject)
Subject: Greater Nazaree's listing on PewSearch
Hi Pastor Williams,
28 Google reviews averaging 4.8 stars says your people love what Greater
Nazaree is doing in Mobile — rare for a Baptist church of your size.
I noticed your listing on PewSearch doesn't link to a website, which is
common for Baptist churches in the South — most pastors I've talked to
either haven't had the time or haven't found a web tool that speaks
their language.
We put together a short guide called the AI Starter Kit for pastors in
exactly this spot. Normally $4.95 — for the 739 churches we're writing
to this month, Greater Nazaree included, it's free:
→ https://pewsearch.com/starter-kit/a8f2-greater-nazaree-mobile
If at some point you'd like something more, PewSearch's Pro Website
($19.95/mo, greaternazaree.pewsearch.com) includes a cinematic hero
video we hand-render from one photo of your church — typically $500+
from a freelance videographer. No pressure; the kit stands alone.
Would love to hear back either way.
John Moelker
Founder, ChurchWiseAI + PewSearch
john@pewsearch.com
---
ChurchWiseAI LTD · 125 Concession Street, Ingersoll, ON N5C 1G2
You're getting this because Greater Nazaree is listed at
pewsearch.com/churches/greater-nazaree-baptist-church-mobile-al.
Unsubscribe: https://pewsearch.com/outreach/unsubscribe/a8f2-greater-nazaree-mobile
Sparse-data (Sharon UMC, Shelby NC, Pattern B subject)
Subject: A note from PewSearch about Sharon UMC
Hi there,
I was looking through the Methodist churches in Shelby listed on PewSearch
and noticed Sharon UMC doesn't have a website attached to its listing.
That's common — especially for churches that have been around longer than
the modern "every church needs a website" assumption. I'm not here to
argue for or against one.
What I did put together is a short guide called the AI Starter Kit — a
plain-English look at what an AI assistant can actually do for a church
(answer phone calls, log prayer requests, help visitors find service
times). Normally $4.95. Here's a free copy for Sharon UMC:
→ https://pewsearch.com/starter-kit/c4e1-sharon-umc-shelby
If it's useful, great. If not, feel free to ignore this — I won't send a
follow-up unless you want one.
John Moelker
Founder, ChurchWiseAI + PewSearch
---
ChurchWiseAI LTD · 125 Concession Street, Ingersoll, ON N5C 1G2
You're getting this because Sharon UMC is listed at
pewsearch.com/churches/sharon-united-methodist-church-shelby-nc.
Unsubscribe: https://pewsearch.com/outreach/unsubscribe/c4e1-sharon-umc-shelby
TODO: Admin UI at churchwiseai.com/admin/[token]/outreach is referenced in the spec (Phases 7–9) but not yet implemented. Spec-vs-impl: kit-download route currently redirects to /ai-starter-kit.pdf but the PDF's hosting location (Supabase Storage vs static file) is still open (spec open-question #2). Cross-sender DNC audit (Resend welcomes, Stripe webhooks, MailerLite) is required by compliance but not yet enumerated in any doc.