# Block runaway agents (/docs/recipes/block-runaway-agents)



An agent stuck calling the same thing over and over, or one that just keeps going, can burn a lot
of money in one run. **Loop detection** watches each run — one trace — on its own and stops it
before it runs away.

## The rule [#the-rule]

This isn't a budget-over-time rule; it's a per-run circuit breaker. On the Firewall page, switch
to the **Runaway loops** tab and click **New rule from template** to open
**Catch a workflow stuck looping**. Two templates cover most cases:

<Steps>
  <Step>
    **Stop the same request on repeat** — fires when the same prompt goes out three times in a row
    inside one trace, or a short back-and-forth keeps repeating. Catches retry storms.
  </Step>

  <Step>
    **Limit how many model calls one trace can make** — the backstop: a hard ceiling per trace
    (default 25 calls). There are USD and token variants too: *Limit how much one trace can spend*
    and *Limit tokens per trace*.
  </Step>
</Steps>

Under **When this happens**, the templates default to **Alert only — just notify me**. Choose
**Block — stop the next model call on this trace** if you want the run halted, not just flagged.
Like every other rule, it is created in Dry-run — switch it to Enforce once you've watched it on
real traffic.

<Callout type="warn">
  **The block lands one call late.** The call that crosses the threshold is allowed; TokenPolice
  stops a *later* call on the same trace. Fast back-to-back calls can slip past. Treat it as a
  circuit breaker, not an exact ceiling.
</Callout>

<Callout type="info">
  A loop rule watches **one trace** — not a session or a conversation. For "cap this whole
  conversation", use a regular firewall rule grouped by `session_id`. Full detail in
  [Loop detection](/docs/rules/loop-detection).
</Callout>

## The snippet [#the-snippet]

Two things make this work in your code: wrap the run so its calls share one trace, and catch the
block when the detector trips.

```python
import token_police as tp

tp.init(
    api_key=os.environ["TOKENPOLICE_API_KEY"],
    base_url="https://collect.tokenpolice.ai",
    firewall="enforce",          # blocking only happens under enforce
)

@tp.workflow(name="research_agent")     # one run = one trace the rule can watch
def run_agent(user_id: str, session_id: str, goal: str):
    while not done:
        try:
            step = client.chat.completions.create(...)
        except tp.TokenPoliceBlockedError as e:
            # the loop detector stopped this run
            return f"Run halted: {e.reason}"
```

```typescript
import { workflow, TokenPoliceBlockedError } from "token-police";

const runAgent = workflow({ name: "research_agent" }, async (input) => {
  try {
    return await client.chat.completions.create(/* ... */);
  } catch (e) {
    if (e instanceof TokenPoliceBlockedError) return `Run halted: ${e.reason}`;
    throw e;
  }
});
```

Your app sees a `TokenPoliceBlockedError` — never an HTTP status. Its `kind` carries the detector
that fired and `reason` explains it. Everything else stays fail-open; see
[Errors](/docs/sdk/errors) for the full field list.

## Next [#next]

<Cards>
  <Card title="Loop detection" href="/docs/rules/loop-detection" description="Both detectors and their thresholds in detail." />

  <Card title="Handling blocks" href="/docs/sdk/errors" description="The TokenPoliceBlockedError contract." />

  <Card title="Runaway loops" href="/solutions#loops" description="The two detectors and what they catch, end to end." />
</Cards>
