# Number routing: live decisions and reconcile

> How the engine picks a route for an inbound DID, how to read the live route decision in the console, why stored bindings can drift from live rules, and what Re-sync routing rewrites.

The **Phone Numbers** screen shows two different things side by side. The
**Trunk** and **Agent** columns show what is stored in the database for each
DID. The **Routes to (live)** column shows what the engine would actually do
with an inbound call to that number right now, read back out of the live
dialplan.

Most of the time they agree. This page covers the precedence the engine uses,
how to read the live decision, and what to do when the two columns disagree.

## Rule precedence: per-number, trunk, park

The TeleQuick engine resolves an inbound call against the live dialplan
blob in a fixed order. The first match wins:

| Order | Rule            | Console `reason` | Shown as      |
| ----- | --------------- | ---------------- | ------------- |
| 1     | Per-number rule matching the DID | `did_rule`    | `number rule` |
| 2     | Rule attached to the trunk the call arrived on | `trunk_rule`  | `trunk rule`  |
| 3     | No rule matched  | `default_park`   | `no rule`     |

A per-number rule always beats a trunk rule. That is what makes a *stale*
per-number rule dangerous: it keeps winning, even after you have changed the
trunk's routing or cleared the number's agent binding in the database.

If neither a number rule nor a trunk rule matches, the call falls through to
park — the engine answers nothing useful and the caller gets silence.

## Reading the live route decision and its reason

The console asks the control plane for the effective route of every DID that
is bound to a trunk:

```ts
const r = await apiClient.admin.dialplanRoutes.query({
  orgId: activeOrg.id,
  numbers: [{ trunkId: "carrier-trunk-id", did: "+15550100" }],
});
// r.numbers[i].route → { action, args, reason, agentId }
```

`admin.dialplanRoutes` reads the live dialplan blob and applies the engine's
own precedence, so the answer is the engine's answer, not a re-derivation from
the `phone_number` rows.

Each entry in the response carries four fields:

| Field     | Meaning                                                          |
| --------- | ---------------------------------------------------------------- |
| `action`  | What the engine will execute for this call.                      |
| `args`    | The action's argument string, where the action takes one.        |
| `reason`  | Which tier of the precedence matched: `did_rule`, `trunk_rule`, or `default_park`. |
| `agentId` | The agent the matched rule routes to, when the action routes to an agent. |

The `reason` is the important half. Two numbers can both show the same agent
and still be configured very differently — one via a per-number rule, one via
its trunk. The bracketed label after the route in the table (`[number rule]`,
`[trunk rule]`, `[no rule]`) tells you which one you are looking at, and
therefore which object you need to edit to change it.

The route lookup is advisory. If it fails, the column falls back to a
placeholder and the rest of the table still renders — a failed lookup never
blocks the number inventory.

## What each action means

| `action`                   | Console renders                                | Meaning |
| -------------------------- | ---------------------------------------------- | ------- |
| `ai_bidirectional_stream`  | The agent name and id, from `agentId`          | The call is bridged to an AI agent over a bidirectional audio stream. |
| `vector`                   | `vector <args>`                                | The call enters a dialplan vector. `args` identifies which one — the routing lives in the vector, not on the number. |
| `park`                     | `PARK (no rule — silence)`, highlighted as a warning | Nothing matched. The caller hears silence. |
| anything else              | The raw action string                          | Actions the console has no special rendering for are shown verbatim. |

A `park` result is always highlighted, because an inbound DID that parks is
almost never intentional.

## Why some numbers show no live decision

The route lookup is only issued for DIDs that have a trunk binding whose trunk
still exists in the org. A number is skipped when:

- `trunk_id` is null — the row shows `— no trunk`.
- `trunk_id` points at a trunk that is not in the org's trunk list.

Inbound calls arrive over a trunk, so a DID with no trunk binding has no
inbound path to evaluate. Bind it to a trunk before you expect a live route.

While the lookup is in flight, bound numbers show `…`.

## When stored bindings and live rules drift

The console flags a mismatch on a row in two cases:

- The number **has** an `agent_id` in the database, but the live rule's
  `agentId` is something else (including null).
- The number has **no** `agent_id` in the database, but a per-number rule
  (`did_rule`) is still matching it — the database says "let the trunk
  decide", the engine says otherwise.

Mismatched rows are highlighted and carry the note *"differs from this page;
use Re-sync routing"*.

Drift is possible because the database rows and the engine's live rules are
two separate stores. Saving a number writes the database row and then triggers
a dialplan rebuild **fire-and-forget** on the server side. The rebuild is not
part of the save's response. So the live rules can lag, or — if a rebuild did
not land — stay behind indefinitely while the number screen happily shows the
new binding.

Because the rebuild is asynchronous, the console deliberately waits before
reading routes back: roughly 1.2 s after adding a number, and 300 ms after a
re-sync. If you read the live column immediately after your own edit through
another client, expect the same lag.

## What Re-sync routing rewrites

**Re-sync routing** calls `admin.reconcileNumbers` for the active org. It is
the repair path for "the number screen says X but calls go to Y", and it
removes any need to touch the engine's Redis state by hand.

```ts
const r = await apiClient.admin.reconcileNumbers.mutate({ orgId: activeOrg.id });
// r → { numbers, removed, rules }
```

It does two things:

1. Rewrites the engine's number mirrors in Redis from the database
   `phone_number` rows — the database is the source of truth.
2. Rebuilds the dialplan so the live rules match those mirrors.

The result banner reports what happened:

| Field     | Banner text                | Meaning |
| --------- | -------------------------- | ------- |
| `numbers` | `N numbers re-synced`      | Number mirrors written from database rows. |
| `removed` | `N stale entries removed`  | Mirror entries that no longer correspond to a database row. |
| `rules`   | `N routing rules live`     | Rules in the dialplan after the rebuild. |

If the call fails, the banner shows the error instead. Nothing is written to
the database by a re-sync — it only rewrites the engine-side state from what
the database already says.

## Verifying a number after a change

1. Make the change — add or edit the number, or change the trunk's routing.
2. Give the asynchronous dialplan rebuild a moment, then re-read the table.
   The **Routes to (live)** column is what the engine will do.
3. Check the `reason` label. If you intended the trunk to decide, you want
   `[trunk rule]`. If you bound an agent directly to the number, you want
   `[number rule]` with that agent.
4. If the row is highlighted, or shows `PARK (no rule — silence)` on a number
   you expect to answer, click **Re-sync routing**.
5. Confirm the banner counts look sane, and that the row's live route now
   matches its Trunk and Agent columns.

If a row still mismatches after a re-sync, the disagreement is in the database
rows themselves rather than in the engine mirrors — re-check the number's
trunk and agent bindings, and the trunk's own routing.

## Related

- [Authentication](/concepts/authentication) — the API key your console session uses for control-plane calls
- [Telemetry](/platform/telemetry) — CDR `agent_id` records which agent a call actually routed through
