# Explain a routing decision

Find the rule and inputs behind one routing decision, then diagnose why it behaved differently from what you expected.

Source: https://tokentriage.com/docs/build/routing-explain

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';

Use `policy explain` when a request matches an unexpected rule or takes an unexpected route. The
command shows the policy revision, the result, and the inputs used to reach it. You do not have to
read the config and guess what happened.

The command reads an existing decision from the log. It does not send a request, change the
policy, or affect traffic.

## What you need

Gather these three things before you run the command.

| You need | Where to find it |
|---|---|
| one completed request | any request already written to the decision ledger |
| its request id | the `x-tt-request-id` response header; [trace a request by id](/docs/build/observability-request-id) shows the other lookup paths |
| read access to the log | the default is `~/.tokentriage/decisions.jsonl` |

You do not need the dashboard, a provider connection, or routing enforcement. `policy explain`
works on a copied log and on rows recorded in `ledger`, `shadow`, or `route` mode.

## Explain the request

Replace `tt_simple` below with your request id. The default log path is used when you omit
`--log`.

```console
$ tokentriage policy explain --req-id tt_simple
req_id:     tt_simple
policy_rev: 184bdaf
decision:   rule=simple-to-cheap tier=— upstream=—
path:       root → rules[simple-to-cheap]
trace:
  • rules[simple-to-cheap]       matched
      read signal.complexity = 0.21
      read $simple_cut = 0.35
```

This request matched `simple-to-cheap` because its complexity value, `0.21`, was below the
configured cutoff, `0.35`. This example was recorded in `ledger` mode, so the policy match was
recorded but no routing tier was applied. That is why `tier` and `upstream` are dashes.

If your log lives somewhere else, add its path:

```console
$ tokentriage policy explain --req-id tt_simple --log ./decisions.jsonl
```

## Read the result in four steps

Reading the fields in this order usually finds the surprise quickly.

1. **Confirm `policy_rev`.** This short fingerprint identifies the policy that decided the row.
   If it does not match the config you are inspecting, you are looking at two different policy
   versions. `tokentriage policy migrate --config ./tokentriage.yaml` prints the config revision
   in its first comment.
2. **Read `decision`.** This is the result: the rule, tier, upstream, and any model rewrite. A
   `ledger` row may show a rule with no tier because ledger mode observes without applying a
   routing target.
3. **Follow `path`.** It lists the policy nodes that were visited, in order. The final entry is the
   node that produced the result.
4. **Inspect `trace` and `read`.** `trace` says what each visited node did. Its `read` lines show
   the values that were present when a condition evaluated them.

The complete output for selectors, traffic splits, and cascades is in
[Routing explanation fields](/docs/build/routing-explanation-fields).

## Diagnose a rule that did not match

A condition stops evaluating as soon as its result is known. For example, once the first part of
an `and` expression is false, later parts are not read. Fewer `read` lines than conditions can
therefore be expected.

A rule shown as `skipped` with `why=when=false` was evaluated and did not match. A rule missing
from `path` and `trace` was not reached. Use
[How a routing decision is made](/docs/build/routing-decision) to check the evaluation order.

<Callout type="warning">

For rule conditions, `read` lists values that were present. If a condition asked for a value that
was absent, the comparison is false and the missing value is not added to `read`. Check for a
misspelled signal, a header the client did not send, or unavailable runtime state. Selector
filters expose missing candidate facts differently: their `filtered` reason can name the absent
fact.

</Callout>

## Fix common problems

Start from the symptom in the output, then take the corresponding next step.

| Symptom | Likely cause | What to do |
|---|---|---|
| request id is not found | wrong log, incomplete request, or a damaged log line | confirm the log path and request id; use [Trace a request by id](/docs/build/observability-request-id) to locate the row |
| `tier=—` on a matched rule | the row was recorded in `ledger` mode | use the rule and reads to debug the policy; use `shadow` when you also need the proposed tier |
| `(no explanation block …)` | an older or non-policy router wrote the row | inspect the raw ledger row; newer policy-language decisions include an explanation |
| an expected rule is missing | an earlier node ended the walk | follow `path` from left to right and check the decision order |
| an expected `read` line is missing | an earlier condition settled the result, or the value was absent | check the preceding reads, then verify the signal, header, or state source |
| `policy_rev` is unfamiliar | the request used a different config revision | print the revision of the config you are inspecting and compare it with the row |

`explain` skips a torn JSON line without printing a warning. If the request id and path are
correct but the row is still not found, run `report`, `eval`, or `calibrate` against the log; those
commands count damaged lines on standard error.

## Use the same explanation elsewhere

The CLI renders the explanation stored on the decision row. The dashboard record drawer and
`GET /tt/api/v1/requests/{req_id}` read that stored block too, although each surface presents it
differently. There is no separate explain endpoint.

Use [Preview a routing policy change](/docs/build/routing-preview) when the next question is not
why one request behaved that way, but how a draft would affect a whole log.

<NextSteps items={[
  { title: 'Preview a routing policy change', href: '/docs/build/routing-preview', description: 'Re-run past decisions under a draft and interpret the changed-request and cost summaries.' },
  { title: 'Routing explanation fields', href: '/docs/build/routing-explanation-fields', description: 'Look up selector scores, filtered candidates, random draws, and cascade attempts.' },
  { title: 'How a routing decision is made', href: '/docs/build/routing-decision', description: 'Follow the evaluation order and see how absent inputs affect a policy walk.' },
  { title: 'Trace a request by id', href: '/docs/build/observability-request-id', description: 'Find the request id and join the ledger, API, dashboard, traces, and provider logs.' }
]} />
