veris.yaml
The .veris/veris.yaml file is the main configuration file for your Veris sandbox. It declares how to run your agent, which mock services it calls, and how the actor reaches it.
Looking for complete working configs? See the cookbook for full agent repos.
Anatomy
version: "1.0"
billing-assistant-env: # Target — its name is the backend environment name
agent: # How to start your agent
name: Billing Assistant
entry_point: python -m app.main
port: 8008
services: # Which mock services your agent calls
- name: calendar
dns_aliases:
- www.googleapis.com
actor: # How the actor reaches your agent
channels:
- type: http
url: http://localhost:8008The three sections you’ll write most of:
agent— how your agent runs inside the container.services— the mock services your agent calls.actor— how the actor reaches your agent (HTTP, WebSocket, email, function call, or CLI). The actor can be a simulated user, an incoming webhook, a scheduled job, or another agent.
These are nested under a target environment key. The target’s name is the backend environment this config maps to. Reach for multiple targets when a monorepo defines more than one agent, or when you want to test a multi-agent system both together and independently. See Multiple Targets below or the CLI guide.
Full schema
version: "1.0" # Optional version
my-agent-env: # Target name (= backend env name)
dockerfile: .veris/Dockerfile.sandbox # Optional per-target override
agent: # How your agent runs
name: My Agent # Display name (optional)
code_path: /agent # Code location (default: /app)
entry_point: python -m app.main # Startup command
port: 8008 # Listen port (default: 8008)
environment: # Runtime env vars
LLM_PROVIDER: ${LLM_PROVIDER}
# DATABASE_URL is auto-injected when `postgres` is in services —
# you don't need to write it yourself. See reference/services/postgres.
services: # Which mock services your agent calls
- name: calendar # Service identifier
dns_aliases: # Domains routed to mock
- www.googleapis.com
- calendar.google.com
config: # Service-specific env vars
SOME_KEY: some_value
port: 8003 # Optional port override
description: "Custom desc" # For generic services
actor: # How the actor reaches your agent
channels: # Communication channels
- type: http # http | ws | email | function | cli
url: http://localhost:8008
method: POST
headers:
Content-Type: application/json
request:
message_field: message
session_field: session_id
static_fields:
prompt_type: default
response:
type: json # json | sse
message_field: response
session_field: session_id
config: # Actor behavior config
MAX_TURNS: "10"Agent
The agent section tells the sandbox how to run your agent inside the container — where the code lives, how to start it, and what environment variables it needs.
agent:
name: Billing Assistant
entry_point: python -m app.main
port: 8008
environment:
LLM_PROVIDER: ${LLM_PROVIDER}Fields
| Field | Description |
|---|---|
name | Display name used in the console. |
code_path | Directory your agent code lives in, relative to the container root. Defaults to /app. |
entry_point | Command to start your agent. Required unless you’re using a function or cli channel. |
port | Port your agent listens on. Defaults to 8008. |
public_endpoint | Set true when an external vendor cloud must call back into your agent over HTTPS (e.g. Vapi/Retell tool webhooks, AssemblyAI BYO-LLM). The platform exposes agent.port through the shared webhook gateway and injects the public base URL into your agent as PUBLIC_BASE_URL — no per-sim tunnel needed. Defaults to false. |
environment | Environment variables passed to the agent process (key/value map). |
Common entry points
| Stack | Example entry_point |
|---|---|
| Python (module) | python -m app.main |
| Python (uvicorn) | uvicorn app.main:app --host 0.0.0.0 --port 8008 |
| Python (shorthand) | app.main:app — auto-expanded to the uvicorn form above, using agent.port |
| Node.js | node dist/server.js |
| Node.js (npm) | npm start |
Services
The services array declares the mock services your agent calls. Each entry names a mock from the service catalog and lists the domains it should intercept.
services:
- name: salesforce
dns_aliases:
- myorg.my.salesforce.com
- login.salesforce.comAt container startup, the listed domains are routed to the local mock — your agent calls myorg.my.salesforce.com the same way it does in production, and the mock responds instead.
Fields
| Field | Description |
|---|---|
name | Service identifier from the catalog (e.g. calendar, salesforce, stripe). Required. |
dns_aliases | Production domains your agent calls. The sandbox routes them to the local mock at startup. |
config | Service-specific environment variables passed to the mock (e.g. STRIPE_TEST_MODE). See each service’s entry in the catalog for accepted keys. |
port | Override the mock’s listen port. Rarely needed. |
description | Free-text description used by the generic mock so the LLM knows what API it’s standing in for. |
Actor
The actor section configures how the actor reaches your agent. At minimum, you declare a transport in channels. Add init if your agent needs a setup step before the first turn, and config to tune actor behavior like turn limits.
actor:
channels:
- type: http
url: http://localhost:8008
config:
MAX_TURNS: "10"Init (pre-connection setup)
Use init when your agent requires a setup step before messaging — like creating a conversation, obtaining a session token, or authenticating.
actor:
init:
type: http
method: POST
url: http://localhost:8080/api/v1/conversations
body:
banker_id: "B001"
branch_id: "BR01"
response:
message_field: content # Capture initial message from responseEach top-level field in init’s response becomes a {field_name} variable you can use in your channel’s URL, headers, or static_fields. For example, if init returns {"conversation_id": "abc123"}, a channel URL can include /conversations/{conversation_id}/messages.
Channel types
Pick one per channel entry; most agents declare exactly one.
| Type | Use when |
|---|---|
http | Agent exposes a chat REST API (JSON or SSE streaming responses). |
ws | Agent communicates over WebSocket. |
email | Agent communicates asynchronously via email. |
function | Python agent invoked as a function in production (LangGraph Platform deploy, Lambda-style handler, importable library). Python-only today. |
cli | Agent’s production entry point is a command-line program (including Python CLIs like python -m app.main). |
voice (beta) | Agent answers a phone number; the persona places an outbound call over Twilio. |
voice_ws (beta) | Agent exposes its own realtime voice WebSocket; the persona connects to it directly, no phone network. |
browser-use (beta) | Agent has a web UI users interact with via a browser. |
HTTP
actor:
channels:
- type: http
url: http://localhost:8008
method: POST
headers:
Content-Type: application/json
Authorization: Bearer test-token
request:
message_field: message # JSON field for user message
session_field: session_id # Session tracking field
static_fields: # Extra fields per request
prompt_type: default
response:
type: json # json or sse
message_field: response # Field containing agent reply
session_field: session_idmessage_field and session_field target top-level JSON keys. If your agent speaks A2A or another nested JSON-RPC protocol, see the A2A adapter pattern under Google ADK.
Server-sent events (response.type: sse) — for agents that stream responses via SSE:
actor:
channels:
- type: http
url: http://localhost:8008/chat
method: POST
request:
message_field: message
response:
type: sse
chunk_event: chunk # SSE event name for chunks
chunk_field: chunk # Field in chunk event data
done_event: end_turn # SSE event signaling completionFor mixed SSE streams where only some frames contain user-visible text, constrain which chunks get shown to the actor:
actor:
channels:
- type: http
url: http://localhost:8008/chat
method: POST
request:
message_field: message
response:
type: sse
chunk_event: message
chunk_field: delta
chunk_filter_field: type
chunk_filter_equals: delta
done_data: "[DONE]"In this example, Veris only appends SSE frames where data.type == "delta" and stops when it sees a raw data: [DONE] sentinel. Use chunk_event: message when the agent sends unnamed/default SSE frames — common in streams that put all frame types on the default SSE event and distinguish them with a JSON type field.
WebSocket
actor:
channels:
- type: ws
url: ws://localhost:8008/ws
request:
message_field: message
session_field: session_id
response:
message_field: responseactor:
channels:
- type: email
email_address: agent@ea.veris.aiFunction
For Python agents invoked as a function rather than as a server or CLI — LangGraph Platform deploys, Lambda-style handlers, importable library agents, orchestrator-spawned sub-agents. Veris imports your existing per-turn function and calls it once per actor turn; what it returns goes back as the agent’s reply.
The function channel is Python-only today. Agents in other languages should use http, ws, cli, or email.
actor:
channels:
- type: function
callable: app.agent:run_turncallable is the dotted import path to your agent’s existing per-turn function (module:function). Whatever your agent already does for a turn (LLM calls, tool routing, state updates) stays in that function:
def run_turn(message: str) -> str:
return f"You said: {message}"Any importable module under agent.code_path works. State you keep in the module (globals, loaded models, open connections) persists across turns within a simulation — useful for agents with heavy startup (model loading, connection pools).
Function vs CLI for Python agents. Both channels work for Python; pick by production shape. If your agent runs as a long-lived process and callers import-and-call it (LangGraph, warm Lambda, a library), use function — the process stays alive across turns and in-memory state persists. If users invoke it fresh per run (python -m app.main ...), use cli — each turn spawns a new process, matching production.
Structured input/output — if your function already takes or returns a Pydantic BaseModel or dataclass, Veris picks up the type hint and auto-(de)serializes. The actor reads the JSON schema and composes schema-valid payloads from natural-language prompts.
from pydantic import BaseModel
class RefundRequest(BaseModel):
order_id: str
reason: str
class RefundResult(BaseModel):
refunded: bool
amount_cents: int
async def process_refund(req: RefundRequest) -> RefundResult:
"""Issue a refund for the given order."""
...Omit agent.entry_point and agent.port when using a function channel — the container skips agent startup, and your callable is the agent.
CLI
For agents whose production entry point is a CLI (cron-driven binaries, shell-invoked programs, single-shot commands). Each actor turn spawns a fresh subprocess, passes the message as an argument or via stdin, and reads stdout as the reply.
actor:
channels:
- type: cli
command: ["openclaw", "agent", "--local", "--agent", "brandpulse",
"--deliver", "--reply-channel", "slack", "--reply-to", "C_SANDBOX"]
message_arg: "--message"
timeout_seconds: 600Pick one of message_arg, message_stdin, or message_positional to control where the actor’s message lands (or omit all three if the command doesn’t take the message at all). timeout_seconds defaults to 600.
Fresh subprocess per turn, so there’s no persistent state on the Veris side. If the CLI supports sessions (e.g. openclaw agent --session-id ...), wire it in via your own arg list.
Omit agent.entry_point and agent.port when using a CLI channel — the container skips agent startup; each actor turn invokes the CLI directly.
Voice
For agents that handle phone/voice interactions. The Veris persona places an outbound call to target_number — your agent is whatever answers the phone.
actor:
channels:
- type: voice
target_number: "+14155550100" # Your agent's phone number
language: en-US # Conversation language. Only en-US is exercised today.
wait_for_callee_first: true # Veris waits for your agent to say "hello" before speaking.
silence_timeout_s: 12 # Max seconds Veris will wait for your agent to speak
max_call_duration_s: 600 # Hard cap on call length, in seconds.target_number is the only required field; the rest have defaults shown above.
Audio effects — configure via the run request’s voice_overrides.audio_effects field (not in veris.yaml). Audio degradation is applied to the persona’s outbound audio uniformly across all simulations in the run (no per-scenario variation). Supported effects:
noise.type(enum:cafe/street/traffic/crowd) — ambient noise profilenoise.level(enum:quiet/medium/loud, defaultmedium) — noise intensityloudness_db(number: −30…10) — caller speech level in dBpacket_loss(number: 0…0.5) — fraction of packets dropped (0.1 = 10%)
Absent or null effects means clean audio. Effects are applied by the actor’s effect chain (Gain → AmbientMixer → PacketLoss).
Voice channels are in beta. If you’re working on phone or voice agents, reach out about enterprise support.
Voice (WebSocket)
For agents that expose their own realtime voice WebSocket. The persona connects to it directly, voice-to-voice, without going through the phone network — use this for an agent already running behind a realtime voice API (OpenAI Realtime, Gemini Live, ElevenLabs, Deepgram) or a Vapi/Retell-style bridge.
actor:
channels:
- type: voice_ws
url: wss://voice.example.com/agent # your realtime WS endpoint
protocol: json # binary (default) | json
connection:
auth_header: "Bearer ${env:AGENT_WS_TOKEN}" # secret ref — never a raw token
headers:
X-Org-Id: ${env:ORG_ID}
init:
message: { type: session.start } # first control frame, sent on connect
await_ready:
match: { type: session.ready } # hold audio until this frame arrives
timeout_s: 10
audio:
encoding: mulaw # pcm16 (default) | mulaw
sample_rate_hz: 8000 # 8000 | 16000 | 24000 (default 24000)
wait_for_callee_first: trueConnecting: url vs setup. Provide exactly one. Give a url to dial a fixed endpoint, or a setup block to have Veris mint a per-call URL first — the Vapi/Retell pattern, where each call gets a freshly minted, single-use WebSocket URL:
actor:
channels:
- type: voice_ws
setup:
url: https://api.example.com/call # REST call that returns a WS URL
method: POST # POST (default) | GET
headers:
Authorization: "Bearer ${env:API_KEY}"
body: { assistant_id: asst_123 }
ws_url_path: data.websocket_url # dotted path to the ws:// URL in the response
timeout_s: 15Veris makes the setup request, reads a ws:///wss:// string out of the response JSON at ws_url_path, and connects to it. Declaring both url and setup — or neither — is a validation error.
Fields
| Field | Default | Description |
|---|---|---|
url | — | Your realtime voice WebSocket (ws:// or wss://). Provide this or setup, not both. |
protocol | binary | Wire framing. binary sends raw audio bytes per WS message and treats the WS close as the hangup (Gemini Live, ElevenLabs, AssemblyAI, Cartesia). json wraps each frame as {"type":"audio","audio":"<base64>"} and ends with {"type":"end"} (OpenAI Realtime, Twilio, Deepgram). |
connection | — | WS-upgrade auth headers. See below. |
setup | — | REST call that mints a per-call WS URL. Mutually exclusive with url. See below. |
init | — | Session-init frame + optional await-ready gate. See below. |
audio | pcm16 / 24000 | Wire audio encoding and sample rate. See below. |
wait_for_callee_first | true | Persona waits for the agent to speak first before delivering its opening line. |
connection — sent on the WebSocket upgrade:
| Field | Description |
|---|---|
auth_header | Value of the Authorization header, e.g. "Bearer ${env:AGENT_WS_TOKEN}". Must reference a secret with ${env:NAME}; a raw literal token is rejected at config-validation time. |
headers | Additional headers for the upgrade (key/value map). Use ${env:NAME} for any credential. |
setup — the mint-a-URL call (used instead of url):
| Field | Default | Description |
|---|---|---|
url | — | REST endpoint Veris calls to mint the per-call WS URL. Required. |
method | POST | POST or GET. |
headers | — | Headers for the setup request (use ${env:NAME} for credentials). |
body | — | JSON body for the setup request. |
ws_url_path | — | Dotted path into the response JSON where the ws:///wss:// URL lives (e.g. data.websocket_url). Required. |
timeout_s | 15 | Setup-request timeout, in seconds. |
init — pre-conversation handshake:
| Field | Default | Description |
|---|---|---|
message | — | JSON object sent as the first frame immediately after connect. |
await_ready.match | — | Key/value pairs; Veris holds audio until the first JSON frame containing all of them arrives. Non-matching, non-JSON, and binary frames are discarded while waiting. |
await_ready.timeout_s | 10 | Seconds to wait for the ready frame. Timing out fails the call with ready_timeout. |
audio — wire format (the persona and the recording are always PCM16 24 kHz; conversion happens only at the wire boundary):
| Field | Default | Description |
|---|---|---|
encoding | pcm16 | pcm16 or mulaw (telephony μ-law). |
sample_rate_hz | 24000 | 8000, 16000, or 24000. |
${env:NAME} in a voice_ws channel is a secret reference, not the ${VAR} config-load expansion documented below. These refs are resolved in-pod at call time from the variables you set with veris env vars set --secret, and their values are never written into the stored config or the logs. A raw credential in auth_header, or in the url query string (?token=…), is rejected by a validator with a message telling you to switch to a ${env:NAME} ref.
Voice channels are in beta. If you’re working on phone or voice agents, reach out about enterprise support.
Browser-use
For agents with a web UI that users interact with via a browser:
actor:
channels:
- type: browser-use
url: http://localhost:3000Browser-use channels are in beta. If you’re working on web-UI agents, reach out about enterprise support.
Actor config
Values in actor.config are exported as environment variables for the actor service. Use the canonical uppercase names below.
| Variable | Default | Description |
|---|---|---|
MAX_TURNS | 10 | Maximum actor turns before the simulation is forced to stop |
Environment variables
Variable expansion
Values in agent.environment, services[].config, and actor.config support shell-style ${VAR} expansion. The variable is resolved from the container’s environment at config-load time:
agent:
environment:
LLM_PROVIDER: ${LLM_PROVIDER} # From container env
DB_NAME: myapp_${SIMULATION_ID} # Dynamic per simulationNever put secrets in veris.yaml. Use veris env vars set --secret for API keys and other sensitive values. Variables set with veris env vars set take precedence over values defined here.
Injected into the agent’s runtime
Beyond the values you set in veris.yaml, Veris injects these into your agent’s environment at startup:
| Variable | Description |
|---|---|
SIMULATION_ID | Unique per run (e.g. sim_abc123). Use it for log correlation, DB isolation, or trace tagging. |
PORT | Value of agent.port. Present so frameworks that read $PORT (Express, Flask, etc.) just work. |
SSL_CERT_FILE, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS | Path to the sandbox’s CA bundle. Python (httpx, requests, urllib) and Node (https) automatically trust mock service TLS. Set manually only if your HTTP client reads certs from a non-standard location. |
Multiple Targets
A single veris.yaml can define multiple targets. Each target is a top-level key with its own services, actor, and agent blocks:
version: "1.0"
billing-assistant-env:
services:
- name: postgres
actor:
channels:
- type: http
url: http://localhost:8008/chat
agent:
name: Billing Assistant
entry_point: uv run app
port: 8008
customer-support-env:
dockerfile: .veris/Dockerfile.support # Per-target Dockerfile override
services:
- name: zendesk-support
dns_aliases:
- help.example.com
actor:
channels:
- type: email
email_address: support@ea.veris.ai
agent:
name: Customer Support
entry_point: uv run support
port: 8009Target-level fields
| Field | Default | Description |
|---|---|---|
dockerfile | .veris/Dockerfile.sandbox | Per-target Dockerfile path (relative to project root) |
services | — | Mock services for this target |
actor | — | Actor config for this target |
agent | — | Agent config for this target |
Each target is independent — it has its own services, actor channels, agent entry point, and optionally its own Dockerfile.
See Multiple Targets (CLI) for how to create, select, and manage targets from the command line.