Declarative Resources
Anything under .voicerun/templates/ is rendered with Helm at vr release time and snapshotted onto the release manifest. Each YAML document declares one resource:
apiVersion: voicerun/v1 kind: Deployment | Simulation | Webhook | Evaluator metadata: name: <unique within the manifest> spec: ...
Preview rendered output with vr render; validate spec shape with vr validate. Values referenced as {{ .Values.foo }} come from .voicerun/values.yaml (and overlays); {{ .Agent.Name }} and friends come from .voicerun/agent.yaml. Secrets are referenced as {{ Secrets.organization.NAME }} — Helm leaves the placeholder intact and the API resolves it at session start.
metadata.name must be unique within a manifest for each kind. It's the handle used by other commands (e.g. vr simulate --name <…>, vr evaluation list --type <…>).
Deployment#
Runtime configuration for the agent in an environment. Every field is optional except kind/metadata.name — omitted fields take platform defaults. The mode field decides whether handler.py is required at the project root.
apiVersion: voicerun/v1 kind: Deployment metadata: name: my-agent-deployment spec: mode: coderunner # 'coderunner' (handler.py sandbox) or 'relay' dedicated: false # true = per-release image + dedicated pods region: us-central1-a variables: LOG_LEVEL: info FEATURE_FLAG: "true" stt: model: nova-3 language: en failover: model: gpt-4o-mini-transcribe # must be a DIFFERENT provider turnTaking: mode: smart_turn externalEndpointing: 300 smartTurnVadStopSecs: 0.4 smartTurnStopSecs: 3.0 smartTurnTimeout: 5.0 tts: provider: elevenlabs model: eleven_flash_v2_5 voice: rachel # voice NAME, not a provider UUID language: en speed: 1.0 relay: url: wss://my-relay.example.com/ws/agent recording: enabled: false location: gs://my-bucket/recordings/ redaction: enabled: false tracing: enabled: true
Top-level fields#
| Field | Type | Description |
|---|---|---|
mode | coderunner | relay | Runtime mode. coderunner (default) runs your handler.py in the sandbox. relay runs in voicerun-relay — no handler.py is required and vr validate skips the handler check automatically. |
dedicated | boolean | When true, each release gets its own container image (built at release time) and dedicated pod(s) instead of the shared sandbox. Dedicated releases report live pod status and support the release lifecycle (vr stop/start/restart release). Default false. |
region | string | Cluster region (e.g. us-central1-a). |
variables | map<string, string> | Values injected into context.variables at session start. Organization-secret placeholders are resolved when the session starts. |
relay | object | Relay endpoint config. Only used when mode: relay. |
stt | object | Speech-to-text config. |
turnTaking | object | Turn-taking strategy. Sibling of stt/tts because the signal can come from STT (provider EoT), raw audio (Silero VAD, Smart Turn V3), or — eventually — semantic analyzers. |
tts | object | Text-to-speech config. |
recording | object | Call recording config. |
redaction | object | PII redaction applied to traces and session events. |
tracing | object | Distributed tracing for the call pipeline. |
spec.relay#
Only honored when mode: relay. Optional failover swaps to a backup relay endpoint on connection errors.
The URL is not parsed or probed at release time — vr release snapshots it into the release manifest verbatim, and it is first used when a session starts. See Relay Mode for transport, TLS, and authentication details.
| Field | Type | Description |
|---|---|---|
url | string | Primary relay WebSocket URL (e.g. wss://relay.example.com/ws/agent). |
failover.url | string | Backup relay WebSocket URL. |
spec.stt#
| Field | Type | Description |
|---|---|---|
model | string | STT model identifier (e.g. flux-general-en, nova-3). |
language | string | BCP-47 language code (e.g. en). |
prompt | string | Prompt biasing for providers that support it. |
filter | string | Provider-specific filter string. |
endpointing | number | Provider-side endpointing silence threshold (ms). |
audioInputDelay | number | Audio input delay (ms) before transcription starts. |
noiseReductionType | string | Provider noise-reduction profile. |
eot.threshold | number | Deepgram-style end-of-turn confidence threshold. |
eot.timeoutMs | number | Hard cap on EoT detection (ms). |
eot.eagerThreshold | number | Eager-EoT pre-confirmation threshold. |
vad.mode | server_vad | semantic_vad | OpenAI VAD mode. Honored only by gpt-4o-transcribe, gpt-4o-mini-transcribe, and gpt-4o-mini-transcribe-2025-12-15. |
vad.eagerness | auto | low | medium | high | OpenAI semantic-VAD eagerness. |
eot.thresholdByLanguage | string | voicerun-asr-realtime-v1 only — per-language eot.threshold overrides, CSV. Not consumed by deployed voice agents yet. |
vadStopMs | number | voicerun-asr-realtime-v1 only — silence floor before the semantic end-of-turn consult (ms). Not consumed by deployed voice agents yet. |
allowedLanguages | string | voicerun-asr-realtime-v1 only — language-ID guardrail set, CSV; "" disables it. Not consumed by deployed voice agents yet. |
failover | object | Same shape as stt (minus failover). Used on provider errors. |
spec.turnTaking#
| Field | Type | Description |
|---|---|---|
mode | provider | silero | smart_turn | How turn boundaries are decided. provider uses the STT provider's built-in endpointing. silero runs local Silero VAD on the relay/agent. smart_turn runs Silero VAD + Smart Turn V3 ML. |
externalEndpointing | number | Silence-stop threshold (ms) for silero / smart_turn. |
smartTurnVadStopSecs | number | smart_turn only — VAD silence-stop window before the ML gates. Default 0.4. |
smartTurnStopSecs | number | smart_turn only — ML's per-window timeout. Default 3.0. |
smartTurnTimeout | number | smart_turn only — hard cap on the ML's running window. Default 5.0. |
Not every model accepts every mode. The Deepgram Flux models (flux-general-en, flux-general-multi) are provider-only, and smart_turn is offered by nova-3, qwen3-asr-flash-realtime-2026-02-10, voicerun-asr-v1, voicerun-asr-realtime-v1, and ink-whisper only. See Speech to Text for the per-model matrix.
spec.tts#
| Field | Type | Description |
|---|---|---|
provider | string | TTS provider (e.g. cartesia, elevenlabs). |
model | string | Provider-specific model id (e.g. sonic-2). |
voice | string | Voice for the provider (e.g. rachel). In coderunner mode this is a platform voice name; in relay mode it must be a provider-native voice ID (for rachel, 21m00Tcm4TlvDq8ikWAM). |
language | string | BCP-47 language code. |
speed | number | Speech rate (provider-specific scale). |
failover | object | Same shape as tts (minus failover). Accepted and forwarded, but currently inert — no runtime reads it, and TTS failover falls back to a fixed built-in provider instead. |
spec.recording#
| Field | Type | Description |
|---|---|---|
enabled | boolean | Record the call audio. Default false. |
location | string | Storage URI override (e.g. gs://my-bucket/recordings/). Leave unset to use the platform default. |
spec.redaction#
| Field | Type | Description |
|---|---|---|
enabled | boolean | Apply PII redaction to traces and session events. Default false. |
spec.tracing#
| Field | Type | Description |
|---|---|---|
enabled | boolean | Emit distributed traces for the call pipeline. Default true. |
Simulation#
A simulated caller used by vr simulate. The CLI submits only the simulation's metadata.name (plus agent, environment, and an optional release ID); the API resolves the entire spec from the release's manifest — the latest release for the agent and environment, or the one pinned with vr simulate --release <id>. The version that runs is always the released one, not whatever is on disk.
apiVersion: voicerun/v1 kind: Simulation metadata: name: happy-path spec: direction: inbound mode: direct # 'direct' (WebSocket, default) or 'pstn' (real phone call) systemPrompt: | You are a customer calling this agent. Keep turns short and realistic. inputData: accountTier: premium numberOfSimulations: 5 provider: gemini_live model: gemini-3.1-flash-live-preview voice: Aoede # Optional: pin delivery variation so a simulated caller is reproducible. varietySeed: 12345 phases: - type: ring durationSecs: 8 - type: message text: "Thank you for calling. All representatives are busy. Please hold." - type: holdMusic durationSecs: 30 loopPhases: true humanPickupAfterSecs: 90
Top-level fields#
| Field | Type | Required | Description |
|---|---|---|---|
direction | inbound | outbound | no | Which side of the call to simulate. Defaults to inbound, where the persona is a caller dialing the agent. Use outbound to test agents that place calls; the persona is the callee who answers. |
mode | direct | pstn | no | How the simulated caller reaches the agent. direct (default) opens a WebSocket to the agents service and emulates the telephony wire format — no carrier in the loop. pstn places a real outbound phone call to the agent's number, exercising codec transcoding, jitter, DTMF-over-PSTN, and the agent's actual telephony ingress. PSTN runs incur telephony charges. Not compatible with direction: outbound. |
toNumber | string (E.164) | no | PSTN only — the number to dial, e.g. +15551234567. Defaults to the phone number assigned to the (agent, environment) the run targets, so most manifests omit it. Setting it without mode: pstn is rejected at vr release time. |
systemPrompt | string | yes | Prompt driving the simulated user's behavior. |
inputData | object | no | Task payload persisted to each spawned session and exposed to the handler as context.input_data. Most useful with direction: outbound, but allowed for inbound simulations too. |
numberOfSimulations | integer (1-100) | no | Number of simulated sessions spawned per vr simulate invocation. Default 1. |
provider | gemini_live | openai_realtime | no | Persona engine vendor. Defaults to gemini_live. |
model | string | no | Provider-specific model id. For gemini_live: e.g. gemini-3.1-flash-live-preview (default). For openai_realtime: e.g. gpt-realtime (default), gpt-realtime-mini. |
voice | string | no | Provider-specific voice id. For gemini_live: Aoede, Puck, Charon, Kore, Fenrir, etc. For openai_realtime: alloy, ash, ballad, coral, echo, sage, shimmer, verse, marin, cedar. |
language | string | no | BCP-47 language tag for the persona, such as en-US or es-MX. Falls back to the simulator default when omitted. |
varietySeed | integer (0-9007199254740991) | no | Seed for the simulator's delivery variation (greeting, phrasing, mood, and verbosity). Omit for fresh randomness; set it to reproduce a specific simulated caller. |
phases | PhaseSpec[] | no | Pre-pickup phase script — see below. |
loopPhases | boolean | no | When phases is non-empty, restart the phase list when it ends. Default true. |
humanPickupAfterSecs | integer (0-600) | no | Seconds of phase playback before the persona takes over. Omit to never auto-pickup (tests the agent's give-up logic). |
realism | object | no | Real-world call conditions on the simulated caller's leg — interruptions, background noise, handset character, line damage. Omit for a polite caller on a clean line. See spec.realism. |
For direction: outbound, omit phases, loopPhases, and humanPickupAfterSecs. Outbound simulations model the callee answering immediately, so the API rejects pre-pickup phases for outbound manifests. direction: outbound is also rejected together with mode: pstn — a PSTN simulation always dials the agent, so the persona is the caller by construction.
Outbound example#
apiVersion: voicerun/v1 kind: Simulation metadata: name: appointment-reminder spec: direction: outbound systemPrompt: | You are Dana Lee. You just answered your phone and are willing to talk briefly. inputData: customerName: Dana Lee appointmentTime: Tuesday at 3pm reason: confirm upcoming appointment numberOfSimulations: 3
PSTN simulations#
mode: pstn swaps the WebSocket for a real outbound phone call. The simulator originates the call through its telephony integration and drives it with the same persona; the agent answers a genuine inbound call, so its own session, recording, and billing behave exactly as they do for a real caller.
apiVersion: voicerun/v1 kind: Simulation metadata: name: pstn-smoke spec: mode: pstn systemPrompt: | You are a customer calling support. Ask one question, then say goodbye. numberOfSimulations: 1
- Each run is a real, billable call.
vr simulatealways confirms before submitting a PSTN run — keepnumberOfSimulationslow. - The dialed number defaults to the one assigned to the agent and environment. Override with
toNumberonly when you need a different one. direction: outboundis rejected withmode: pstn.- The pre-created sessions anchor the simulated caller's side. The agent-side session is created by its telephony ingress, so
vr simulate --waitis skipped for PSTN runs.
spec.realism#
Real-world call conditions applied to the simulated caller's leg. By default a simulated caller is unnaturally polite — the persona engine waits for a clean end of turn, and the simulator drops its audio while the agent is speaking, so it cannot talk over the agent. The line is pristine too. Omit the block and none of that changes.
spec: systemPrompt: | You are an impatient customer with a billing problem. Keep every turn to one short sentence. If the rep starts explaining at length, cut them off. realism: seed: 4242 # pin for a reproducible run; omit for fresh randomness bargeIn: technique: both # nudge | clip | both probability: 0.45 afterAgentSpeechMs: 1200 cooldownSecs: 7 maxPerCall: 6 background: profile: cafe # none | hiss | cafe | street levelDbfs: -34 eventsPerMin: 8 events: - kind: dogBark atSecs: 12 effects: muffle: 0.5 room: 0.4 gainDb: -6 impairments: packetLossPct: 3 dropoutsPerMin: 12 dropoutMs: [250, 800] clipping: 0.2
| Field | Type | Required | Description |
|---|---|---|---|
seed | integer | no | Seed for every random decision in the block. Omit for fresh randomness — the chosen seed is logged, so a run can be replayed by passing it back. |
bargeIn | object | no | Make the caller interrupt the agent mid-sentence. |
background | object | no | A continuous room the caller is standing in. |
effects | object | no | Handset and room character — how the caller sounds. |
impairments | object | no | Packet loss, dead-air dropouts, clipping — how badly the caller is transmitted. |
The chain runs in the order the physical world applies it: the persona's voice, plus the room the same microphone picks up, then the handset, then the network. So effects reach the background bed too — muffling a caller in a cafe muffles the cafe.
realism.bargeIn
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Presence of the block is the opt-in. Set false to keep a configured block on the manifest while turning it off. |
technique | nudge | clip | both | nudge | How the interruption is produced — see below. |
probability | number (0-1) | 0.45 | Chance of cutting in, rolled once per cooldown window while the agent is talking. |
afterAgentSpeechMs | integer (0-60000) | 1200 | The agent must have been talking this long before an interruption is considered. |
delayMs | [min, max] integers (0-60000, ordered) | [150, 1400] | Jitter applied once the caller has decided to interrupt. |
cooldownSecs | number (0-600) | 7 | Minimum gap between two interruptions. |
maxPerCall | integer (0-100) | 6 | Hard cap, so a high probability can't turn the call into a shouting match. |
phrases | string[] | built-in set | Interjections rendered to audio for the clip / both techniques. |
atSecs | number[] (each 0-3600) | [] | Absolute offsets that force an interruption regardless of probability. Deterministic — this is what regression runs should use. |
openGate | boolean | true | Hold the simulator's silence gate open after an interruption, so the persona's own next utterance also rides over the agent. |
holdGateMs | integer (0-60000) | 2500 | How long past the injected clip the gate stays open. |
technique | What happens |
|---|---|
nudge | The persona model is told to cut in now. The interruption is in its own voice and contextually relevant, and needs no TTS pre-render, so it works from the first second of the call. |
clip | A pre-rendered interjection is injected straight onto the wire, bypassing the persona. Guaranteed and exactly reproducible. |
both | Clip first, then the persona keeps going — closest to how a person really interrupts: a filler word, then the actual sentence. |
realism.background
The bed is mixed into every frame the caller sends, including the silence between its own utterances, because the room a real caller is standing in does not go quiet when they stop talking. Every profile except hiss is a real field recording, looped for the length of the call.
| Field | Type | Default | Description |
|---|---|---|---|
profile | none | hiss | cafe | street | none | Which bed to play. hiss is analog line hiss; cafe is multi-talker babble and crockery; street is traffic and city wash. |
levelDbfs | number (-90 to -12) | -40 | Bed level in dBFS RMS; the generator self-calibrates, so the number means the same thing across profiles. -45 is a quiet room you only hear under speech; -28 keeps a naive energy VAD permanently open — which is the point. |
eventsPerMin | number (0-120) | 4 | Rate of spontaneous one-off noises drawn from the profile's pool. 0 disables them. |
events | object[] | [] | Scripted one-off noises at exact offsets — deterministic, for regression runs. |
Each events entry:
| Field | Type | Required | Description |
|---|---|---|---|
kind | enum | yes | One of dogBark, doorSlam, carHorn, siren, keyboard, clink, phoneRing, staticBurst, cough, thump. |
atSecs | number (0-3600) | yes | Seconds from the start of the conversation. |
gain | number (0-10) | no | Linear gain relative to the event's natural level. Default 1. |
realism.effects
How the caller's audio sounds before the network touches it. Applies to their voice and the background bed together, because one microphone picks up both.
| Field | Type | Default | Description |
|---|---|---|---|
muffle | number (0-1) | 0 | Rolls the top end off, down to about 700 Hz at 1. A phone in a pocket, or a hand over the mic. |
brightness | number (-1 to 1) | 0 | Spectral tilt. Negative is darker and chestier; positive is thin and tinny, like a cheap headset. |
room | number (0-1) | 0 | A small reflective room — what makes a speakerphone sound like a speakerphone. |
gainDb | number (-24 to 12) | 0 | Level trim. Quiet callers break VADs. |
realism.impairments
| Field | Type | Default | Description |
|---|---|---|---|
packetLossPct | number (0-50) | 0 | Share of 20 ms frames dropped outright — the frame never reaches the agent, like a lost RTP packet. |
dropoutsPerMin | number (0-600) | 0 | Dead-air bursts: the caller "cuts out" and the wire carries silence, background bed included. |
dropoutMs | [min, max] integers (0-30000, ordered) | [200, 900] | Duration of each dropout. |
clipping | number (0-1) | 0 | Overdrive into hard clipping — a hot handset or an overloaded codec. |
Manifest keys are camelCase, as everywhere else in a spec. The API translates them to snake_case (barge_in, after_agent_speech_ms, level_dbfs, gain_db, packet_loss_pct, dog_bark, …) when it calls the simulator, which is the form you will see in simulator logs.
Every range above is enforced by the API when it accepts the rendered manifest at vr release time, not by vr validate — see Validation.
spec.phases#
Phase entries play before the simulated persona starts speaking — useful for warm-transfer testing where the outbound leg waits through ringing/queue/IVR before someone "answers".
type | Fields | Description |
|---|---|---|
ring | durationSecs (int, 1-600) | North-American ringback tone (440+480 Hz, 2s on / 4s off). |
holdMusic | durationSecs (int, 1-600) | Looping arpeggio that reads as hold music to a VAD. |
message | text (non-empty string) | Pre-rendered automated-IVR speech (Gemini TTS). |
ivrMenu | prompt, options, optional timeoutSecs / maxRepeats / onNoInput / onInvalid | Recursive IVR menu node. See below. |
ivrMenu phase
| Field | Type | Description |
|---|---|---|
prompt | string | The IVR prompt the simulator plays. |
options | map<DTMF digit, PhaseSpec[]> | Single-character keys (0-9, *, #) mapped to the sub-phases that fire when the agent sends that digit. Phases can themselves be ivrMenu entries for multi-level trees. |
timeoutSecs | integer (1-120) | Seconds to wait for a digit after the prompt finishes. Default 8. |
maxRepeats | integer (0-10) | Additional re-prompts when no digit arrives. Default 2. |
onNoInput | PhaseSpec[] | Phases to run after maxRepeats re-prompts produce no input. |
onInvalid | PhaseSpec[] | Phases to run when the agent sends a digit not in options. |
Webhook#
Session-end webhook delivery configuration. The destination URL must be http(s).
apiVersion: voicerun/v1 kind: Webhook metadata: name: my-agent-webhook spec: url: https://example.com/voicerun/session-webhook events: - session.ended signingToken: "{{ Secrets.organization.WEBHOOK_SIGNING_TOKEN }}"
Top-level fields#
| Field | Type | Required | Description |
|---|---|---|---|
url | string (http/https URL) | yes | Destination URL. |
events | string[] | yes | Event triggers this webhook listens for. Only session.ended is supported today; the list is intentionally small so adding a new event requires explicit code review. |
signingToken | string | no | HMAC-SHA256 signing token for outgoing deliveries. Typically supplied via {{ Secrets.organization.NAME }} and resolved at consume time. When absent, the worker sends an unsigned request. |
Evaluator#
Scores or extracts data from a session after it completes. Results are surfaced through vr evaluation list and vr evaluation info.
apiVersion: voicerun/v1 kind: Evaluator metadata: name: resolution-judge spec: title: Resolution Judge evalType: judge targetFormat: transcript systemPrompt: | Score the session 1-5 on whether the agent resolved the caller's request. Respond with a JSON object matching the response schema. responseSchema: type: object properties: score: { type: integer, minimum: 1, maximum: 5 } reasoning: { type: string } required: [score, reasoning] successCriteria: score: { ">=": 4 } apiProvider: google model: gemini-3.5-flash
Common fields#
| Field | Type | Required | Description |
|---|---|---|---|
title | string | yes | Human-readable title (shown in evaluation listings). |
evalType | judge | extraction | deterministic | yes | Whether this evaluator scores a session against criteria (judge), extracts structured data (extraction), or asserts on the derived session view without an LLM (deterministic). |
targetFormat | events | transcript | no | What the evaluator sees. events passes the raw session-event stream; transcript passes a flattened user/agent transcript. (Deterministic always reads the session view; this field is ignored.) |
apiProvider | string | no | LLM provider for the evaluation call. Defaults to openai. Only openai, anthropic, google, vertex, and vertex_maas actually run — see Judge provider and model. Not applicable to deterministic. |
model | string | no | Model id within the provider (e.g. gemini-3.5-flash). Defaults to gpt-5-mini. Must be a model registered for that provider — see Judge provider and model. Not applicable to deterministic. |
precondition | object | no | Optional JSON predicate evaluated against the derived session view. Sessions that don't satisfy it are recorded as status="skipped" with no LLM call. See Preconditions. |
Judge provider and model#
apiProvider and model are not validated when you push or apply the resource — both are plain optional strings on the API request, so any value is accepted and stored. The gate is at evaluation time, when the completion is dispatched. A misconfigured evaluator therefore looks healthy until the first session it runs against, then fails per-evaluation.
Two independent checks run then:
- The provider must be dispatchable. Only five values map to a completion path:
openai,anthropic,google,vertex, andvertex_maas. Anything else throwsUnknown completion apiProvider: <value>. Note thatalibaba,deepseek, andvoicerunare legalapiProvidervalues elsewhere in the platform (they exist for cost attribution) — an evaluator can be saved with one of them and will then throw at evaluation time. Do not use them here. - The model must be registered for that provider. The named
modelhas to exist as a registered model definition for the resolved provider, or the call errors withModel <model> not supported for <PROVIDER> provider.
The defaults are model: gpt-5-mini and apiProvider: openai, and they are applied independently. This is the most common way to trip check 2: setting apiProvider: vertex_maas without also setting model leaves the default in place and fails with Model gpt-5-mini not supported for VERTEX_MAAS provider. Whenever you change apiProvider, set a model that belongs to it.
# Runs: gemini-3.5-flash is registered for the google provider. apiProvider: google model: gemini-3.5-flash
# Fails at evaluation time: apiProvider changed, model left at the default. apiProvider: vertex_maas
Credentials. The declarative Evaluator spec has no field for a provider key — manifest-defined evaluators always run on VoiceRun-managed credentials. Where an evaluator is configured with an organization secret through the API instead, note that it applies to
openai,anthropic, andvertexandvertex_maasalways run on VoiceRun's own GCP credentials, and a supplied secret is routed into the OpenAI-key slot that neither Vertex path reads, so it is effectively ignored.
judge evaluators#
| Field | Type | Required | Description |
|---|---|---|---|
systemPrompt | string | yes | Instructions for the judge model. |
responseSchema | object | yes | JSON schema describing the judge's structured response. |
successCriteria | object | yes | Criteria evaluated against the judge's response — drives the success flag on the resulting Evaluation. |
extraction evaluators#
| Field | Type | Required | Description |
|---|---|---|---|
systemPrompt | string | yes | Instructions describing what to extract. |
responseSchema | object | no | Optional JSON schema constraining the extracted payload. |
deterministic evaluators#
A deterministic evaluator asserts on the derived session view rather than calling an LLM — same input, same result, zero token cost. Use for purely factual checks: did a specific tool get called, did the caller say a specific word, was the duration in range.
| Field | Type | Required | Description |
|---|---|---|---|
assertion | object | yes | JSON predicate (same operator set as successCriteria) evaluated against the session view. Match → success: true; mismatch → success: false with a structured details.failedPath and details.reason. |
apiVersion: voicerun/v1 kind: Evaluator metadata: name: inbound-cancellation-request spec: title: Inbound caller mentioned cancellation evalType: deterministic assertion: direction: "inbound" events: $any: name: "transcript_part" data.role: "user" data.content: { $icontains: "cancel" }
Session view fields the assertion (or a precondition) can read:
| Field | Description |
|---|---|
turn_count | Number of completed turns (count of turn_end events). A mid-turn hangup doesn't count. |
duration_seconds | Seconds between startedAt and endedAt. Returns 0 when the session hasn't ended. |
direction | inbound or outbound |
origin | Where the session came from (e.g. phone, web, simulation, native) |
tags | Session tags as a string array. Matchable by bare primitive (tags: "billing") or by $inc / $ninc. |
environment | [id, name] for the session's organization-scoped environment. Matchable by bare primitive against either value: environment: "production" or environment: "<uuid>" both work. |
events | Raw event list in arrival order — each element { name, data, timestamp }. Use with $any to assert on event names or payloads (transcript content, tool arguments, etc.). |
Operators:
| Operator | Meaning |
|---|---|
$eq / $ne | Equals / not equals (deep equality) |
$gt / $gte / $lt / $lte | Numeric comparison |
$in / $nin | Value is / is not in a literal array |
$inc / $ninc | Input array does / does not contain the target value |
$contains / $icontains | String input contains the substring (case-sensitive / -insensitive). Returns false when either side isn't a string. |
$any | Input array has at least one element matching the sub-predicate. Recurses; the sub-predicate is itself a predicate object. |
Dotted field paths walk nested objects. Mixing operators and field names at the same level is rejected at evaluation time.
Bare primitive vs array input. When a predicate's value is a primitive and the input field is an array, the engine does membership matching (equivalent to $inc). This lets tags: "billing" work without $inc, and lets environment: "production" match against [id, name] regardless of whether you wrote the name or the ID. Scalar-vs-scalar equality is unchanged.
Event-payload checks via $any — ask questions like "did the caller use a phrase" or "did the agent reach a particular closing" without flattening the event stream up front:
apiVersion: voicerun/v1 kind: Evaluator metadata: name: refund-mentioned spec: title: Caller mentioned refund evalType: deterministic assertion: events: $any: name: "transcript_part" data.role: "user" data.content: { $icontains: "refund" }
apiVersion: voicerun/v1 kind: Evaluator metadata: name: agent-closed-with-goodbye spec: title: Agent ended with a goodbye evalType: deterministic assertion: events: $any: name: "transcript_part" data.role: "agent" data.content: { $icontains: "goodbye" }
Preconditions#
Any evaluator type can declare a precondition to gate whether it runs. If the predicate doesn't satisfy, the evaluator is skipped — the row is persisted with status="skipped" and a skipReason naming the failing field, no LLM call is made. This protects you from running expensive "did the agent handle the objection well?" evals against 1-turn hangups while keeping the skip auditable.
apiVersion: voicerun/v1 kind: Evaluator metadata: name: objection-handling spec: title: Objection Handling evalType: judge targetFormat: transcript systemPrompt: … responseSchema: { type: object, properties: { score: { type: integer } } } successCriteria: { score: { $gte: 4 } } precondition: turn_count: { $gte: 3 } duration_seconds: { $gte: 30 }
Filter skipped rows with vr evaluation list --status skipped or the web dashboard's status filter.
Values and Secrets#
Values files#
.voicerun/values.yaml is the base values file used by Helm. Per-environment overlays (e.g. prod.yaml, staging.yaml) live alongside it and are pulled in with --values prod.yaml on vr release, vr render, or vr simulate.
# .voicerun/values.yaml variables: LOG_LEVEL: info region: us-central1-a stt: model: flux-general-en language: en tts: provider: elevenlabs model: eleven_flash_v2_5 voice: rachel recording: enabled: false tracing: enabled: true webhook: url: null # leave null to skip the Webhook resource entirely simulation: numberOfSimulations: 1 evaluator: apiProvider: google model: gemini-3.5-flash
Secret placeholders#
Anywhere a string value lands in the rendered manifest, you can reference an organization secret as:
signingToken: "{{ Secrets.organization.WEBHOOK_SIGNING_TOKEN }}"
Helm leaves the placeholder intact through rendering. The API resolves it against organization secrets at session start, so secrets never round-trip through the release record itself. Create secrets with vr create secret.
Validation#
Both vr validate and vr render run shape-only validation against rendered manifests. The validator checks that:
- Each document has a known
kind(Deployment,Simulation,Webhook,Evaluator). speccontains only the allowed top-level keys for that kind.
That is the whole of the client-side check — vr validate does not look inside a spec. Required fields, enum membership, numeric bounds, and metadata.name uniqueness are all enforced by the API when it accepts the rendered manifest at vr release time. For Simulations that includes numberOfSimulations (1-100), humanPickupAfterSecs (0-600), toNumber (E.164, and only with mode: pstn), the direction: outbound rejections for both phases and mode: pstn, and every realism range.
A manifest with an out-of-range value therefore passes vr validate and fails at vr release, with an error naming the exact key path:
Simulation "impatient-caller": realism.background.levelDbfs must be a number between -90 and -12 (got -5)
Because a top-level spec key is all the CLI checks, an older CLI will also reject a manifest using a newer field: spec.mode needs 1.7.1 or later, and spec.realism needs 1.7.15 or later.
Validator-level checks don't hit the database either, so they don't catch missing organization secrets or unknown providers — those are surfaced at vr release time when the API processes the manifest.
