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 + SermonWise →
outreach_contacts.replied_at/reply_category - Local business →
lead_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_category | Meaning |
|---|---|
interested | wants the demo, asks about pricing, books a call |
question | engaged but unclear intent |
not_interested | polite/explicit no |
unsubscribe | asks to stop — ALSO stamp suppression (step 4) |
auto_reply | OOO / autoresponder — do not count as a conversation |
other | anything 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');Ifreply_categoryhas 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). interestedlist = 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.