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: flux-general-en language: en failover: model: nova-3 turnTaking: mode: smart_turn externalEndpointing: 300 smartTurnVadStopSecs: 0.4 smartTurnStopSecs: 3.0 smartTurnTimeout: 5.0 tts: provider: cartesia model: sonic-2 voice: lyric 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 Realtime / Qwen3 VAD mode.
vad.eagernessauto | low | medium | highOpenAI semantic-VAD eagerness.
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.

spec.tts#

FieldTypeDescription
providerstringTTS provider (e.g. cartesia, elevenlabs).
modelstringProvider-specific model id (e.g. sonic-2).
voicestringProvider-specific voice id (e.g. lyric).
languagestringBCP-47 language code.
speednumberSpeech rate (provider-specific scale).
failoverobjectSame shape as tts (minus failover). Used on provider errors.

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 the simulation's metadata.name; the API resolves spec from the active release's manifest, so 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 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.
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.
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).

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.

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

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 (e.g. google, openai, anthropic). Not applicable to deterministic.
modelstringnoModel id within the provider (e.g. gemini-3.5-flash). 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 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: cartesia model: sonic-2 voice: lyric 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.
  • Required fields are present (e.g. Webhook.spec.url, Evaluator.spec.systemPrompt for judge, Evaluator.spec.assertion for deterministic).
  • precondition and assertion (when present) are plain objects.
  • Bounded fields are in range (e.g. numberOfSimulations is 1-100).
  • metadata.name is unique per kind within a manifest.

Validator-level checks don't hit the database, 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