You trigger causes (domain events). You never fire effects (individual webhooks).A webhook can never exist that contradicts state, because every webhook is a byproduct of a real state transition — exactly like production.
Why you can’t just “send a webhook”
It’s tempting to want afireWebhook("task_run.completed") button. ProdBreak deliberately doesn’t
have one. If you could fire effects directly, you could create a task_run.completed webhook for a run
that never completed — a lie your test would then assert against, and a habit that hides real bugs in
your handler.
Instead you cause the thing that produces the webhook, and ProdBreak derives the rest:
Through the normal API
Your app’s own
POST/PATCH ride the interception path. ProdBreak runs the
transition and its fan-out exactly like prod. No special verb.Through world events
External causes your app didn’t initiate — “the customer paid on the portal”, “the caller hung
up”. These are the only things on the control surface, and they’re causes, never effects.
You can only go as far as the non-dependent event
Because only causes are on the surface, “you can only trigger what could really happen next” is enforced for free. Derived events simply aren’t nameable. World events are also state-gated: each declares a precondition over current state. You can fire “payment received” only when there’s an account to receive it. The live set of valid triggers is the “what can happen next” view:Lifecycle vocabulary, not lifecycle mechanics
You say “the caller hung up” — never “setcall.status = completed”. Same effect, but you speak
the domain’s language and never puppeteer internal state. The pack owns the mechanics; you own the
narrative.
Multi-step sequences over time (a run completes → a transcript is produced → a webhook fires) are part
of the pack. You control the head (the cause) and the timing — see
The virtual clock — but never the shape. When a sequence branches (success vs
failure), you force the arm you want — you don’t edit the sequence.