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#

FieldTypeDescription
modecoderunner | relayRuntime 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.
dedicatedbooleanWhen 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.
regionstringCluster region (e.g. us-central1-a).
variablesmap<string, string>Values injected into context.variables at session start. Organization-secret placeholders are resolved when the session starts.
relayobjectRelay endpoint config. Only used when mode: relay.
sttobjectSpeech-to-text config.
turnTakingobjectTurn-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.
ttsobjectText-to-speech config.
recordingobjectCall recording config.
redactionobjectPII redaction applied to traces and session events.
tracingobjectDistributed 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.

FieldTypeDescription
urlstringPrimary relay WebSocket URL (e.g. wss://relay.example.com/ws/agent).
failover.urlstringBackup relay WebSocket URL.

spec.stt#

FieldTypeDescription
modelstringSTT model identifier (e.g. flux-general-en, nova-3).
languagestringBCP-47 language code (e.g. en).
promptstringPrompt biasing for providers that support it.
filterstringProvider-specific filter string.
endpointingnumberProvider-side endpointing silence threshold (ms).
audioInputDelaynumberAudio input delay (ms) before transcription starts.
noiseReductionTypestringProvider noise-reduction profile.
eot.thresholdnumberDeepgram-style end-of-turn confidence threshold.
eot.timeoutMsnumberHard cap on EoT detection (ms).
eot.eagerThresholdnumberEager-EoT pre-confirmation threshold.
vad.modeserver_vad | semantic_vadOpenAI VAD mode. Honored only by gpt-4o-transcribe, gpt-4o-mini-transcribe, and gpt-4o-mini-transcribe-2025-12-15.
vad.eagernessauto | low | medium | highOpenAI semantic-VAD eagerness.
eot.thresholdByLanguagestringvoicerun-asr-realtime-v1 only — per-language eot.threshold overrides, CSV. Not consumed by deployed voice agents yet.
vadStopMsnumbervoicerun-asr-realtime-v1 only — silence floor before the semantic end-of-turn consult (ms). Not consumed by deployed voice agents yet.
allowedLanguagesstringvoicerun-asr-realtime-v1 only — language-ID guardrail set, CSV; "" disables it. Not consumed by deployed voice agents yet.
failoverobjectSame shape as stt (minus failover). Used on provider errors.

spec.turnTaking#

FieldTypeDescription
modeprovider | silero | smart_turnHow 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.
externalEndpointingnumberSilence-stop threshold (ms) for silero / smart_turn.
smartTurnVadStopSecsnumbersmart_turn only — VAD silence-stop window before the ML gates. Default 0.4.
smartTurnStopSecsnumbersmart_turn only — ML's per-window timeout. Default 3.0.
smartTurnTimeoutnumbersmart_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#

FieldTypeDescription
providerstringTTS provider (e.g. cartesia, elevenlabs).
modelstringProvider-specific model id (e.g. sonic-2).
voicestringVoice 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).
languagestringBCP-47 language code.
speednumberSpeech rate (provider-specific scale).
failoverobjectSame 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#

