# Routing inputs

Look up every routing input, where you can use it, its type, and when it may be absent.

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

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

Use this page when you are writing or debugging a routing condition. It lists each supported
input, the clauses where you can use it, and the cases where TokenTriage has no value for it.

An input with no value is **absent**. Absence is different from `0` and `false`, which are both
present values. Most comparisons with an absent input evaluate to false, but `!=` and `not` are
important exceptions. [How a routing decision is made](/docs/build/routing-decision#how-a-missing-input-behaves)
shows the complete behavior.

If tiers, upstreams, or routing clauses are new to you, start with the
[Routing overview](/docs/build/routing-overview). A **tier** is a named model-and-upstream target;
an **upstream** is the configured provider connection that serves it.

## Scope decides what exists

Routing conditions run at different stages of a request. The stage determines which inputs are
available.

<RoutingScopesDiagram />

Read left to right along one request. Request-scope inputs exist as soon as the request
arrives and stay readable everywhere afterwards. Candidate-scope inputs exist only while the
router is comparing tiers, so they are readable only inside a `select … where`. Outcome-scope
inputs exist only after an attempt has returned, so they are readable only inside a
`cascade … accept_when`.

Use the clause you are writing to choose the matching scope:

| Clause | Inputs you can read |
|---|---|
| rule `when` or predicate | `request.*`, `trace.*`, `dim.*`, `signal.*`, `ctx.*` |
| `select … where` | all request-scope inputs, plus `candidate.*` |
| `cascade … accept_when` | all request-scope inputs, plus `outcome.*` |

Using an input outside its scope is different from reading an absent value. An out-of-scope input
makes validation fail and identifies the invalid path. An absent input is valid, but it changes how
the condition evaluates.

For example, this rule condition reads one request-scope input:

```yaml
when: 'dim.tenant exists and dim.tenant = "acme"'
```

If business dimensions are disabled or the request has no `x-tt-tenant` header, `dim.tenant exists`
is false. Evaluation stops there and the rule does not match.

<Details summary="How absent values affect conditions">

`X exists` is true only when `X` has a value. `X = false` is true only when `X` is present and
false. If `X` is absent, both `X != false` and `not X` are true. Test `exists` first when absence
must behave differently from a present `false`.

</Details>

## Request scope

Request-scope inputs describe the incoming request and the runtime state available before a tier is
chosen. You can read them in a rule's `when`, in a predicate body, and inside `where` and
`accept_when`.

### `request.*` — what the client asked for

These inputs describe the request body after TokenTriage has parsed it.

| Input | Type | What it holds | Absent when |
|---|---|---|---|
| `request.model` | string | model name from the request | the request names no model |
| `request.dialect` | string | parsed request format | not expected; the parser normally sets it |
| `request.endpoint` | string | classified API endpoint | the endpoint cannot be classified |
| `request.stream` | bool | whether the client requested streaming | never; `false` is present |
| `request.prompt_bytes` | number | combined prompt length in bytes | never |
| `request.messages` | number | message count | never |
| `request.system_bytes` | number | system-prompt length in bytes | never |
| `request.has_tools` | bool | whether the request declares tools | never |
| `request.tool_count` | number | declared tool count | never |
| `request.tool_result_count` | number | tool-result message count | never |
| `request.max_tokens` | number | requested output cap; `0` when unset | never |
| `request.reasoning_effort` | string | requested reasoning level | the request sets none |

Numbers and booleans here are present even at zero or false. Only the string fields go absent,
and only when they are empty.

### `trace.*` — the agent and the step

Trace inputs connect a request to an agent, conversation, and step. Each value comes from an
`x-tt-*` header unless the table says otherwise.

| Input | Type | What it holds | Absent when |
|---|---|---|---|
| `trace.agent` | string | `x-tt-agent` value | the header was not sent |
| `trace.step_class` | string | `user_turn`, `tool_loop`, or `provider_state` | never; derived when the header is missing |
| `trace.step_class_source` | string | `header` or `derived` | never |
| `trace.trace_id` | string | conversation ID | the header was not sent |
| `trace.step_id` | string | current step ID | the header was not sent |
| `trace.parent_id` | string | parent step ID | the header was not sent |
| `trace.w3c` | string | raw `traceparent` header | the header was not sent |

### `dim.*` — your business dimensions

Business dimensions are optional labels supplied by your application. They are disabled by
default. Set `ledger.dimensions.enabled` to `true` before sending their headers; otherwise every
`dim.*` input is absent.

| Input | Type | What it holds | Absent when |
|---|---|---|---|
| `dim.user` | string | `x-tt-user` value | dimensions are disabled or the header is missing |
| `dim.tenant` | string | `x-tt-tenant` value | dimensions are disabled or the header is missing |
| `dim.feature` | string | `x-tt-feature` value | dimensions are disabled or the header is missing |
| `dim.tag.<key>` | string | matching `x-tt-tag-<key>` value | dimensions are disabled or the header is missing |

`dim.tag.<key>` is a family: any single-segment key is accepted, so `dim.tag.env` works without
being declared anywhere. A key containing a dot is not accepted.

### `signal.*` — the classifier and your scorers

The built-in classifier runs on every request. It always emits these numeric inputs:

| Input | Type | What it holds | Absent when |
|---|---|---|---|
| `signal.complexity` | number | combined complexity score in `[0,1]` | never |
| `signal.token_est` | number | estimated prompt tokens, calculated as characters divided by four | never |
| `signal.code` | number | code-vocabulary score | never |
| `signal.reasoning_markers` | number | reasoning-phrase score | never |
| `signal.reasoning_marker_count` | number | phrase count behind the reasoning score | never |
| `signal.technical` | number | technical-vocabulary score | never |
| `signal.simple` | number | simple-phrase score, which lowers complexity | never |
| `signal.multi_step` | number | multi-step-structure score | never |
| `signal.question` | number | question-density score | never |
| `signal.has_tools` | number | `1` when tools are declared; otherwise `0` | never |
| `signal.tool_count` | number | declared tool count | never |
| `signal.tool_result_count` | number | tool-result message count | never |
| `signal.message_count` | number | message count | never |

Declaring a scorer adds more: `semantic` emits `signal.semantic.<topic>` per topic, and
`est_cost` emits `signal.est_cost`. Both are described in
[selectors and scorers](/docs/build/routing-selectors#request-scorers).

<Callout type="warning">

Built-in signal names are not checked against a fixed list. A typo such as
`signal.complexty` passes validation but stays absent, so a positive comparison
does not match. Dotted names such as `signal.semantic.legal` must match a scorer
you declared. After changing a signal name, send a test request and inspect its
decision trace to confirm that the input was read.

</Callout>

### `ctx.*` — clock and governance state

The clock is always available. Budget, limit, and principal inputs expose governance state to a
routing condition. They steer a request but do not decide whether it is admitted.

| Input | Type | Available when | Absent when |
|---|---|---|---|
| `ctx.time.hour_utc` | number | always | never; `0`–`23`, UTC |
| `ctx.time.weekday` | number | always | never; `0` is Sunday |
| `ctx.time.unix` | number | always | never; request arrival, epoch seconds |
| `ctx.budget.<id>.remaining_frac` | number | that budget is configured | budget state is unavailable or suspended |
| `ctx.budget.<id>.remaining_usd` | number | that budget is configured | budget state is unavailable or suspended |
| `ctx.budget.<id>.exhausted` | bool | that budget is configured | budget state is unavailable or suspended |
| `ctx.limit.<id>.remaining_frac` | number | a global or node-pinned limit with that ID is configured | limit state is unavailable or that limit is suspended |
| `ctx.limit.<id>.exhausted` | bool | a global or node-pinned limit with that ID is configured | limit state is unavailable or that limit is suspended |
| `ctx.limit.<id>.suspended` | bool | a global or node-pinned limit with that ID is configured | limit state is unavailable |
| `ctx.principal.budget_remaining_frac` | number | governance is enabled | anonymous or unresolved traffic |
| `ctx.principal.rpm_remaining_frac` | number | governance is enabled | anonymous or unresolved traffic; otherwise present as `1` |
| `ctx.principal.exhausted` | bool | governance is enabled | anonymous or unresolved traffic |

<Callout type="note">

- Only **global and node-pinned** limits appear as `ctx.limit.<id>`. A template-level limit has no
  single state, so using its ID is a validation error.
- A limit whose enforcement has failed open reports `suspended` and omits `remaining_frac` and
  `exhausted` rather than reporting zero remaining.
- `ctx.principal.rpm_remaining_frac` currently reports fully available (`1`) for resolved traffic.
  A condition such as `ctx.principal.rpm_remaining_frac < 0.2` therefore does not match.

</Callout>

Naming an unconfigured budget or limit ID is a validation error, not an absent read. Admission is a
separate [governance](/docs/build/governance) gate that runs in every mode regardless of the routing
policy.

## Candidate scope

Candidate-scope inputs describe one tier while a `select` node filters its choices. They are
readable only inside `select … where`.

### Identity and eligibility

Use these inputs to identify a candidate and check whether it can serve the request.

| Input | Type | What it holds | Absent when |
|---|---|---|---|
| `candidate.tier` | string | tier name | never |
| `candidate.upstream` | string | configured upstream name | never |
| `candidate.model` | string | model assigned to the tier | never |
| `candidate.rank` | number | position in the declared `tiers:` list | never |
| `candidate.pool_weight` | number | configured pool weight | the candidate set came from a tier list rather than a pool |
| `candidate.healthy` | bool | whether cooldown and request-format checks both pass | never |

### Cost, latency, and rewards

These inputs support cost, performance, and learned-outcome comparisons.

| Input | Type | What it holds | Absent when |
|---|---|---|---|
| `candidate.price.input_per_tok` | number | catalog input price in dollars per token | the catalog cannot price that tier |
| `candidate.price.output_per_tok` | number | catalog output price in dollars per token | the catalog cannot price that tier |
| `candidate.latency.p50_ms` | number | median observed latency in milliseconds | that upstream has no observations |
| `candidate.latency.p95_ms` | number | 95th-percentile observed latency in milliseconds | that upstream has no observations |
| `candidate.reward_mean` | number | graded pass rate in `[0,1]` | that tier has no graded outcomes |
| `candidate.reward_count` | number | number of graded outcomes behind the pass rate | that tier has no graded outcomes |

### Calibrated quality

These six inputs come from the same calibration result. If no result is available for the
candidate, all six are absent.

| Input | Type | What it holds | Absent when |
|---|---|---|---|
| `candidate.quality.lcb` | number | lower confidence bound used for quality floors | no calibration result is available |
| `candidate.quality.n` | number | sample count stored with the bound | no calibration result is available |
| `candidate.quality.calibrated` | number | `1`, marking the value as calibrated | no calibration result is available |
| `candidate.quality.confidence` | number | nominal confidence stored with the result | no calibration result is available |
| `candidate.quality.method` | string | calibration method that produced the bound | no calibration result is available |
| `candidate.quality.estimand` | string | type of guarantee represented by the bound | no calibration result is available |

### Inputs that are not available

Two candidate names pass validation but currently have no value. A positive comparison against
either one evaluates to false. The `!=` and `not` exceptions described earlier still apply.

| Input | Type | What happens | Use instead |
|---|---|---|---|
| `candidate.provider` | string | always absent | `candidate.upstream` |
| `candidate.max_context` | number | always absent | `request.prompt_bytes` with a threshold you set for each tier |

`candidate.quality.mean` is different: it is not a readable routing input. Using it in a `when`,
`where`, `accept_when`, predicate body, or selector dependency produces a validation error. Route on
the calibrated lower confidence bound, `candidate.quality.lcb`, instead.

## Outcome scope

Outcome-scope inputs describe one completed cascade attempt. They are readable only inside that
stage's `accept_when`. The optional judge is a model-based quality check requested by the cascade
stage.

| Input | Type | What it holds | Absent when |
|---|---|---|---|
| `outcome.status` | number | HTTP response status | never |
| `outcome.response_bytes` | number | response size in bytes | never |
| `outcome.latency_ms` | number | attempt latency in milliseconds | never |
| `outcome.output_tokens` | number | measured output-token count | usage cannot be measured for that attempt |
| `outcome.finish_reason` | string | provider finish reason | the provider sends none or the value is unrecognized |
| `outcome.judge.available` | bool | whether the optional judge returned a result | the stage did not set `judge: true` |
| `outcome.judge.ok` | bool | optional judge's pass-or-fail result | the stage did not set `judge: true` |
| `outcome.refusal_markers` | number | distinct decline patterns in the first 8 KiB of the response | never |

## What this page does not cover

This page lists values; it does not teach the syntax that reads them or the components that produce
them. See [the policy language](/docs/build/policy-language) for operators and grammar,
[selectors and scorers](/docs/build/routing-selectors) for the producers and consumers of routing
inputs, and [ledger fields](/docs/reference/ledger-fields) for the recorded result.

<NextSteps items={[
  { title: 'The policy language', href: '/docs/build/policy-language', description: 'The operators and grammar that read these inputs, and what validation rejects.' },
  { title: 'Selectors, scorers, and context providers', href: '/docs/build/routing-selectors', description: 'What produces each family of inputs, with parameters, defaults and formulas.' },
  { title: 'How a routing decision is made', href: '/docs/build/routing-decision', description: 'The absent-value rule these tables assume, and the shapes that fail the wrong way.' },
  { title: 'Headers', href: '/docs/reference/headers', description: 'The x-tt-* headers that populate the trace and dimension inputs above.' }
]} />
