Routes & Policies
Runs unlicensed, in full. The complete Policy-as-Code engine — DSL, versioning, dry-run and shadow rollout — is available in the Development posture. Earlier releases offered only a static model allow/deny list here; 1.7.0 removed that split.
Routes decide which provider answers a request. Policies decide whether the request is allowed at all. Both are versioned, both propagate live to the data plane without restart, and both support dry-run / shadow modes before going active.
Route configuration
Open Routing → Routes in the sidebar.
Route list
Lists every route with ID, model pattern, strategy, providers with weights, version, and pinned model version. Auto-refreshes every 10 seconds. Click Edit to open the route editor or Delete to remove a route.


Create / edit route
Click New Route to create a route with:
- Model Pattern — glob pattern to match model names (e.g.
gpt*,claude*,ollama/*) - Routing Strategy —
model-prefix,round-robin,weighted,latency-aware,cost-aware,canary,geo-aware,intelligent - Pinned Model Version — optional, pins requests to a specific model version
- Providers — dynamic table of provider entries with name, weight, and region. Add / remove rows as needed.
Changes propagate to every gateway instance within a few seconds — no restart and no deployment required. Routes are platform-global (no workspace scope); policies, by contrast, are workspace-scoped or global (blank workspace = global).
Every route mutation (ROUTE_CREATED / _UPDATED / _ROLLED_BACK / _DELETED) and policy mutation (POLICY_CREATED / _UPDATED / _STATUS_CHANGED / _ROLLED_BACK / _PROMOTED / _DELETED) emits a signed audit event — from the Console and from the workspace Portal (policies only).


Version history
In edit mode, the right panel shows the route's version history (last 10 retained). Each version has two actions:
- Diff — loads a field-by-field comparison between the selected version and the current configuration. Changed fields are highlighted with an amber background.
- Rollback — rolls back the route to the selected version (creates a new version with the restored configuration). Requires confirmation.


Canary A/B testing
When a route uses the canary strategy, a dedicated canary dashboard is available at Routes > (route) > Canary.
Dashboard features:
- Canary configuration — baseline provider, candidate provider, current split percentage, and workspace scope
- Live metrics (auto-refreshes every 5 seconds) — side-by-side comparison of baseline vs. candidate:
- Request count
- Error count and error rate
- Total and average latency
- Total and average cost
- Split adjustment — update the traffic split percentage on the live route without restarting
- Reset — clear all canary metrics and start fresh
Canary and shadow metrics are persisted and survive pod restarts, deployments, and autoscaling events. Counters are aggregated cluster-wide — the dashboard shows a single combined view across every gateway instance, not a per-pod slice. Deleting a route cleans up its live metrics automatically.


Route shadow dashboard
Shadow sends a copy of live traffic to a second provider without serving its response, so you can compare a candidate against production on real requests. Routes → (route) → Shadow reports the comparison and offers a Reset; the report fragment polls every 5s.
Shadow is a property of a route, not a strategy — the route form carries shadowConfig under any strategy, alongside modelTiers (revealed for intelligent), and offers geo-aware and intelligent as strategies.
Before 1.7.0 an edit rebuilt the route from the form, so any field the form did not render was destroyed on save — shadowConfig among them. The save succeeded, the rendered fields were right, and sampling silently stopped. An edit now mutates the loaded route, so a field the form does not show is preserved rather than dropped.
If you configured shadow before 1.7.0 and later edited that route from the Console, check the shadow config is still there.
Policy management
Open Governance → Policies in the sidebar.
Policy list
Lists every policy with ID, name, workspace, status badge (DRAFT / ACTIVE / SHADOW / ARCHIVED), version, and last updated timestamp. Auto-refreshes every 10 seconds.
Use the Workspace dropdown filter to narrow the list. Status badges are color-coded: blue for DRAFT, green for ACTIVE, purple for SHADOW, grey for ARCHIVED.


Create / edit policy
Click New Policy to create a policy with:
- Name — human-readable policy name
- Workspace ID — scope to a workspace (blank = global)
- DSL — YAML policy rules, edited in an in-browser YAML editor with syntax highlighting
- Status — lifecycle status (
DRAFT,ACTIVE,SHADOW,ARCHIVED)


In edit mode, the right panel shows:
- Version History — same diff and rollback functionality as routes
- Status Changes — change the policy lifecycle status
- Dry-Run — simulate the policy against a sample request and see the decision plus timing
DSL validation at submit time
The YAML is compiled and validated before the policy is persisted. Authoring mistakes that would have silently produced a no-op rule before now fail the submit immediately with HTTP 400 INVALID_POLICY_DSL and a compiler error pointing at the rule id and offending key:
- Unknown keys. Anything that isn't a recognised top-level / rule / condition key — including typos like
condition:(singular) and non-existent fields likeworkspace:as a per-rule condition — is rejected with a hint at the correct shape. The DSL is closed-schema; YAML comments use#as usual but extra unknown keys are not silently absorbed. - Unknown rule actions. Rules accept exactly two actions:
DENYandWARN_AGENT(case-insensitive). Typos likeaction: warn(silently fell through to DENY before) oraction: shadow(a customer reaching for a per-rule shadow that isn't a real product feature) are rejected with a pointer at the policy-lifecycleSHADOWstatus — see Shadow policy dashboard below. - Conflict detection at promotion time. When you flip a policy from
DRAFTtoACTIVE(or promote a SHADOW), the conflict detector checks the candidate against every currently-active policy across all nine condition types — model, tools, MCP server/tool/args, data_residency (flags disjoint allowed-region sets that no request can satisfy), time_of_day (flags redundant rules + timezone mismatches on overlapping ranges), max_tokens, and budget_utilization (flags different thresholds firing at different cap utilization levels). Conflicts are returned as warnings on the status-change response; the policy still activates but operators see the overlap.
How platform and workspace policies interleave
Platform-wide policies (no workspace) and a workspace's own policies are merged into one list and sorted by rule priority alone. Scope is not part of the ordering:
- Lower priority number evaluates first. The first matching
DENYreturns immediately;WARN_AGENTcollects and continues; no match means allowed. - A workspace rule can therefore pre-empt a platform rule. A workspace rule at priority 10 is evaluated before a platform rule at priority 50, and if it denies, the platform rule is never reached. Platform scope confers no inherent precedence — if you need a platform rule to win, give it a lower priority number than any workspace rule can use.
WARN_AGENTis never "overridden". It is not a decision, so nothing later cancels it; warnings from either scope accumulate and are attached to the response.
Two rules with the same priority number are ordered by a stable sort over a list that happens to be built platform-first — so platform currently wins a tie. That is a property of the current implementation, not a documented guarantee. Do not design a hierarchy that depends on it; give rules distinct priorities where the order matters.
CEL expressions for patterns the closed-form matchers can't compose
For patterns the closed-form conditions: matchers above can't express — anything that needs OR, NOT, or arithmetic across fields — a rule can use a per-rule expression: field instead of conditions:. The value is a CEL (Common Expression Language) string compiled and type-checked at submit time. CEL is the sandboxed expression language Kubernetes admission policies, Envoy, Cilium, and gRPC use for the same problem class — no I/O, no host calls, no loops, deterministic, halting-decidable.
rules:
- id: gpt4o-outside-eu-or-large-requests
expression: |
request.model == "gpt-4o" &&
(context.region != "EU" || request.message_count >= 50)
action: DENY
deny_message: "gpt-4o restricted outside EU or for ≥ 50 messages"
That single rule covers a pattern that would otherwise require three separate conditions: rules.
expression: and conditions: are mutually exclusive on the same rule — specifying both fails the submit; specifying neither also fails (a matcher-less rule is almost certainly an authoring mistake). Mixing rule kinds within the same policy is fine — author whichever shape fits each rule.
Evaluation context (closed surface): request.{model, workspace_id, message_count, total_input_chars, metadata[key]}, context.{region, workspace_region}, budget.utilization_pct, plus the CEL standard library (string methods in receiver form — s.startsWith(t), s.contains(t), s.matches(regex) — list/map indexing, in, comprehensions). Two CEL-API notes worth keeping in mind while authoring: (a) every request.* / context.* / budget.* field is declared as Dyn at the type-checker level so the surface can evolve without breaking compiled policies — cast with string(...) before calling string methods (string(request.model).matches("^gpt-.*$"), not matches(request.model, "...")); (b) the embedded CEL library only registers receiver-style overloads for the string methods, so matches(s, regex) fails to compile and s.matches(regex) is the working shape. Temporal functions (now(), hour(ts, tz)) are not in v1 — for time-of-day deny windows use the existing conditions: { time_of_day: ... } matcher.
Static conflict detection is skipped for CEL rules — equivalence between arbitrary CEL expressions is undecidable in general. The promotion endpoint emits one "skipped — verify manually via dry-run" warning per CEL rule on the candidate policy; non-CEL rules in the same policy still get the full 9-detector check above.


Shadow policy dashboard
When a policy is in SHADOW status, a dedicated shadow dashboard monitors divergence between the shadow and currently-active policy decisions without affecting real traffic.
Accessing the dashboard:
- From the policies list: click the eye icon next to any SHADOW-status policy
- From the policy editor: click View Shadow Dashboard in the status panel
Dashboard features:
- Period Selector — toggle between 1 h, 24 h, and 7 d time windows
- Stats Panel (auto-refreshes every 5 seconds) — three cards:
- Total Evaluations — number of times the shadow policy was evaluated
- Divergent Count — times shadow would have denied when active allowed
- Divergence Rate — percentage of evaluations that diverged
- Rule Breakdown Table — divergence counts grouped by rule ID
- Recent Divergent Events Table — latest divergent events with timestamp, request ID, model, workspace, active decision, and shadow decision


Rolling back to version 1 does not return the policy to version 1. It writes version 1's content as a new version — roll back from v2 and you land on v3 carrying v1's rules, with snapshots 1, 2 and 3 all retained.
That is the useful behaviour: the history is append-only, so a rollback is itself auditable and can be rolled back in turn. Verified on 1.7.0.
A policy created outside the Console cannot be rolled back to its first version. The first snapshot is written by the create path, so a policy inserted another way has no version 1 to restore — the rollback redirects with a flash error and changes nothing, which is easy to misread as rollback being broken.
Promoting a shadow policy
Click Promote to Active to transition the policy from SHADOW to ACTIVE status. This:
- Runs conflict detection against existing active policies (warnings shown if conflicts are found)
- Changes the policy status to ACTIVE
- Clears all shadow event data for the policy


This action promotes a policy from SHADOW to ACTIVE within the workspace it already belongs to. It does not copy anything anywhere. If what you want is author in a development workspace, then apply the same rule in production, that is the next section — there is no button for it.
Promoting a policy to another workspace
There is no in-product action that copies a policy from one workspace to another. The supported route is the GitOps export → edit → import cycle on the Import / Export page, which is also what makes the promotion reviewable — the thing you apply to production is a file you can diff and commit.
- Export from
/config/export, scoped to the source workspace or fleet-wide. - Edit the policy entry in the exported JSON: give it a new
idand setworkspaceIdto the target workspace. Keep thedslbyte-identical — that is the whole point of the exercise. - Preview the file at
/config/import. The diff names each entity it will create, and nothing is written until you apply. - Apply. Use
mergemode, which upserts and never deletes.
Trimming the export down to just the policies you are promoting is fine and is the easier file to review:
{
"version": "1.0",
"policies": [
{
"id": "policy-no-expensive-prod",
"workspaceId": "ws-production",
"name": "No expensive model",
"status": "ACTIVE",
"dsl": "version: \"1\"\nrules:\n - id: no-expensive-model\n conditions:\n model:\n denylist: [gpt-4o-expensive]\n action: DENY\n"
}
]
}
Four things worth knowing, all measured on 1.7.0:
- It is a copy, not a move. The source policy is untouched and stays active in its own workspace — which is what you want when the source is a development workspace you will keep iterating in.
- The
idmust be new. Ids are global, not per-workspace, so reusing the source id updates the source policy instead of creating one in the target. That ismergemode doing exactly what it says, and it is the mistake to avoid. statusis carried across, so a policy exported asACTIVEarrives enforcing. Export it asSHADOWinstead if you want to observe divergence in the target workspace before it blocks anything.- The imported policy gets a proper version-1 snapshot, so version history and rollback work on it normally. (Policies inserted straight into the database — by a seeding script, say — do not get one, and their rollback silently does nothing.)
Allow up to a poll interval before the new policy is enforced; config reaches the data plane through the version poller, not immediately on commit.
Output schemas
Structured-output JSON schemas that clients reference by ID (instead of sending inline on every request) live under Routing → Schemas in the sidebar (owner and policy-admin only). The page lists every registered schema with its scope (route id, model pattern, or both) and provides a create / edit form with a JSON-schema textarea plus a delete action behind a confirm gate. Schemas are also carried in the Console's GitOps export for pipelines that prefer to keep them in source control next to the apps that consume them.
See Structured Outputs → Inline schema vs. registered schema for when to register a schema, and Administering DVARA for the REST endpoints.