name: outbound-policy-per-provider description: Apply the correct outbound pacing and kill-switch policy before sending or changing any proactive path. Dispatch routes on outbound_queue.transport — cloud_api follows Meta tiered conversation limits + quality-rating + the F4 template guard (no free-text outside the 24h window); whapi keeps the legacy <=10/hr cap. Kill switches are the G4 surface x direction flags (solo_proactive_paused / solo_reactive_paused / family_reactive_paused) read via readBotFlag, with BOT_SILENT_MODE as master kill. Use when relaxing the cap, adding an outbound message_type, editing the drainer, or wiring a new net.http_post path. G4 implementation is blocked on G3<-G2.
This skill is the operating playbook for master-plan Workstream G4 + the pacing already shipped in F4. Source of truth: docs/plans/2026-05-09-recovery-hardening-plan.md. Never fork its vocabulary.
(a) Two pacing policies keyed on outbound_queue.transport
cloud_api
Follows Meta tiered conversation limits: 250 → 1K → 10K → 100K business-initiated conversations per 24h window, gated by quality-rating monitoring. The F4 cloud_api_template_guard CHECK constraint (NOT (transport='cloud_api' AND within_care_window=FALSE AND template_id IS NULL)) makes free-text outside the 24h customer-care window structurally impossible at the DB layer — the row cannot be inserted without a template.
whapi
Keeps the legacy ≤10/hr GLOBAL cap enforced inside drain_outbound_queue_inner. Drain is priority-ordered (0=welcome, 1=recovery, 2=recovery_group, 3=google_reconnect_prompt, 4=google_push_retry, 5=everything else), with FOR UPDATE SKIP LOCKED for race-safety. Within each priority tier, oldest queued_at fires first.
(b) G4 kill-switch flag matrix
Three bot_settings flags govern the G4 surface × direction matrix, read via readBotFlag (10s cache TTL — flip takes effect within one drain cycle, no redeploy needed):
| Flag | Scope |
|---|---|
solo_proactive_paused |
1:1 proactive sends (reminders, nudges, reconnect prompts) |
solo_reactive_paused |
1:1 reactive replies |
family_reactive_paused |
Family-group reactive replies |
BOT_SILENT_MODE env var = master kill. Setting it true blocks all outbound via sendAndLog + drain_outbound_queue_inner. Layers 2–4 (the bot_settings flags + unscheduled crons) must still be set independently — BOT_SILENT_MODE=false during recovery posture is intentional (Boti formula: reactive stays ON, proactive stays OFF).
Family proactive has NO flag and no column in the matrix — it never exists. Permanent Boti reactive-only posture for family groups is by design.
(c) Cloud API 24h customer-care window + template constraint
The 24h customer-care window opens when the user sends any inbound message. Inside that window, Sheli may reply with free-form text. Outside that window, any business-initiated send MUST use an approved Utility template (template_id + template_variables populated, within_care_window=FALSE). The F4 cloud_api_template_guard CHECK enforces this at the DB layer — a missing template_id on an out-of-window cloud_api row causes an INSERT failure, not a silent policy violation.
When building a new outbound message type on cloud_api, always set within_care_window explicitly. Do not assume the window is open.
(d) Stray-path audit
Before declaring an outbound change safe, run:
SELECT proname FROM pg_proc WHERE prosrc LIKE '%http_post%';
Current legitimate callers and their status:
| Function | Status |
|---|---|
drain_outbound_queue_inner |
Canonical sender — all WhatsApp sends must flow through here |
fire_due_reminders_inner |
Temporary — retires at F2 Phase 4 cutover |
trigger_google_calendar_nightly_sync |
Allowlisted — read-only cache refresh, not a WhatsApp send |
Any function returned by the query that is not in this table is a stray outbound path and is a policy violation. The 2026-04-17 ban was triggered at ~40 auto-replies/hr from exactly this failure mode.
(e) Checklist for adding a new message_type or outbound row
Before inserting a new row into outbound_queue or adding a new message_type value:
- Set
transportexplicitly. If the Solo number (Cloud API) is not yet live, usewhapi. - If
transport='cloud_api'and the 24h window may be closed: populatetemplate_id+template_variables, setwithin_care_window=FALSE. Thecloud_api_template_guardCHECK will reject the row otherwise. - Respect the
render_shapeCHECK constraint. Eachmessage_typehas a documented NOT NULL combination (e.g.google_reconnect_promptrequiresbody+chat_id). Match it exactly. - Do not open a second outbound path outside
outbound_queue. All sends must go through the canonical drainer. Adding a directnet.http_postin a new function or trigger is a ban-risk violation regardless of volume. - Add the new
message_typeto theoutbound_queue.message_typedomain/CHECK if the column has a constraint — the INSERT will fail silently otherwise. - Verify the drain priority assignment. New types default to priority 5 (lowest). Assign a lower number only if there is a documented business reason.
- Run the stray-path audit query from section (d) after any schema or function change.
G4 flag IMPLEMENTATION is blocked on master-plan G3←G2 (operator must acquire the Solo number first). Do not invent the flags ahead of the number cutover — this skill documents the policy; Roi implements G4 when G3 unblocks.