Skip to main content

Runbook — Weekly Outreach Reply Stamping

Created: 2026-06-11 · Cadence: weekly (Friday) · Owner: founder or session agent Why: Replies are the #1 cold-email outcome, and as of 2026-06-10 replied_at/reply_category had NEVER been written (0 of 5,767 outreach_contacts rows). All replies live only in Gmail, so variant comparison optimizes for bot-polluted clicks instead of conversations, and the funnel cannot distinguish 0 replies from 50. Source: AUDIT_demo_process_and_paid_ads_2026-06-10.md (P0 #6).

Scope

Stamp replies for BOTH outreach models:

  • Church + SermonWiseoutreach_contacts.replied_at / reply_category
  • Local businesslead_events + leads.status

This is a manual SOP first; automation (Resend inbound routing or Gmail API poll) is a P2 follow-up.

Procedure

1. Find replies in Gmail (since last pass)

Search each sending identity (Gmail MCP or web):

to:(john@churchwiseai.com) in:inbox after:YYYY/MM/DD
to:(john@sermonwise.ai) in:inbox after:YYYY/MM/DD
to:(john@wiseaiagency.com) in:inbox after:YYYY/MM/DD

Also search subject:re: against the campaign subject lines if replyTo routing makes the to: filter miss threads.

2. Classify each reply

reply_categoryMeaning
interestedwants the demo, asks about pricing, books a call
questionengaged but unclear intent
not_interestedpolite/explicit no
unsubscribeasks to stop — ALSO stamp suppression (step 4)
auto_replyOOO / autoresponder — do not count as a conversation
otheranything else

Verify the live column accepts these values before first use (rule #18): SELECT column_name, data_type FROM information_schema.columns WHERE table_name='outreach_contacts' AND column_name IN ('replied_at','reply_category'); If reply_category has a CHECK/enum, conform to it.

3. Stamp church/SermonWise replies

One UPDATE per replying prospect (match by sender email; fall back to church name in the thread):

UPDATE outreach_contacts
SET replied_at = COALESCE(replied_at, '<first-reply-timestamp>'),
reply_category = '<category>'
WHERE email = '<sender-email>' AND sent_at IS NOT NULL;

Keep replied_at as FIRST reply time (the COALESCE preserves it on re-runs).

4. Unsubscribe / do-not-contact asks

UPDATE outreach_contacts
SET unsubscribed_at = COALESCE(unsubscribed_at, now()),
status = 'do_not_contact',
replied_at = COALESCE(replied_at, now()),
reply_category = 'unsubscribe'
WHERE email = '<sender-email>';

For LB: set leads.is_suppressed = true for the matching lead as well.

5. Stamp local-business replies

Verify lead_events accepted event types first (writers: send-members.ts, lead-unsubscribe.ts, upsert-lead.ts), then insert a replied event for the lead and promote leads.status to engaged (statuses observed in GCC: new/active/engaged).

6. Log the pass

Append one line to DECISION_LOG.md: date, replies stamped per cohort, category counts, any interested leads (those go straight to the GCC leads view / founder follow-up).

Reporting it enables

  • Reply rate per variant: SELECT email_variant, COUNT(*) FILTER (WHERE replied_at IS NOT NULL)::float / COUNT(*) FROM outreach_contacts WHERE sent_at IS NOT NULL GROUP BY 1;
  • Conversation rate per campaign (exclude auto_reply).
  • interested list = the warm queue for founder follow-up.

Guardrails

  • READ Gmail, WRITE only the columns above. Never send from this SOP.
  • First reply wins on replied_at — never overwrite.
  • An unsubscribe reply is honored immediately regardless of CASL timing windows.