# Actions: block, notify, reroute (/docs/rules/actions)



Every rule ends in an **action**. There are three:

| Action                      | What it does                                                             | You set                         |
| --------------------------- | ------------------------------------------------------------------------ | ------------------------------- |
| **Block Request (BLOCK)**   | Drops the request through the SDK before the call reaches your provider. | Nothing extra.                  |
| **Alert Admin (NOTIFY)**    | Lets the call through, and raises an alert.                              | Which channels, and a throttle. |
| **Reroute Model (REROUTE)** | Swaps the call's model for a cheaper one before dispatch.                | A target provider and model.    |

## When the action fires [#when-the-action-fires]

An action fires in one of two ways, depending on whether the rule has a limit:

* **With a limit** — the rule counts spend for each pool and acts only once a pool crosses the
  limit. This is what people mean by a budget.
* **With no limit (unconditional)** — the rule acts on *every* call its filter matches, with no
  counting at all. "Block every call from `plan = free`" needs no threshold.

In the custom builder this is the **Unconditional** checkbox. Reroute defaults to unconditional;
Block and Notify default to budget-gated. Uncheck it on a reroute (or tick it on a block) to swap
the behaviour.

## Block [#block]

The request is stopped in the SDK and your code receives a `TokenPoliceBlockedError` — the one
exception TokenPolice ever raises into your app, and only under enforce. Everything else stays
[fail-open](/docs/concepts/fail-open). See [Handling blocks](/docs/sdk/errors) for the catch
pattern.

Blocking happens *before* the provider request, so a blocked call costs you nothing. Earlier
calls in the same agent run already happened and were billed.

## Notify [#notify]

The call still happens; TokenPolice just tells you it crossed the line. Pick one or more
**verified** channels to send to, and set a throttle so a noisy rule doesn't flood you — a
cooldown (default 300 seconds) and a maximum alerts per hour (default 20). Those throttles apply
to external channels only; the in-app inbox always receives the alert.

<Callout type="info">
  Notify needs somewhere to send. See [Notifications](/docs/rules/notifications) to add and verify
  a channel first.
</Callout>

**Notify rules have no Dry-run/Enforce dial.** They never affect a request, so there is nothing
to gate — the rules table shows a dash in the Current Mode column, and a Notify rule cannot be
promoted to Enforce.

## Reroute [#reroute]

Instead of blocking, swap the model for a cheaper one. You give a target provider and model (for
example `openai` / `gpt-4o-mini`), and matching calls run on that model instead. If the rule has
a limit, the swap starts once the pool crosses it; with no limit, every matching call is swapped.

The target model has to be one TokenPolice already has pricing for, or the rule won't save.

<Callout type="warn">
  **Reroute supports same-provider swaps only** — pick a smaller or cheaper model from the same
  provider. A cross-provider target is rejected at run time and shows as *Reroute rejected* in the
  [Audit log](/docs/dashboard/audit-log).
</Callout>

<Callout type="warn">
  **Reroute does not switch the model on some frameworks — including LangChain, LangGraph,
  LlamaIndex, Pydantic AI, and the Vercel AI SDK. Those calls still run on the original model.
  Confirm a swap actually happens in your app before you enforce this in production.** Look for
  the *Reroute rejected* rows in the [Audit log](/docs/dashboard/audit-log) — they are in the
  default **All** tab — and for the rejected-reroute count on each run in
  [Traces](/docs/dashboard/workflows-sessions-traces). Not on the
  [Routing tab](/docs/dashboard/dry-run-and-routing): that feed lists reroutes that *did* happen,
  so a swap the SDK refused never appears there.
</Callout>

## Watch before you act [#watch-before-you-act]

Every Block and Reroute rule starts in **Dry-run**, and while it's there — or while your app's
SDK is in dry-run — the rule only *records* what it would have done. Two switches have to agree
before anything is stopped:

1. The **rule** is set to Enforce, and
2. your app initialized the SDK with `firewall: "enforce"`.

See [Dry-run vs enforce](/docs/concepts/dry-run-vs-enforce).

<Callout type="warn">
  If a rule is Enforce but your app most recently reported its SDK `firewall` mode as `dry_run`
  or `off&#x60;, the Firewall page shows a banner — &#x2A;"Rules set to Enforce, app in Dry-run"&#x2A; (it reads
  &#x2A;"app in Off"* when the SDK reported `off`). The rule evaluates but never acts until your app
  initializes with `firewall: "enforce"`.
</Callout>

The [Audit log](/docs/dashboard/audit-log) is where you see which switch was open: a would-be
action is labelled **Would deny** or **Would reroute**, with a chip naming *SDK in dry-run*,
*Rule in dry-run*, or *SDK & rule in dry-run*.

## Next [#next]

<Cards>
  <Card title="Notifications" href="/docs/rules/notifications" description="Add and verify the channels a Notify rule can send to." />

  <Card title="Downgrade the model" href="/docs/recipes/downgrade-model" description="A reroute recipe end to end." />

  <Card title="Dry-run and Routing tabs" href="/docs/dashboard/dry-run-and-routing" description="See what your rules would do, and every swap they made." />
</Cards>
