Outreach Campaign Send Flow
Summary
The outreach campaign send flow is the full pipeline for sending CASL/CAN-SPAM-compliant, HEAR-protocol-voiced cold emails to churches in the PewSearch database. It begins with a church-outreach skill command that dispatches N parallel Claude sub-agents — one per church — for deep research (Google Business reviews, denomination, pastor name confidence, website absence). Sub-agents write research_json into outreach_contacts, the drafter synthesizes a personalized 120–220 word email from rich or sparse templates, and the founder reviews every draft individually in the workspace at churchwiseai.com/founder/[token]/outreach before any message is approved. Approved messages are sent via the john@pewsearch.com Gmail alias (copy-paste in Week 1; Gmail API via cron in Week 2+), and every send, click, kit-download, reply, and unsubscribe is state-tracked per recipient in outreach_contacts. A kill-switch banner auto-pauses sending if the rolling 24h unsubscribe rate exceeds 3%.
Flow
Phase tracker
- Phase 1 — Manual drafting + CASL-compliant templates (merged 2026-04-12).
church-outreachskill:research,draft,reviewcommands live. Reply templates authored. - Phase 2 — Parallel research sub-agents + lint gate (merged 2026-04-12).
dispatch-research.tsdispatches N parallel Claude agents;draft-emails.tsVOICE_BLOCKLIST+REQUIRED_ELEMENTSenforce compliance automatically. - Phase 3 — Founder approval workspace UI + Gmail API send (in progress, 2026-04-13 plan).
churchwiseai-web/src/app/founder/[token]/outreach/page built. Gmail OAuth scope expansion (gmail.compose,gmail.modify,gmail.readonly) requires one-time founder reconnect.outreach-gmail-pollcron andoutreach-followupcron wired. - Phase 4 — Reply auto-classification + followup sequences (planned). Haiku 4.5 classifier on Gmail poll; auto-responder for
send-kit-link-again; founder escalation surface for substantive replies. Analytics funnel, A/B subject split.
Code files
Authoritative list in frontmatter code-files:. Key roles:
| File (relative to C:/dev) | Role |
|---|---|
~/.claude/skills/church-outreach/scripts/pull-targets.ts | Seeds outreach_contacts — idempotent 739-row upsert with HMAC tokens |
~/.claude/skills/church-outreach/scripts/dispatch-research.ts | Batch-claims queued rows, dispatches parallel sub-agents, writes research_json |
~/.claude/skills/church-outreach/scripts/draft-emails.ts | Rich/sparse template selection, VOICE_BLOCKLIST, REQUIRED_ELEMENTS lint, saveDraft() |
pewsearch/web/src/lib/outreach-tokens.ts | generateOutreachToken, lookupOutreachContactByToken with expiry check |
pewsearch/web/src/app/starter-kit/[token]/page.tsx | Landing page — kit download + Pro Website CTA |
pewsearch/web/src/app/outreach/unsubscribe/[token]/page.tsx | One-click unsubscribe confirm — POST-only Server Action defeats email-scanner pre-clicks |
churchwiseai-web/src/app/founder/[token]/outreach/OutreachWorkspace.tsx | Founder review UI shell — draft list, filters, drawer, kill-switch banner |
churchwiseai-web/src/lib/outreach-queries.ts | selectActiveContacts() — the ONE DNC-baked read path (structural guard, !inner join, .eq('churches.email_do_not_contact', false)) |
churchwiseai-web/src/lib/outreach-gmail.ts | Gmail API client — draft creation, send, label management |
churchwiseai-web/src/lib/outreach-watchtower.ts | Kill-switch logic — rolling 24h unsub rate; triggers campaign pause |
churchwiseai-web/src/lib/outreach-attribution.ts | Tags premium_churches.acquired_via at conversion point |
churchwiseai-web/src/app/api/cron/outreach-gmail-poll/route.ts | Polls Gmail for draft→sent transitions, reply detection, bounce handling |
churchwiseai-web/src/app/api/cron/outreach-followup/route.ts | Sequences follow-up cadence for non-responders |
Tests
No entries in knowledge/tests/registry.yaml yet. Unit tests exist in churchwiseai-web/src/test/unit/:
outreach-queries.test.ts— DNC guard,selectActiveContactsfiltersoutreach-gmail.test.ts— Gmail API clientoutreach-gmail-poll.test.ts— poll cronoutreach-gmail-route-contract.test.ts— API route shapesoutreach-helpers.test.ts— helper utilitiesoutreach-funnel-stats.test.ts— funnel metricsoutreach-dashboard-alerts.test.ts— kill-switch alert logicoutreach-watchtower-dedup.test.ts— dedup logicwebhook-outreach-attribution.test.ts— conversion attribution
Also: pewsearch/web/src/test/unit/outreach-tokens.test.ts — HMAC token generation + expiry.
TODO: Register these in knowledge/tests/registry.yaml. No Playwright end-to-end spec for the outreach funnel yet — required before scaling beyond Week 1 manual sends.
Decisions
2026-04-13 — Founder workspace shipped same day as campaign launch. The 2026-04-13 workspace plan (docs/superpowers/plans/2026-04-13-outreach-send-workspace.md) drove same-day build of the approval UI, Gmail draft API, and DNC-baked query layer. MVP for the 9/10 AM send slots was copy-paste via clipboard; Gmail-draft automation targeted the 11 AM slot.
2026-04-09 — CASL/CAN-SPAM footer standardized. Physical address (ChurchWiseAI LTD · 125 Concession Street, Ingersoll, ON N5C 1G2) and pewsearch.com/outreach/unsubscribe/[token] URL are enforced at the draft lint gate — not just guidelines but hard blockers (REQUIRED_ELEMENTS in draft-emails.ts).
Gotchas
- Never hit a real recipient's token when testing. Every funnel URL hit (
/starter-kit/[token],/api/outreach/kit-download/[token],/api/outreach/pro-website-click/[token],/outreach/unsubscribe/[token]) writes state tooutreach_contacts, corrupting campaign analytics. Always insert a synthetic row against the demo church UUID (00000000-0000-4000-a000-000000000001) and test against that. This applies to prompts passed to dispatched sub-agents too — agents will curl any URL you hand them. Seememory/feedback_never_test_against_real_tokens.md. - DNC must be checked structurally, not by convention.
outreach-queries.ts:selectActiveContacts()uses a Supabase!innerjoin filteringchurches.email_do_not_contact = falseANDstatus != 'unsubscribed'. This is the ONE approved read path. Every new surface (new cron, new send route) MUST callassertSendable(contactId)as a pre-send guard. Grepchurches.email_do_not_contactwhen adding any new send path. - CAN-SPAM requires physical postal address in every email. Address:
ChurchWiseAI LTD · 125 Concession Street, Ingersoll, ON N5C 1G2. The lint gate enforces this — but if you add a new template or send path outside the skill, you must include it manually. - CASL unsubscribe must be honored within 10 business days (the UI honors immediately). The
/outreach/unsubscribe/[token]page is POST-only Server Action — never GET — to prevent email-scanner pre-clicks from auto-unsubscribing recipients. - Concurrent-agent races on
research N.dispatch-research.tsuses aqueued → researchingUPDATE gate, but it is not transactional across parallel agent sessions. Rule: one outreach agent at a time. TODO: wrap batch-claim in a singleUPDATE ... WHERE status='queued' ... RETURNINGfor Postgres serialization. - Gmail OAuth scope. Existing
founder_google_tokensOAuth covers Calendar + Drive. Outreach Gmail API needsgmail.compose,gmail.modify,gmail.readonly. Requires one-time founder reconnect before first Gmail-draft automation. Week 1 uses copy-paste as fallback. - Send-As alias MIME header. Gmail API draft MIME must explicitly set
From: John Moelker <john@pewsearch.com>— Gmail only honors Send-As aliases via raw MIME, not the default sender. - MailerLite is NOT used for cold outreach. MailerLite handles opted-in newsletter subscribers. Cold, research-personalized, token-bearing first-touch emails go through this system only. JWT key is in
churchwiseai-web .env.localfor MailerLite API access — agents can use it directly without sending founder to the dashboard. ai-starter-kit.pdfhosting. The kit-download route redirects to/ai-starter-kit.pdf. Whether this lives inpewsearch/web/public/or Supabase Storage is still an open question (runbook open-question #2) — verify before Week 2 scaling.- Cross-sender DNC audit is incomplete. Resend welcome emails, Stripe webhook emails, and MailerLite stubs are all required to check
churches.email_do_not_contactbefore sending. Full audit has not been documented or verified. TODO before going beyond the initial 739-church segment. - DMARC at
p=noneduring warmup. Review at Week 3 end for upgrade top=quarantine. Do not accelerate send ramp even if Week 1 reply rate looks strong.
Recent activity
2026-04-13 — Phase 3 workspace shipped; Gmail API routes, DNC-baked query layer, kill-switch banner, and copy-to-clipboard MVP all live. Campaign no-website-churches-2026-04 (739 targets) entered active send cadence.