Appearance
Routing Preference Tiers
What happens if no provider meets all of your requirements? Preference tiers let you answer that question in advance — "prefer a German provider, but accept a Swiss one" — instead of choosing between an over-strict policy that blocks requests and a loose one that quietly routes anywhere.
Overview
A routing policy's hard constraints are absolute: a provider that fails one is never used, and if none survive, the request does not route at all. That is the correct behavior for a legal floor, but it is a blunt instrument for a ranked requirement. Plenty of real requirements are not "this or nothing" — they are "this if it exists, otherwise that."
Preference tiers (preferenceTiers on the routing policy) express exactly that: an ordered ladder of acceptability, applied on top of the hard constraints. Veriprompt walks the ladder from the top, uses the first rung that has any provider in it, and records which rung it landed on.
Two properties make tiers different from the preference weights you may already be using:
- Weights are economic: a boost blends into a composite score and can be outvoted by a latency or cost advantage.
- Tiers are categorical: while any provider satisfies a tier, no provider outside that tier is considered at all. A 20 ms latency win cannot overturn "German provider if one exists."
Use weights for "cheaper is nicer." Use tiers for requirements that came from a contract, a regulator, or your own counsel.
Typical Use Cases
- Data residency with a documented fallback — prefer in-country hosting, accept a named neighbouring jurisdiction, never leave the region entirely.
- Sovereignty ladders — national provider first, EU provider second, with the drop legible in the audit trail.
- Output traceability — prefer a provider that does not watermark its output; accept one whose marking you can clean afterwards.
- Certification ladders — prefer a provider carrying every certification you would like; accept one carrying the subset you actually require.
- Vendor-preference under contract — prefer providers named in a negotiated agreement; accept anything the compliance floor allows if none is available.
The floor and the preference ladder
This is the part that is easiest to get wrong, so it gets its own section. Four rules, and they never bend:
- Hard constraints are absolute and are never relaxed by tiering. They are the floor. A provider your hard constraints exclude stays excluded. There is no tier, no order, and no fallback that puts it back in play.
- Tiers are applied on top of the hard-constraint result, in array order. Tiering can only ever narrow what the floor already allowed. The order of the array is your ranking of acceptability — it is meaningful data, not cosmetic.
- The first tier with at least one surviving provider wins. Later tiers are not evaluated at all. Each tier is evaluated against the full hard-constraint result, not against the previous tier's output — tier 2 is what you accept instead of tier 1, so it does not inherit tier 1's filter.
- If no tier matches, the hard-constraint-only set is used. This is a degradation, not a failure. The request still routes, because every remaining candidate already satisfies your floor. This is materially different from an unsatisfiable hard constraint, which legitimately produces no route at all.
The practical consequence: put anything you will never accept in hardConstraints, and anything you would rather not accept in a lower tier. If you find yourself writing a bottom tier that you would not actually tolerate, it belongs in the floor instead.
Worked example: prefer Germany, accept Switzerland
The requirement: the request must stay in the EU/EFTA area, full stop. Within that, a German provider is strongly preferred; a Swiss provider is an acceptable second choice.
json
{
"version": "routingPolicyV1",
"name": "DE preferred, CH accepted",
"scope": "company",
"hardConstraints": {
"geo": {
"allow": {
"memberships": ["EU"],
"countries": ["CH", "NO", "IS", "LI"]
}
}
},
"preferenceTiers": [
{
"label": "germany",
"constraints": {
"geo": { "allow": { "countries": ["DE"] } }
}
},
{
"label": "switzerland",
"constraints": {
"geo": { "allow": { "countries": ["CH"] } }
}
}
],
"preferences": {
"weights": { "latency": 0.25, "cost": 0.25, "quality": 0.25, "stability": 0.25 }
}
}Note the shape: preferenceTiers sits beside hardConstraints, never inside it. A tier's constraints block accepts the same fields as hardConstraints, so anything you can require as a floor you can also express as a preference — geo, provider compliance, allow/deny lists, watermarking, data-training policy, privacy tier, latency, cost, availability.
Case 1 — a German provider exists
The floor keeps every EU/EFTA provider. Tier germany is evaluated against that set and returns at least one candidate, so it wins immediately. Tier switzerland is never evaluated. Scoring (latency, cost, quality, stability) then runs within the German candidates only — a cheaper French provider that cleared the floor is not considered, because tier 1 matched.
json
"preferenceTier": {
"satisfied": "germany",
"attempted": ["germany"],
"degraded": false,
"requiresResponseCleaning": false
}Case 2 — no German provider, but a Swiss one
Tier germany returns nothing, so Veriprompt moves to the next rung. Tier switzerland returns candidates and wins. The request routes to a Swiss provider, and the drop from your first choice is recorded:
json
"preferenceTier": {
"satisfied": "switzerland",
"attempted": ["germany", "switzerland"],
"degraded": true,
"requiresResponseCleaning": false
}degraded: true means "you did not get your first choice." It does not mean anything went wrong.
Case 3 — neither exists, but the floor still has candidates
Say your only available providers that day are in France and Ireland. Both tiers come back empty. The request still routes, using the hard-constraint set — France and Ireland both satisfy your EU/EFTA floor, so both are legal destinations under this policy. The fall-through is recorded with satisfied: null:
json
"preferenceTier": {
"satisfied": null,
"attempted": ["germany", "switzerland"],
"degraded": true,
"requiresResponseCleaning": false
}Contrast this with a genuinely unsatisfiable hard constraint. If your floor demanded countries: ["DE"] and no German provider existed, there would be no route at all — not a degraded one. That is the difference the two fields exist to draw.
Second use case: prefer no watermarking, accept marking we can clean
Some providers fingerprint their own output. If your threat model includes "this response could later be attributed to us," you would rather use a provider that does not mark at all — but you may be willing to accept a marked response if it is cleaned before you use it.
json
{
"hardConstraints": {
"geo": { "allow": { "memberships": ["EU"] } }
},
"preferenceTiers": [
{
"label": "no-marking",
"constraints": {
"watermark": { "excludeMarked": true, "treatUnknownAsMarked": true }
}
},
{
"label": "clean-after",
"constraints": {},
"requiresResponseCleaning": true
}
]
}Tier no-marking is deliberately strict: treatUnknownAsMarked means a provider nobody has probed does not count as unmarked. If any positively-verified unmarked provider exists, it is used.
Tier clean-after states no additional constraint, so it always has candidates — it is a catch-all rung. Its purpose is not to filter but to carry requiresResponseCleaning: true, which then appears on the routing result:
json
"preferenceTier": {
"satisfied": "clean-after",
"attempted": ["no-marking", "clean-after"],
"degraded": true,
"requiresResponseCleaning": true
}Be aware of what this flag does today. requiresResponseCleaning is recorded and surfaced on the routing result so that a downstream consumer can act on it. It is not yet consumed — Shield's and Veritas's response-cleaning passes do not currently read it, and that wiring is a deliberate follow-up. Setting the flag today does not by itself cause a response to be cleaned. The reason to author it now is that a policy written today will already carry the flag when that consumer lands; do not treat it as active end-to-end behavior in the meantime.
Shield's own watermark avoidance option is a separate, already-working mechanism operating at the Shield request level. It is not driven by this flag.
Seeing what happened
A degradation you cannot see is worse than no degradation policy at all, so every tiering outcome is recorded on the routing result under preferenceTier:
| Field | Meaning |
|---|---|
satisfied | Label of the tier that matched, or null if every tier was empty and routing fell through to the hard-constraint set |
attempted | Every tier label tried, in order — including the ones that came back empty |
degraded | true when satisfied is not the first tier attempted (or is null); i.e. you did not get your first choice |
requiresResponseCleaning | The satisfied tier's cleaning flag; false when no tier matched |
degraded is precomputed rather than left for each reader to derive, so a dashboard, an alert rule, and an audit export cannot disagree about what counts as a degradation.
The block is absent entirely when the policy declares no tiers, and also when the hard constraints themselves excluded everything — in that case tiering never ran, so reporting a fall-through would be misleading.
You can see this record today in two places:
- Policy simulation —
POST /api/v1/routing/policies/{policyId}/simulatereturns it undersimulation.preferenceTier. Use this to check a ladder before you make the policy live. - Provider ranking —
POST /api/v1/routing/rankreturns it underranking.preferenceTier.
attempted is the part a compliance reviewer will care about: it shows which rungs were tried and found empty, which cannot be reconstructed later by comparing the chosen provider against the policy document.
Configuration
Preference tiers are part of the routing policy document, as the preferenceTiers array.
In the app, open Admin → Routing Policies, create or edit a policy, and expand the Preference Tiers section (between Geofencing and Advanced Settings). Add a tier, give it a label, and set the countries, regions, or watermarking rule it prefers. The tiers are evaluated top to bottom, so use the move-up and move-down controls to put your first choice at the top — the order shown in the Evaluation order strip is exactly the order routing walks. Labels must be unique and non-empty, because the label is what the audit record carries.
The section deliberately covers the geo and watermarking dimensions. To express a tier on any other constraint the floor supports — provider compliance, data-training policy, privacy tier, latency, cost — set it through the Routing Policies API. A tier authored through the API keeps those constraints intact if the policy is later opened and re-saved in the editor; the editor preserves what it does not render rather than dropping it.
Each tier takes:
| Field | Required | Description |
|---|---|---|
label | Yes | Operator-facing name, e.g. germany. Appears in the audit record, so make it meaningful to whoever reads it later |
constraints | Yes | Same shape as hardConstraints; state only the dimension this tier cares about. Anything omitted is already governed by the floor |
requiresResponseCleaning | No | Marks that this tier's output must be cleaned downstream. Recorded on the result; see the caveat above |
Limitations
requiresResponseCleaninghas no consumer yet. It is recorded and surfaced, not acted on. Shield/Veritas consumption lands separately.- Tiers are not visible on the gateway execute response. The record is produced by the ranking engine and returned by the simulate and rank endpoints. A degradation on a normal gateway execution is logged server-side but is not echoed in the execution response body today.
- A tier cannot rescue a provider the floor excluded. If a ladder looks like it is doing nothing, check whether your hard constraints already removed the candidates the top tier was meant to find.
- An empty
constraintsobject matches everything. That is useful as an intentional catch-all rung (see the watermark example), but an accidentally empty tier will silently win and stop the ladder. - Order is data. Reordering the array changes routing behavior. Treat a tier reorder as a policy change, not a cosmetic edit.
Troubleshooting
The ladder always lands on the bottom tier
Your top tiers are probably empty after the floor is applied. Simulate the policy and read attempted — every label listed before satisfied returned no candidates. Check whether the providers you expected are registered with the location, compliance, or watermark metadata the tier filters on; a provider with missing metadata is excluded by a fail-closed constraint.
preferenceTier is missing from the simulation result
Either the policy declares no preferenceTiers, or the hard constraints excluded every provider. In the second case primary.provider will be none — fix the floor first, not the ladder.
A request routed somewhere I did not expect
Check degraded. If it is true with satisfied: null, no tier matched and routing used the hard-constraint set — which is by design. If that outcome is unacceptable to you, the requirement belongs in hardConstraints, where an empty result correctly blocks the request.
Related docs
- Intelligent Routing — policies, weights, and how provider selection works overall
- Geofencing — the geo rules a tier's
geoblock reuses - Watermark Avoidance — Shield's separate, request-level marking check
- Data Privacy & Training — the
dataTrainingandprivacyTierconstraints - Routing Policies API — creating and updating policy documents
