> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prodbreak.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Causes, not effects

> You trigger domain events; webhooks fall out of real state transitions, exactly like prod.

The single principle behind ProdBreak's event model:

> **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 a `fireWebhook("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:

<CardGroup cols={2}>
  <Card title="Through the normal API" icon="code">
    Your app's own `POST`/`PATCH` ride the [interception](/interception) path. ProdBreak runs the
    transition and its fan-out exactly like prod. No special verb.
  </Card>

  <Card title="Through world events" icon="globe">
    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.
  </Card>
</CardGroup>

```ts theme={null}
// a cause — "the customer paid on the portal". ProdBreak runs the resulting transition + webhooks.
await sandbox.world.trigger("portal.payment_received", { account: "acct_1" });
```

## 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:

```ts theme={null}
const available = await sandbox.world.next();
// [{ event: "portal.payment_received", requires: "an account exists", data_schema: {…} }]
```

## Lifecycle vocabulary, not lifecycle mechanics

You say *"the caller hung up"* — never *"set `call.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.

<Note>
  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](/concepts/clock) — but never the *shape*. When a sequence branches (success vs
  failure), you [force the arm you want](/concepts/branching) — you don't edit the sequence.
</Note>
