Skip to content

Routes

A route block is an ordered pool of trunks with a selection strategy. Where a trunk translation target sends a call out one specific trunk, a route target gives the call a list of trunks to try — with failover, load distribution, and optionally a rate-table-driven least-cost ordering in front.

routes:
domestic:
strategy: sequential
destinations:
- trunk: carrier-a
- trunk: carrier-b

A translation targets it like any other resource:

target:
resource-type: route
resource-id: domestic
failure-treatment: all-circuits-busy

destinations is the static trunk list. strategy controls the order the list is tried in:

Strategy Ordering
sequential Declared order, every call. Primary/backup failover.
random Uniformly shuffled per call. Even load spread.
weighted Random order biased by weight — higher-weight trunks are proportionally more likely to be tried first.

With weighted, every destination must carry a positive weight:

routes:
load-shared:
strategy: weighted
destinations:
- trunk: carrier-a
weight: 70
- trunk: carrier-b
weight: 30

Weights bias the first attempt roughly 70/30 here; the losing trunk remains in the order as failover rather than being excluded.

Failover semantics — what advances the chain

Section titled “Failover semantics — what advances the chain”

Each trunk in the resolved order is dialed according to its own trunk profile: its URI list (in uri-selection order), per-URI ring-for duration, codec set, and egress caller-ID policy. The outcome of each attempt decides what happens next:

  • Try the next URI of the same trunk — user-level or transient outcomes where another gateway of the same carrier may still succeed: busy, temporary failure, circuit unavailable.
  • Advance to the next trunk in the route — carrier- or network-level failures where retrying the same carrier is pointless: congestion, call rejected, no route to destination, network out of order, interworking errors. Exhausting all of a trunk’s URIs also advances, and so does a trunk that can’t be attempted at all — one at its concurrent-call cap, or missing/misconfigured in the running config. Listing an overflow trunk after a capacity-capped primary is therefore a supported pattern: calls spill to the next destination the moment the primary is full.
  • Stop immediately with a treatment — permanent, number-level outcomes that every carrier would reject the same way: unallocated number, invalid number format, number changed. Failing over would only delay the error the caller is going to hear anyway. A genuine no-answer likewise ends the attempt rather than re-ringing the same called party via another carrier.

When the whole chain is exhausted, the caller receives the route target’s failure-treatment (default reorder).

These outcomes are visible per-call: the CDR’s egress leg records which trunk ultimately carried the call, and the disposition and Q.850 cause of the final attempt.

An lcr block puts a rate table in front of the static destination list. At call time the switch looks up candidate trunks in the table, ranks them by effective cost, and tries them before destinations:

routes:
domestic-lcr:
strategy: sequential
lcr:
tablename: us-rates
prefix-match-source: dialednum
prefix-match-logic: prefix
selector: "!all"
route-count: 3
assumed-call-duration-seconds: 120
destinations:
- trunk: overflow-carrier

Rate tables themselves — rows, uploads, versioning, activation — are managed through the LCR API and covered in the LCR deck administration guide. The route block only points at a table and sets the lookup policy.

Field Values Meaning
tablename string (required) Which of your LCR tables to consult. The table’s currently active version is used.
route-count positive integer (required) Maximum LCR candidates to try (top-N by effective cost).
prefix-match-logic exact | prefix exact: single point lookup of the whole key. prefix: longest-prefix match.
prefix-match-source dialednum or a call-detail field What value keys the lookup — the dialed number, or an enrichment field such as lrn or dlata.
selector literal | !all | !match-on:<field> Which rows within a matched prefix are eligible (below).
assumed-call-duration-seconds positive integer, default 60 The duration used in the effective-cost comparison.
  1. Resolve the lookup key from prefix-match-source. dialednum uses the post-translation dialed number; a call-detail field (e.g. lrn) resolves from enrichment data. A leading + on the key is stripped before matching, so an E.164 dialed number or LRN compares digits-to-digits against the table — store your table prefixes as bare digits (1305, not +1305).

  2. Match the prefix. With prefix logic, the longest prefix present in the table that matches the key wins — a 1415555 row beats 1415 beats 1. With exact, only a row keyed by the full value matches.

  3. Filter by selector. Each table row carries a selector column:

    • a literal selector matches rows whose column equals it — use this to keep several independent rate decks in one table;
    • !all accepts every row under the prefix;
    • !match-on:<field> resolves the named call-detail field at call time and matches rows whose selector equals its value — e.g. rows tagged per call-type jurisdiction.
  4. Filter by validity window. Rows carry optional effective-on / valid-until timestamps; rows outside their window at call time are ignored. This is how future rate decks are staged in advance.

  5. Rank by effective cost. Each row has a per-minute cost, a setup cost, and a priority. Candidates are ordered by:

    effective-cost = setup-cost + per-minute-cost × (assumed-call-duration-seconds / 60)

    Lower cost first; among equal costs, lower priority value first; exact ties are broken randomly per call, spreading traffic across equivalent carriers. Costs are integer units — only relative magnitudes matter, so pick one unit (e.g. thousandths of a cent per minute) and use it consistently within a table.

  6. Truncate to route-count and dial in order, with the same failover semantics as static destinations. The static destinations list follows as the final fallback.

The lookup contributes nothing — and the call falls through to the static destinations list — when:

  • the named table has no active version,
  • no prefix in the table matches the key,
  • the selector is !match-on:<field> and the field can’t be resolved for this call (enrichment data unavailable — the usual fail-closed behavior),
  • prefix-match-source names a call-detail field that can’t be resolved,
  • a candidate row references a trunk that no longer exists in your configuration (that row is skipped; the rest are kept).

Activating a new table version propagates to call processing within about 30 seconds.

routes:
simple-failover:
strategy: sequential
destinations:
- trunk: primary-carrier
- trunk: backup-carrier

One table, rows tagged with a call-type selector, port-corrected lookups:

routes:
rated-domestic:
strategy: sequential
lcr:
tablename: us-rates
prefix-match-source: lrn # rate where the number lives, not its dialed prefix
prefix-match-logic: prefix
selector: "!match-on:call-type" # match rows tagged local / interstateInterlata / ...
route-count: 3
assumed-call-duration-seconds: 180
destinations:
- trunk: overflow-carrier

A ported number rates by its LRN; the jurisdiction selector picks the correct rate deck; the three cheapest valid carriers are tried in order; overflow-carrier catches everything the table can’t.

Meet a minimum-commit on carrier A while keeping carrier B warm:

routes:
commit-split:
strategy: weighted
destinations:
- trunk: carrier-a
weight: 85
- trunk: carrier-b
weight: 15

The route-forensics record shows the translation walk that selected the route; the CDR’s egress leg shows which trunk finally carried the call, the dialed URI, and the final disposition. When validating a new LCR table or selector policy, the LCR API’s lookup/resolve preview endpoints let you run the same resolution the call path uses against a hypothetical call — see the LCR deck administration guide.