FieldTypeDescription
enabledbooleanRecord the call audio. Default false.
locationstringStorage URI override (e.g. gs://my-bucket/recordings/). Leave unset to use the platform default.

spec.redaction#

FieldTypeDescription
enabledbooleanApply PII redaction to traces and session events. Default false.

spec.tracing#

FieldTypeDescription
enabledbooleanEmit 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#

FieldTypeRequiredDescription
directioninbound | outboundnoWhich 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.
modedirect | pstnnoHow 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.
toNumberstring (E.164)noPSTN 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.
systemPromptstringyesPrompt driving the simulated user's behavior.
inputDataobjectnoTask 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.
numberOfSimulationsinteger (1-100)noNumber of simulated sessions spawned per vr simulate invocation. Default 1.
providergemini_live | openai_realtimenoPersona engine vendor. Defaults to gemini_live.
modelstringnoProvider-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.
voicestringnoProvider-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.
languagestringnoBCP-47 language tag for the persona, such as en-US or es-MX. Falls back to the simulator default when omitted.
varietySeedinteger (0-9007199254740991)noSeed for the simulator's delivery variation (greeting, phrasing, mood, and verbosity). Omit for fresh randomness; set it to reproduce a specific simulated caller.
phasesPhaseSpec[]noPre-pickup phase script — see below.
loopPhasesbooleannoWhen phases is non-empty, restart the phase list when it ends. Default true.
humanPickupAfterSecsinteger (0-600)noSeconds of phase playback before the persona takes over. Omit to never auto-pickup (tests the agent's give-up logic).
realismobjectnoReal-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 simulate always confirms before submitting a PSTN run — keep numberOfSimulations low.
  • The dialed number defaults to the one assigned to the agent and environment. Override with toNumber only when you need a different one.
  • direction: outbound is rejected with mode: pstn.
  • The pre-created sessions anchor the simulated caller's side. The agent-side session is created by its telephony ingress, so vr simulate --wait is 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
FieldTypeRequiredDescription
seedintegernoSeed 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.
bargeInobjectnoMake the caller interrupt the agent mid-sentence.
backgroundobjectnoA continuous room the caller is standing in.
effectsobjectnoHandset and room character — how the caller sounds.
impairmentsobjectnoPacket 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

FieldTypeDefaultDescription
enabledbooleantruePresence of the block is the opt-in. Set false to keep a configured block on the manifest while turning it off.
techniquenudge | clip | bothnudgeHow the interruption is produced — see below.
probabilitynumber (0-1)0.45Chance of cutting in, rolled once per cooldown window while the agent is talking.
afterAgentSpeechMsinteger (0-60000)1200The 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.
cooldownSecsnumber (0-600)7Minimum gap between two interruptions.
maxPerCallinteger (0-100)6Hard cap, so a high probability can't turn the call into a shouting match.
phrasesstring[]built-in setInterjections rendered to audio for the clip / both techniques.
atSecsnumber[] (each 0-3600)[]Absolute offsets that force an interruption regardless of probability. Deterministic — this is what regression runs should use.
openGatebooleantrueHold the simulator's silence gate open after an interruption, so the persona's own next utterance also rides over the agent.
holdGateMsinteger (0-60000)2500How long past the injected clip the gate stays open.
techniqueWhat happens
nudgeThe 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.
clipA pre-rendered interjection is injected straight onto the wire, bypassing the persona. Guaranteed and exactly reproducible.
bothClip 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.

FieldTypeDefaultDescription
profilenone | hiss | cafe | streetnoneWhich bed to play. hiss is analog line hiss; cafe is multi-talker babble and crockery; street is traffic and city wash.
levelDbfsnumber (-90 to -12)-40Bed 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.
eventsPerMinnumber (0-120)4Rate of spontaneous one-off noises drawn from the profile's pool. 0 disables them.
eventsobject[][]Scripted one-off noises at exact offsets — deterministic, for regression runs.

Each events entry:

FieldTypeRequiredDescription
kindenumyesOne of dogBark, doorSlam, carHorn, siren, keyboard, clink, phoneRing, staticBurst, cough, thump.
atSecsnumber (0-3600)yesSeconds from the start of the conversation.
gainnumber (0-10)noLinear 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.

FieldTypeDefaultDescription
mufflenumber (0-1)0Rolls the top end off, down to about 700 Hz at 1. A phone in a pocket, or a hand over the mic.
brightnessnumber (-1 to 1)0Spectral tilt. Negative is darker and chestier; positive is thin and tinny, like a cheap headset.
roomnumber (0-1)0A small reflective room — what makes a speakerphone sound like a speakerphone.
gainDbnumber (-24 to 12)0Level trim. Quiet callers break VADs.

realism.impairments

FieldTypeDefaultDescription
packetLossPctnumber (0-50)0Share of 20 ms frames dropped outright — the frame never reaches the agent, like a lost RTP packet.
dropoutsPerMinnumber (0-600)0Dead-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.
clippingnumber (0-1)0Overdrive 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".

typeFieldsDescription
ringdurationSecs (int, 1-600)North-American ringback tone (440+480 Hz, 2s on / 4s off).
holdMusicdurationSecs (int, 1-600)Looping arpeggio that reads as hold music to a VAD.
messagetext (non-empty string)Pre-rendered automated-IVR speech (Gemini TTS).
ivrMenuprompt, options, optional timeoutSecs / maxRepeats / onNoInput / onInvalidRecursive IVR menu node. See below.

ivrMenu phase

FieldTypeDescription
promptstringThe IVR prompt the simulator plays.
optionsmap<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.
timeoutSecsinteger (1-120)Seconds to wait for a digit after the prompt finishes. Default 8.
maxRepeatsinteger (0-10)Additional re-prompts when no digit arrives. Default 2.
onNoInputPhaseSpec[]Phases to run after maxRepeats re-prompts produce no input.
onInvalidPhaseSpec[]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#

FieldTypeRequiredDescription
urlstring (http/https URL)yesDestination URL.
eventsstring[]yesEvent 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.
signingTokenstringnoHMAC-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#

FieldTypeRequiredDescription
titlestringyesHuman-readable title (shown in evaluation listings).
evalTypejudge | extraction | deterministicyesWhether this evaluator scores a session against criteria (judge), extracts structured data (extraction), or asserts on the derived session view without an LLM (deterministic).
targetFormatevents | transcriptnoWhat 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.)
apiProviderstringnoLLM 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.
modelstringnoModel 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.
preconditionobjectnoOptional 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:

  1. The provider must be dispatchable. Only five values map to a completion path: openai, anthropic, google, vertex, and vertex_maas. Anything else throws Unknown completion apiProvider: <value>. Note that alibaba, deepseek, and voicerun are legal apiProvider values 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.
  2. The model must be registered for that provider. The named model has to exist as a registered model definition for the resolved provider, or the call errors with Model <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, and google only: vertex and vertex_maas always 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#

FieldTypeRequiredDescription
systemPromptstringyesInstructions for the judge model.
responseSchemaobjectyesJSON schema describing the judge's structured response.
successCriteriaobjectyesCriteria evaluated against the judge's response — drives the success flag on the resulting Evaluation.

extraction evaluators#

FieldTypeRequiredDescription
systemPromptstringyesInstructions describing what to extract.
responseSchemaobjectnoOptional 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.

FieldTypeRequiredDescription
assertionobjectyesJSON 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:

FieldDescription
turn_countNumber of completed turns (count of turn_end events). A mid-turn hangup doesn't count.
duration_secondsSeconds between startedAt and endedAt. Returns 0 when the session hasn't ended.
directioninbound or outbound
originWhere the session came from (e.g. phone, web, simulation, native)
tagsSession 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.
eventsRaw 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:

OperatorMeaning
$eq / $neEquals / not equals (deep equality)
$gt / $gte / $lt / $lteNumeric comparison
$in / $ninValue is / is not in a literal array
$inc / $nincInput array does / does not contain the target value
$contains / $icontainsString input contains the substring (case-sensitive / -insensitive). Returns false when either side isn't a string.
$anyInput 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).
  • spec contains 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.

clitemplatesdeploymentsimulationwebhookevaluatorhelm