Skip to main content

Outreach Send Preflight Gate

Verified: 2026-06-11

Summary

Every cold-outreach BATCH send must pass the mandatory preflight gate before a single email goes out. src/lib/outreach/campaign-preflight.ts (runBatchPreflight) is wired into BOTH batch-send routes — /api/founder/drafts/batch-send-approved and /api/founder/send-batch — as a HARD BLOCK ahead of safeSendBatch; if pass is false the route refuses the whole batch and surfaces blockers. A CLI twin, scripts/campaign-preflight.mjs, runs the same gate from the terminal. Shipped PR #788 (2026-06-06); hardened by PRs #834, #835, #837, #839 (2026-06-09). Origin incident: a batch reached the approve list with 30 duplicate demos + 2 bounced addresses.

Fuller standard — do not duplicate: churchwiseai-web/docs/COLD_EMAIL_SEND_STANDARD.md is the complete send standard (throttle, env, signature, CASL footer). This doc is the knowledge-base pointer + the invariants agents must never regress.

Absolute policy — founder presses send

  • The founder personally previews/blesses every batch and presses send himself (CASL). Agents may stage, verify, and preflight — never send.
  • NO cron may ever send a campaign batch (feedback_never_cron_send_campaigns, P0 since the 2026-05-15 incident). Any new send path must be manual-trigger + founder-auth.

Single-source email rendering

  • renderChurchProspectEmailHtml() (src/lib/email.ts) feeds BOTH the preview UI and the actual send. NEVER re-inline a second template — preview-vs-send drift is how wrong emails ship. Local-business twin: renderBusinessProspectEmailHtml().
  • Golden email-render check: preflight renders each draft and asserts GOLDEN_REQUIRED / GOLDEN_FORBIDDEN (+ BUSINESS_GOLDEN_*) markers from src/lib/outreach/business-golden.ts — e.g. forbidden stale church pricing ($49.95/$54.95) in business sends. Add new forbidden strings there when a copy bug is found, in the same PR as the fix.

What the gate checks (two layers)

  1. Per-row checkGates (safe-send.ts) — row-level safety incl. the already-sent guard (PR #835: skip any row with sent_at unless an intentional resend), unsub, bounce, do-not-contact.
  2. Batch runBatchPreflight (campaign-preflight.ts) — batch-level problems per-row gates can't see: in-batch duplicates, NeverBounce coverage, cross-campaign already-sent, existing customers, generic/unpersonalized rows, golden-render markers. Read-only; performs no writes.

The campaign_members/leads model (local business) has a mirror gate: campaign-members-preflight.ts, enforced inside /api/founder/campaigns/[id]/send.

Send-ready accounting (don't over-count "ready")

Full gate for counting genuinely send-ready church prospects:

  • checkGates passes AND preflight passes, including: nb_result IN ('valid','catchall','unknown'), chatbot_validated, demo token present, plan set, email variant set, real prospect name (not generic);
  • AND exclude rows with status='skipped', a skip_reason, or sent_at.
  • Skipped rows have null nb_result by design — never count them as "needs NB verify" (re-verifying them is a no-op that wastes credits). True count on 2026-06-09 was 214. See memory/project_outreach_send_ready_accounting.

PR #834 added the client-safe outreachProspectSendability() predicate (src/lib/outreach-shared.ts) mirroring the server gate, wired into OutreachEngineView / SendBatchModal / DraftCard, so the outreach-engine UI "N ready" matches what the server will actually send (the premium_churches.plan join stays server-only). PR #839 made it SermonWise-aware (source='sermonwise_v1' rows are always demo_prep_status='not_run').

Gotchas

  • Preflight is a strict-superset BATCH BLOCKER: one bad row rejects the whole batch. Move un-sendable rows OUT of the drafted set (status='skipped' + skip_reason) before send — the per-row gate never gets a chance to drop them.
  • Mid-send hard bounces flip bounced_at/DNC, staling the modal's loaded cohort → preflight then refuses the batch; refresh and re-fire.
  • PR #837: /api/founder/send-batch has maxDuration=600 + 520s wall budget with partial/remainingProspectIds continuation — one press finishes a batch.