Skip to main content

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-outreach skill: research, draft, review commands live. Reply templates authored.
  • Phase 2 — Parallel research sub-agents + lint gate (merged 2026-04-12). dispatch-research.ts dispatches N parallel Claude agents; draft-emails.ts VOICE_BLOCKLIST + REQUIRED_ELEMENTS enforce 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-poll cron and outreach-followup cron 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.tsSeeds outreach_contacts — idempotent 739-row upsert with HMAC tokens
~/.claude/skills/church-outreach/scripts/dispatch-research.tsBatch-claims queued rows, dispatches parallel sub-agents, writes research_json
~/.claude/skills/church-outreach/scripts/draft-emails.tsRich/sparse template selection, VOICE_BLOCKLIST, REQUIRED_ELEMENTS lint, saveDraft()
pewsearch/web/src/lib/outreach-tokens.tsgenerateOutreachToken, lookupOutreachContactByToken with expiry check
pewsearch/web/src/app/starter-kit/[token]/page.tsxLanding page — kit download + Pro Website CTA
pewsearch/web/src/app/outreach/unsubscribe/[token]/page.tsxOne-click unsubscribe confirm — POST-only Server Action defeats email-scanner pre-clicks
churchwiseai-web/src/app/founder/[token]/outreach/OutreachWorkspace.tsxFounder review UI shell — draft list, filters, drawer, kill-switch banner
churchwiseai-web/src/lib/outreach-queries.tsselectActiveContacts() — the ONE DNC-baked read path (structural guard, !inner join, .eq('churches.email_do_not_contact', false))
churchwiseai-web/src/lib/outreach-gmail.tsGmail API client — draft creation, send, label management
churchwiseai-web/src/lib/outreach-watchtower.tsKill-switch logic — rolling 24h unsub rate; triggers campaign pause
churchwiseai-web/src/lib/outreach-attribution.tsTags premium_churches.acquired_via at conversion point
churchwiseai-web/src/app/api/cron/outreach-gmail-poll/route.tsPolls Gmail for draft→sent transitions, reply detection, bounce handling
churchwiseai-web/src/app/api/cron/outreach-followup/route.tsSequences 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, selectActiveContacts filters
  • outreach-gmail.test.ts — Gmail API client
  • outreach-gmail-poll.test.ts — poll cron
  • outreach-gmail-route-contract.test.ts — API route shapes
  • outreach-helpers.test.ts — helper utilities
  • outreach-funnel-stats.test.ts — funnel metrics
  • outreach-dashboard-alerts.test.ts — kill-switch alert logic
  • outreach-watchtower-dedup.test.ts — dedup logic
  • webhook-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 to outreach_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. See memory/feedback_never_test_against_real_tokens.md.
  • DNC must be checked structurally, not by convention. outreach-queries.ts:selectActiveContacts() uses a Supabase !inner join filtering churches.email_do_not_contact = false AND status != 'unsubscribed'. This is the ONE approved read path. Every new surface (new cron, new send route) MUST call assertSendable(contactId) as a pre-send guard. Grep churches.email_do_not_contact when 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.ts uses a queued → researching UPDATE gate, but it is not transactional across parallel agent sessions. Rule: one outreach agent at a time. TODO: wrap batch-claim in a single UPDATE ... WHERE status='queued' ... RETURNING for Postgres serialization.
  • Gmail OAuth scope. Existing founder_google_tokens OAuth covers Calendar + Drive. Outreach Gmail API needs gmail.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.local for MailerLite API access — agents can use it directly without sending founder to the dashboard.
  • ai-starter-kit.pdf hosting. The kit-download route redirects to /ai-starter-kit.pdf. Whether this lives in pewsearch/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_contact before sending. Full audit has not been documented or verified. TODO before going beyond the initial 739-church segment.
  • DMARC at p=none during warmup. Review at Week 3 end for upgrade to p=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.