TEASEDocs
ProductsClubTroubleshooting

Shadow sends and the kill switch

Why a message or campaign comes back "shadow", "deferred", or "stopped" instead of "sent" — and which of several gates is actually holding it back.

Every send path in Club — a 1:1 reply, a scheduled broadcast, an auto-message — routes through the same firewall-then-engine pipeline, but each layer can hold a message back for a different reason. This page is the map of which "not sent" you're looking at and what actually controls it.

The 1:1 send status ladder

POST /inbox/send (and retry) returns exactly one of six statuses:

statusWhat it meansWhat the chatter sees
blockedThe content firewall refused it. Nothing was queued or logged."Not sent: …" with a concrete, fixable reason where the category maps to one.
stoppedThe cross-tenant emergency brake is down."Not sent: sending is stopped for this account right now" — no retry offered, because retrying can't help.
deferredThe engine's single send slot was busy with a previous message."Not sent: the previous message is still going out. Tap Retry."
shadowPassed every check, but live sending is off account-wide."Saved — it goes out once sending opens."
queuedLive sending is on; the engine accepted the send and delivery is async."Sending…"
sentA confirmed, acknowledged delivery."Delivered to the fan."

"shadow" is the default state of the whole product today

A global ENGINE_WRITE_READY setting controls whether 'live' mode resolves to a real sending executor or to a stub that logs intent and sends nothing. While it's off — which it is by default — every 1:1 send, broadcast send, and auto-message send in the product lands as shadow, never sent. This is the intended current state of the whole system, not a per-account misconfiguration to chase down.

stopped is a different lever from shadow

stopped comes from a separate, cross-tenant kill switch an operator can throw to halt every workspace at once — genuinely an emergency brake, not a per-account setting a workspace owner toggles. It's distinct from a per-owner opt-out (AutomationOptIn.kill_switch), which only affects that one workspace's automation sends. If a send is coming back stopped, the fix isn't anything on the workspace side.

Broadcasts have their own three-state lane, not the same two flags

Scheduling a campaign under /api/admin/broadcasts doesn't reuse the 1:1 send ladder directly — it rides a lane with three states, surfaced on the schedule/list responses:

LaneMeaning
offThe broadcast-sending tick isn't running at all.
shadowThe tick is running, passes every message through every door, and sends nothing — a full rehearsal.
liveThe tick is running and the engine is ready; sends go out for real.

Getting to live requires two separate gates to agree: a settings flag the lane itself checks, and a file-based readiness flag a watchdog process sets once it's satisfied the engine is actually healthy. Either one alone leaves the lane at shadow — this is deliberately fail-closed on both sides, not an oversight to reconcile.

A rehearsal doesn't mark anything as sent

In shadow mode the lane's tick doesn't leave any row in a state that looks terminal — it runs the full check sequence and reports what would have gone and what would have been blocked, without stamping any send row as done. This is intentional: if it silently marked shadow sends as terminal, turning live sending on later would mean those messages never actually go, because the system would already believe they did.

Track-B automation campaigns need to pass a soak, not just flip a flag

Auto-messages and the in-chat "Broadcast" composer both run on a third, separate engine (Track B / core.automation) with its own path to going live: a campaign starts in shadow and status: 'draft', and before it's allowed to send for real it has to clear a soak — a harness that checks its shadow-run history for full audience coverage, zero firewall escapes (a blocked-verdict message that somehow still sent — this must always be zero), acceptable send pacing, and a passing evaluation window. All four have to pass before a soak_passed_at stamp is written; short of that, the campaign simply cannot promote to live no matter what toggle is flipped in the UI.

What to actually check, in order

  1. Is this a 1:1 send, a /broadcasts campaign, or an auto-message / in-chat "Broadcast"? Each has its own gate above — don't diagnose one using another's model.
  2. For a 1:1 send: read the exact status the API returned, not just "it didn't send."
  3. For a broadcast: check the lane field on the campaign/list response.
  4. For an auto-message or Track-B campaign: check whether it has cleared soak yet.

What's next

On this page