Resource Management
The VoiceRun CLI exposes platform resources through four parallel verbs: vr get, vr describe, vr create, and vr delete. The current resource set covers agents and their functions, organization-scoped environments, releases that bind agents to environments, entrypoints that route traffic, organization secrets, phone numbers, telephony providers, and agent templates.
Listing Resources#
vr get prints a table of resources. Every command takes an optional positional filter that narrows the same table down to a single matching row — vr get agents my-agent is the one-row form of vr get agents.
vr get agents # List all agents vr get functions <agent> # Functions for an agent vr get environments # Organization-scoped environments vr get releases # Releases (filterable by agent/environment) vr get entrypoints # Organization-scoped entrypoints (filterable by type) vr get secrets # Organization secrets vr get phonenumbers # Organization phone numbers vr get telephony # Telephony providers vr get templates # Available agent templates vr get organizations # Organizations you belong to vr get organizations "Prim Voices" # Single-row organization match vr describe organization "Prim Voices" # Detailed organization info
The <agent> argument accepts a name or ID and defaults to the agent in .voicerun/agent.lock when run inside a project.
vr get releases supports --agent / --environment filters. vr get entrypoints supports --type (phone, web, native).
Viewing Resource Details#
vr describe shows full detail for a single resource:
vr describe agent <name-or-id> vr describe function <agent> <name-or-id> vr describe environment <name-or-id> vr describe release <release-id> vr describe entrypoint <name-or-id> vr describe secret <name-or-id> vr describe phonenumber <id-or-phone-or-friendly-name> vr describe telephony <name-or-id>
vr describe entrypoint shows the environment and ordered routes with each release's share within its route.
Environments#
Environments are organization-scoped — created once for the organization (for example, staging and production) and reused across agents. Agents are bound to an environment by creating a release.
vr create environment production vr create environment staging --description "Pre-production testing" vr delete environment staging
Releases#
A release ties (agent, function, environment, rendered manifest) together at a point in time. The latest release for (agent, environment) is implicitly the active one.
Create a release with vr release. Other release operations:
vr get releases # All releases vr get releases --agent my-agent --environment production vr describe release <release-id> vr delete release <release-id>
Dedicated release lifecycle#
Releases whose Deployment spec sets dedicated: true run in their own pods with a per-release image, and can be stopped, started, and restarted without rebuilding:
vr stop release <release-id> # Scale pods to zero (standby) vr start release <release-id> # Bring a standby release back (~seconds) vr restart release <release-id> # Rolling bounce, same image
Stop puts the release in standby: the built image and infrastructure stay, only the pods go, so vr start release is seconds rather than a rebuild. Restart is rejected while in standby — start it instead.
Stopping a serving release — the newest for its (agent, environment), or one an active entrypoint routes to — takes live traffic down, so the API refuses it (409) unless you pass --force:
vr stop release <release-id> --force
Dedicated release builds run asynchronously: vr release returns as soon as the release is created and then follows the build to completion (created → building → starting → running), printing the final state. Pass --no-wait to return immediately and watch progress with vr get releases (STATE column) or vr describe release.
The platform derives an operational status for every dedicated release from operator intent plus the continuously observed pod state (never hand-maintained, so it cannot drift):
| Status | Meaning |
|---|---|
created | Release exists; nothing has been built or observed yet |
building | The per-release image build is in flight |
starting | Pods are coming up but none is ready yet |
running | At least one pod is ready |
degraded | Pods should be running but show failure signals (crash loops, restarts) |
stopping | Stop requested; pods still winding down |
standby | Deliberately stopped — not a failure |
failed | The release build/prepare failed |
The status appears in release API responses (operationalStatus), the app's releases tab and admin coderunner page, and the output of the lifecycle commands.
Automatic standby and wake
Two platform behaviors manage dedicated release pods without operator action:
- Idle reaper (per-environment, off unless configured): a release that has served no sessions for the configured window is automatically put in standby — pods scale to zero, the built image stays. Idleness is judged only on fresh monitoring data, and a freshly started release always gets a full idle window before it can be reaped.
- Wake-on-demand (always on): any session that reaches a standby release — a phone call, web session, native client, or simulation — automatically scales it back up and connects once a pod is ready, typically within seconds since the image is already built. This also covers manually stopped releases:
vr stop releasefollowed by an incoming call wakes the release rather than failing it.
Together they mean dormant dedicated releases cost nothing to keep active, and the first call after a quiet period simply waits a few seconds instead of failing.
Entrypoints#
Entrypoints route phone, web, and native traffic through named routes in one environment. See Routing below.
Creating an entrypoint#
There are three kinds: phone, web, and native. Each accepts --release <releaseId>[:<weight>] (repeatable). Weight defaults to 1 when omitted; weights don't need to sum to a particular number — shares are computed relative to the total. --environment and at least one --release are required.
# Phone entrypoint pointing at a single release vr create entrypoint phone support-line --environment production \ --release REL_ID \ --phone-number +15551234567 \ --friendly-name "Support" --direction inbound # 70/30 A/B split vr create entrypoint phone support-line --environment production \ --release rel-a:70 --release rel-b:30 \ --phone-number +15551234567 # Web (widget) entrypoint vr create entrypoint web widget --environment production \ --release REL_ID \ -o https://acme.com -o https://app.acme.com # Native SDK entrypoint vr create entrypoint native mobile-app --environment production \ --release REL_ID \ --client-id mobile-ios \ --client-secret-secret-id SECRET_ID
Updating an entrypoint#
Mutate an existing entrypoint with vr update entrypoint. Passing --release replaces the catch-all release pool and preserves conditional routes.
# Cut over to a new release at 100% vr update entrypoint ENTRYPOINT_ID --release rel-new:100 # Replace the weighted list (replaces, not appends) vr update entrypoint ENTRYPOINT_ID --release rel-a:50 --release rel-b:50 # Disable an entrypoint without deleting it vr update entrypoint ENTRYPOINT_ID --status disabled # Rename vr update entrypoint ENTRYPOINT_ID --name new-name # Allow an inbound number to also originate outbound calls (phone entrypoints only) vr update entrypoint ENTRYPOINT_ID --direction both
Provide at least one routing, environment, or metadata option.
--direction only applies to phone entrypoints. It changes just the call direction — the rest of the phone config (number, telephony provider, …) is preserved. The API rejects the change with a 409 if the new direction conflicts with another entrypoint claiming the same phone number.
vr create entrypoint update still works as a deprecated alias and prints a warning directing you at vr update entrypoint.
Delete an entrypoint:
vr delete entrypoint <name-or-id>
Secrets#
Secrets are organization-scoped values stored in GCP Secret Manager — only metadata touches the VoiceRun database. They're referenced from rendered manifests via {{ Secrets.organization.NAME }} placeholders, which Helm leaves intact through rendering and the API resolves at session start. Secrets never round-trip through the release record itself.
vr create secret OPENAI_API_KEY sk-... vr describe secret OPENAI_API_KEY vr delete secret OPENAI_API_KEY
Reference a secret from the declarative resource that consumes it. To inject one into context.variables, put the placeholder in Deployment.spec.variables:
apiVersion: voicerun/v1 kind: Deployment metadata: name: my-agent-runtime spec: variables: OPENAI_API_KEY: "{{ Secrets.organization.OPENAI_API_KEY }}"
Put non-sensitive configuration directly in Deployment.spec.variables or a values file. At session start, VoiceRun resolves referenced organization secrets and exposes the resulting variables through context.variables.
Phone Numbers#
Register an existing phone number or purchase a new one from a connected telephony provider:
vr create phonenumber --phone-number +15551234567 # Register existing number vr create phonenumber --purchase --area-code 415 # Purchase new number vr create phonenumber <telephony-id> --purchase -a 212 -c US # From specific provider
| Flag | Description |
|---|---|
--purchase | Purchase a new number from the telephony provider |
--area-code, -a | Area code for the number |
--country-code, -c | Country code (default: US) |
--friendly-name, -n | Friendly name for the number |
--phone-number, -p | Phone number to register (without --purchase) |
Delete or release a phone number:
vr delete phonenumber +15551234567 # Remove from VoiceRun vr delete phonenumber +15551234567 --release # Release back to the provider
Telephony Providers#
Connect a Twilio or Telnyx account:
vr create telephony
Without flags, this runs interactively. You can also pass all values up-front:
vr create telephony --name "My Twilio" --provider-type twilio \ --account-sid AC123 --api-key-sid SK123 --api-key-secret secret123 vr create telephony --name "My Telnyx" --provider-type telnyx --api-key KEY123
Templates#
Create a reusable agent template from the current project:
vr create template my-template vr create template my-template --description "Restaurant booking agent" --category booking --public vr create template my-template --upsert # Update if it exists, else create
--public is admin-only; non-admins get a private template. --upsert matches on (name, visibility) — flipping a template between public and private stays a deliberate delete + recreate.
agent.lock is automatically excluded from published templates so consumers don't inherit the author's agent/function IDs.
Deleting Resources#
vr delete agent <name-or-id> vr delete function <agent> <name-or-id> vr delete environment <name-or-id> vr delete release <release-id> vr delete entrypoint <name-or-id> vr delete secret <name-or-id> vr delete phonenumber <name-or-id> vr delete phonenumber <name-or-id> --release # Release back to provider vr delete telephony <name-or-id> vr delete template <name-or-id>
Routing#
An entrypoint belongs to one organization environment and has an ordered list of named routes. The first matching route chooses a release from its weighted pool. Releases may belong to different agents, but must share the entrypoint's environment. Exactly one catch-all route comes last. Its conventional name is default.
Create an entrypoint with an explicit environment and at least one release:
vr create entrypoint phone support-line --environment production \ --phone-number +14015550100 --direction both --release <stable-release>
Supported routing attributes#
The API accepts exactly these matcher attributes. Availability depends on how the session enters the entrypoint:
| Matcher | Phone /start | Web/native /start | /outbound |
|---|---|---|---|
caller | NormalizedCallPayload.callerPhone: the calling number | Unavailable | Unavailable |
called | NormalizedCallPayload.calledPhone: the called number | Unavailable | destination.value: the dialed phone number or normalized SIP destination |
custom_parameters[<name>] | The custom_<name> URL query parameter, decoded once | Unavailable | Unavailable |
sip_headers[<Header>] | The corresponding string SipHeader_<Header> field in the phone webhook | Unavailable | Unavailable for matching |
track | Unavailable | String track in the request body | TriggerOutboundRequest.track, including engine input_parameters.track |
parent[track] | Unavailable | Unavailable | Parent AgentSession.track, found through parameters.parent_session_id, only within the same organization |
An unavailable attribute cannot satisfy a matcher. Sending From, Caller, To, or SIP fields in a web/native request does not expose phone matchers. /outbound does not expose caller or sip_headers[<Header>] to route selection, even when the outbound call has a caller number or outgoing SIP headers. Arbitrary request fields and inputData are not matcher attributes.
Matching is exact and case-sensitive, including SIP header names and track values. Use the normalized destination string for called; matching does not support prefixes, regexes, or phone-number ranges. Routes are evaluated in order, so a requested track takes precedence over parent[track] only if its matching route comes first. Supplying /outbound releaseId pins a release and bypasses route selection.
Route by the called destination#
For an outbound phone entrypoint, use called to choose a release based on the number being dialed. This example inserts priority-destination immediately before the catch-all; its release must belong to the entrypoint's environment:
vr update entrypoint support-outbound \ --set-route 'priority-destination: called=+15555550103 => <destination-release>' vr outbound support-outbound --to +15555550103
If no earlier conditional route matches, dialing +15555550103 selects priority-destination and records AgentSession.track = "priority-destination". Other destinations continue through the remaining routes and catch-all. On phone /start, called instead matches the called number supplied by the incoming webhook.
Canary rollout: release, route, force, promote#
Create the new release with vr release production. Use its returned ID below. Route selected callers to it, and give other calls a 10% chance of using it:
vr update entrypoint support-line \ --route 'canary: caller=+15551234567,+15557654321 => <new-release>' \ --route 'default: => <stable-release>:90 <new-release>:10' vr describe entrypoint support-line
Call from either listed number to force the canary route. Other callers use the default route's weighted pool. session.track records the route name, so a call that draws the new release from the default pool still has track: default. Use releaseId when comparing release versions and track when comparing routing cohorts.
vr get sessions --track canary vr update entrypoint support-line --promote canary
Promotion replaces the catch-all's pool with the canary's pool and removes the canary route. Roll back by pointing the catch-all at the previous release:
vr update entrypoint support-line --release <stable-release>
Route grammar and edits#
<name>: [<attribute>=<value1>,<value2> [<attribute>=<value>] [| ...]] => <releaseId>[:<weight>] ...
Values separated by commas are OR; attributes separated by whitespace are AND; | separates OR entries. Matching is exact and case-sensitive. A release weight defaults to 100 in a route string; weights are relative and need not total 100. Quote the whole route string in your shell.
| Command option | Behavior |
|---|---|
--route SPEC (repeatable) | Replace all routes in the given order |
--set-route SPEC | Replace a named route, or insert it before the catch-all |
--before NAME / --after NAME | Place a set or moved route relative to another named route |
--move-route NAME | Move an existing route; requires before or after |
--remove-route NAME | Remove a conditional route |
--promote NAME | Copy its pool to the catch-all, then remove it |
--release ID[:WEIGHT] | Replace only the catch-all pool; preserve conditional routes |
--environment NAME_OR_ID | Repair or change the environment; every route's releases must belong to it |
Route edits use If-Match: "N" with the integer Entrypoint.routingVersion read before editing. The API increments routingVersion once when updating routes, the releases alias, or environmentId; metadata-only changes leave it unchanged. Replacing routes without a version returns 428; an outdated version returns 412. The app can send the same expected value as routingVersion in the PATCH body. updatedAt remains a modification timestamp and is not a concurrency token. A conflict requires reloading and reapplying the edit; the CLI never silently retries with stale routes. Route names must be unique slugs; default is reserved for the catch-all. Empty or zero-total pools, unknown attributes, deleted/cross-environment releases, and shadowed/duplicate routes are rejected. Overlapping routes are allowed; vr describe and the app warn that order matters.
For automation, capture routingVersion with vr get entrypoints NAME --json, then pass it explicitly:
vr update entrypoint support-line --promote canary --expected-routing-version 7 --json
The command rejects a mismatch before writing and sends the same supplied version as If-Match on the PATCH, protecting against a change after its read too. Without this option, it retains the existing behavior of using the version read within the command. The option requires a routing or environment update, not a metadata-only edit; it fails against APIs without a valid routingVersion. A conflict exits nonzero without retrying. Only consume the JSON success document when the exit code is zero; diagnostics and errors (including DEBUG request logging) go to stderr in JSON mode, leaving stdout for the success document.
For conditional rollback, save the successful response's data.routes and data.routingVersion: the API returns the snapshot produced by that conditional update, rather than a later read. Restore the complete pre-update route list using repeated --route arguments and pass the successful response's data.routingVersion as --expected-routing-version. A subsequent routing change makes that rollback fail instead of overwriting the newer edit. --release alone restores only the catch-all pool, not a conditional route removed by promotion. Never substitute a newly read version after a conflict without inspecting the new state and deciding whether the edit is still appropriate. These preconditions protect one entrypoint per request; they do not make changes across entrypoints atomic. A transport failure can leave the result unknown, so inspect routing before attempting recovery.
vr release production -e support-line replaces the catch-all with the newly created release at weight 100; --weight 10 appends to that pool. vr release production -e support-line --route canary updates that named route. The route must already exist; a missing route fails before release creation. Its matchers are preserved. Create it explicitly first with vr update entrypoint support-line --set-route 'canary: caller=+15551234567 => <existing-release>'. The name canary has no special meaning.
Web/native client tracks#
A web/native /start body can include {"track":"next"}. It only selects a different route if the operator configured a corresponding matcher:
vr update entrypoint widget --set-route 'next: track=next => <new-release>' vr outbound support-line --to +15551234567 --track next vr debug --release <new-release> --track next
Outbound uses track as a routing attribute. Debugging a release directly stamps the supplied track without selecting a route. Sessions created without an entrypoint have a null track unless explicitly supplied. vr simulate --track NAME stamps direct simulation sessions. origin remains independent of track.
Chained sessions#
The engine sends parent_session_id automatically. The API exposes its track to routing only when the parent belongs to the same organization. A null or foreign parent track matches nothing.
vr update entrypoint outbound-line \ --route 'canary: parent[track]=canary => <new-outbound-release>' \ --route 'default: => <stable-outbound-release>'
A handler may supply input_parameters={"track": "spanish"}. To make that override inheritance, place a track=spanish route before the parent[track] route.
SIP trunk / PBX headers#
SIP delivery can carry custom headers; ordinary PSTN callers carry no custom headers. Twilio SIP trunk webhooks expose SipHeader_<Name>. The VoiceRun SBC forwards headers configured in the DID's sip_headers allowlist.
vr update entrypoint support-line \ --set-route 'pbx-canary: sip_headers[X-VR-Track]=canary => <new-release>' --before canary
Custom URL query parameters#
On inbound phone /v1/entrypoints/:id/start, a matcher custom_parameters[<name>] reads the URL query parameter custom_<name> before release selection. Route authors choose the names; there is no platform-maintained allowlist. Names are case-sensitive, start with a letter, digit or underscore, and contain only letters, digits, underscores, dots or hyphens ([A-Za-z0-9_][A-Za-z0-9._-]*). Values are exact strings: false is different from true, False, and 0. Case and whitespace are preserved.
For example, configure the upstream Twilio webhook or redirect URL as:
https://api.voicerun.com/v1/entrypoints/<entrypoint-id>/start?custom_agent_profile=assistant&custom_warm_transfer=false
In TwiML XML, escape the query separator as &. These are HTTP URL query parameters, not nested TwiML Stream <Parameter> values: stream parameters arrive after release selection and cannot select an entrypoint route. Web/native /start, /outbound, request-body fields and release deployment defaults do not supply custom_parameters routing attributes.
vr update entrypoint support-line \ --set-route 'pilot: custom_parameters[agent_profile]=assistant custom_parameters[warm_transfer]=false => <stable-release>:95 <candidate-release>:5'
This inserts the route before the existing catch-all and preserves other routes. Calls with both values enter the 95/5 pool; calls with another profile, warm_transfer=true, or missing/invalid required parameters continue to later routes and ultimately the catch-all. Ordinary strings can be chosen for any parameter; agent_profile and warm_transfer have no special platform meaning.
You can also configure the complete route list at creation:
vr create entrypoint phone support-line --environment production \ --phone-number +15555550100 \ --route 'pilot: custom_parameters[customer_segment]=blue => <candidate-release>' \ --route 'default: => <stable-release>'
Use --route or --release, not both. --route requires exactly one catch-all. The existing route-string grammar uses whitespace, commas and | as separators; use the app or the API's JSON route representation for literal values containing those delimiters. vr get entrypoints --json and vr describe entrypoint --json preserve all matcher keys and values. Human output shows the route conditions and weighted targets. Unrelated named edits and vr release --route preserve existing custom conditions.
Routing eligibility (separate from agent parameter forwarding):
- For routing, URL encoding is decoded exactly once;
+means a space,%2Ba literal plus, and%2526the literal text%26. - Missing and empty parameters do not match. An empty string still forwards to the agent and overrides a deployment default.
- Repeated names (even identical values), malformed percent/UTF-8 encoding and bracket/object/array forms do not match. These routing checks do not filter parameters forwarded to the agent. A mixed scalar/bracket value invalidates the scalar too.
- Agent parameter forwarding is unchanged:
CallParametersService.build()continues to read Expressreq.query, apply its existingdecodeURIComponentconversion, and merge request parameters over deploymentcustomParameterswith the existing precedence. Routing name validation, duplicate rejection and scalar checks apply only to route selection. Existing malformed-value forwarding errors are unchanged. Deployment defaults never determine the routing cohort. - Routing and forwarding intentionally use different decoding paths: for example, incoming
custom_text=%2526matches routing value%26, while the existing forwarding decoder passes&to the agent. - Existing exact AND/OR semantics, route ordering, shadowing validation, authorization, simulation release pinning and
routingVersion/If-Matchprotection still apply. Deploy the API before clients that author custom matchers. No route is changed automatically.
Compatibility window#
Existing entrypoints route through a default route even before backfill. The releases field remains a readable/writable alias for the catch-all pool during one CLI/app cycle. Legacy entrypoints may have environmentId: null until backfill. A release update can infer it when all stored and proposed releases belong to one environment. Empty pools, mixed environments, and missing/deleted or environment-less releases require explicit repair: set the environment and valid target releases before adding routes. Existing sessions keep a null track.
vr describe entrypoint displays “environment unresolved — fix before adding routes” when environmentId is absent; it does not assume the entrypoint spans multiple environments.
When the API does not return a valid routingVersion, explicit route edits report that the API must be upgraded. vr release --route checks this before creating a release. Legacy catch-all --release updates retain their compatibility behavior. The app requires routingVersion for release edits; deploy the API before the app.
Nested matcher JSON#
Route conditions use nested JSON groups: custom_parameters, sip_headers, and parent. caller, called, and track remain top-level arrays. All conditions within one match object are ANDed; separate match objects are ORed.
{ "name": "pilot", "match": [{ "custom_parameters": { "agent_profile": ["assistant"], "warm_transfer": ["false"] }, "sip_headers": { "X-Customer-Segment": ["premium"] } }], "releases": {"stable-release": 95, "candidate-release": 5} }
CLI route arguments use custom_parameters[agent_profile]=assistant, sip_headers[X-Customer-Segment]=premium, and parent[track]=pilot. Quote the complete route argument to protect brackets and | from the shell. A name such as segment.v2 remains a literal name inside its group; dots never introduce further nesting.
The API and app use nested matcher JSON. vr get entrypoints --json and vr describe entrypoint --json return the same nested structure.
Route updates require If-Match with the current routingVersion.
