Scenario grammar reference
Generated by tools/gen_grammar_reference.py from callqa_spec. Do not edit by hand.
Grammar version: schema_version: 1. This page is generated from the models the server uses to validate scenarios, and every example on it is checked with callqa_spec.load_scenario.
- Machine readable:
scenario.schema.json(JSON Schema, also served byGET /api/v1/scenarios/schema). - How to write good scenarios, server-side checks and plan limits: Scenario guide.
- Compatibility rules: Versioning.
Contents
- Scenario
- Target
- Persona
- Limits
- Variant
- Steps
- Assertions
- Short form
- Extension fields
x_* - Complete examples
General rules
- YAML and JSON are equivalent. The API takes JSON, or YAML on
POST /scenarios/import. - Unknown fields are rejected, at every level, unless their name starts with
x_. - Strings are trimmed (leading and trailing spaces removed).
typeselects the kind of a step or an assertion.
Scenario
The document you write. Required: name, target, persona, goal.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
schema_version |
integer | no | 1 |
Older documents are upgraded on read; a version newer than the server is refused. | |
name |
string | yes | |||
target |
Target | yes | |||
persona |
Persona | yes | |||
goal |
string | yes | |||
test_data |
map (free keys) | no | {} |
||
steps |
list of Step | no | [] |
Optional IVR navigation hints. | |
assertions |
list of Assertion | no | [] |
||
limits |
Limits | no | defaults of the object | ||
variants |
list of Variant | no | [] |
||
tags |
list of string | no | [] |
||
schedule |
string or null | no | null | Cron expression, UTC unless timezone. The API also accepts every day at 09:00 UTC-style text. See the scenario guide. |
|
timezone |
string or null | no | null | IANA time zone of schedule (e.g. Europe/Paris). Default UTC. Local wall-clock time: 8:00 stays 8:00 across daylight saving changes. Checked against the IANA time zone database. A time zone may also end the schedule text; if both are given they must be the same. |
|
call_window |
string or null | no | null | 'HH:MM-HH:MM Area/City'. Only scheduled runs wait for the window; checked by the API. | |
environments |
map (free keys) or null | no | null | Per environment plan, keyed by environment slug (staging): whether the scenario runs there, with its own schedule, timezone and call_window. The top-level fields stay the plan of the default environment. |
Target
The phone number under test and what answers it.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
phone |
string | yes | Number to call, E.164 (e.g. +15550123456). E.164 after removing spaces, dashes, dots and parentheses (+1 (415) 555-0100 is accepted and stored as +14155550100). |
||
kind |
enum: ai_agent, ivr, iva, human, unknown |
no | unknown |
||
platform |
string or null | no | null | Free text: vapi, retell, bland, livekit, twilio, genesys... | |
language |
string | no | en-US |
BCP-47 language the call is held in. |
kind values: ai_agent, ivr, iva, human, unknown.
Persona
Who the caller is and how they behave.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
description |
string | yes | Who the caller is and how they behave. | ||
voice |
string or null | no | null | TTS voice id; provider default if empty. | |
accent |
string or null | no | null | ||
speaking_rate |
number | no | 1.0 |
>= 0.5, <= 2.0 | |
patience |
enum: low, normal, high |
no | normal |
||
interrupts |
boolean | no | false | If true the caller barges in while the agent speaks. |
Limits
Hard limits for each call of this scenario.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
max_duration_s |
integer | no | 300 |
> 0, <= 1800 | |
silence_timeout_s |
integer | no | 20 |
> 0, <= 300 | |
ring_timeout_s |
integer | no | 45 |
> 0, <= 120 |
Variant
A test case derived from the scenario (#1..#N). Every field except title overrides the scenario for this case; assertions are appended to the scenario's assertions, test_data is merged over it.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
title |
string | yes | |||
source |
enum: auto, manual |
no | manual |
||
persona |
Persona or null | no | null | ||
goal |
string or null | no | null | ||
instructions |
string or null | no | null | Extra guidance for this run. | |
test_data |
map (free keys) or null | no | null | ||
assertions |
list of Assertion or null | no | null | Appended to the base assertions. | |
enabled |
boolean | no | true |
Steps
Optional navigation hints, followed in order. Useful for IVR menus. An AI agent target usually needs none: the caller converses from persona and goal.
steps:
- type: expect
text: "main menu options"
timeout_s: 20
- type: dtmf
digits: "2"
- type: wait
seconds: 1.5
- type: say
text: "account balance"
say (SayStep)
The caller says this text.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
type |
"say" |
yes | |||
text |
string | yes |
dtmf (DtmfStep)
The caller presses these keys. w is a 0.5 s pause, W a 1 s pause.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
type |
"dtmf" |
yes | |||
digits |
string | yes | Only 0-9 * # w W. |
expect (ExpectStep)
The caller waits until the target says something with this meaning.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
type |
"expect" |
yes | |||
text |
string | yes | What the target should say (semantic match, not exact). | ||
timeout_s |
number | no | 15.0 |
> 0, <= 300 |
wait (WaitStep)
The caller stays silent for this many seconds.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
type |
"wait" |
yes | |||
seconds |
number | yes | > 0, <= 600 |
Assertions
Pass/fail checks run on the call trace after the call. Every assertion has these common fields:
| Field | Type | Default | Description |
|---|---|---|---|
id |
string or null | null | Stable id, reused across runs for trends. |
severity |
enum: fail, warn |
fail |
severity: fail makes the call fail when the check fails. severity: warn only marks it warn. Semantic assertions are judged by an LLM judge that quotes the transcript as evidence; deterministic ones are computed from the trace timings.
must_include (MustInclude)
Semantic: judged on meaning by the LLM judge, not by exact words.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
text |
string | yes | Behaviour the agent must show, judged semantically. |
assertions:
- type: must_include
text: "confirm date and time"
must_not (MustNot)
Semantic: the agent must never show this behaviour.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
text |
string | yes |
assertions:
- type: must_not
text: "hallucinate availability"
goal_reached (GoalReached)
Semantic: did the caller reach the scenario (or variant) goal?
No other field.
assertions:
- type: goal_reached
llm_check (LlmCheck)
Semantic: a yes/no question the judge answers from the transcript.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
question |
string | yes | Yes/no question answered by the judge from the transcript. |
assertions:
- type: llm_check
question: "Did the agent offer a callback?"
max_latency_ms (MaxLatency)
Deterministic. Latency of one turn = start of target speech minus end of caller speech. percentile picks p50, p95 or the max over the call.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
value |
integer | yes | > 0 | Per turn, end of caller speech to start of target speech. | |
percentile |
enum: p50, p95, max |
no | p95 |
assertions:
- type: max_latency_ms
value: 1500
percentile: p95
max_duration_s (MaxDuration)
Deterministic. Whole call, from dialing to the last event.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
value |
integer | yes | > 0 |
assertions:
- type: max_duration_s
value: 180
severity: warn
max_interruptions (MaxInterruptions)
Deterministic. Counts the times the target talked over the caller.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
value |
integer | yes | >= 0 | Times the target talked over the caller. |
assertions:
- type: max_interruptions
value: 1
transcript_regex (TranscriptRegex)
Deterministic. Python regular expression, case insensitive, searched in each turn of speaker. expect_match: false asserts that nothing matches.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
pattern |
string | yes | Must compile as a Python regular expression. | ||
speaker |
enum: target, caller, any |
no | target |
||
expect_match |
boolean | no | true |
assertions:
- type: transcript_regex
pattern: "booking (reference|number)"
speaker: target
Short form
The format shown on callqa.dev is accepted as is. On import it is folded into the typed grammar:
| Short form | Becomes |
|---|---|
phone: "+1..." |
target.phone |
persona: "text" |
persona.description |
must_include: [..] |
one must_include assertion per item |
must_not: [..] |
one must_not assertion per item |
max_latency_ms: N |
a max_latency_ms assertion (p95) |
This short document:
name: "Book appointment - edge case"
phone: "+14155550100"
persona: "Impatient customer, speaks fast, has accent"
goal: "Book a haircut for tomorrow 2pm"
must_include:
- "confirm date and time"
must_not:
- "hallucinate availability"
max_latency_ms: 2000
is stored as (with every default filled in):
schema_version: 1
name: Book appointment - edge case
target:
phone: '+14155550100'
kind: unknown
language: en-US
persona:
description: Impatient customer, speaks fast, has accent
speaking_rate: 1.0
patience: normal
interrupts: false
goal: Book a haircut for tomorrow 2pm
test_data: {}
steps: []
assertions:
- severity: fail
type: must_include
text: confirm date and time
- severity: fail
type: must_not
text: hallucinate availability
- severity: fail
type: max_latency_ms
value: 2000
percentile: p95
limits:
max_duration_s: 300
silence_timeout_s: 20
ring_timeout_s: 45
variants: []
tags: []
Extension fields x_*
Any key that starts with x_ is accepted at any level and kept verbatim (stored, exported, returned by the API). CallQA never interprets them. Use them for your own metadata (owner, ticket, region) or to try a field before it joins the grammar.
name: "Checkout flow"
target:
phone: "+14155550100"
x_region: "us-west"
persona:
description: "Returning customer"
goal: "Buy the blue plan"
x_owner: "voice-team"
x_jira: "VOICE-142"
Complete examples
Real-world scenarios from examples/scenarios/. Each one is validated by the test suite.
book-appointment.yaml: Book appointment - edge case (5 assertions, 4 variants)cancel-subscription.yaml: Cancel subscription (5 assertions, 2 variants)ivr-dtmf-navigation.yaml: IVR - check account balance (3 assertions, 1 variant)payment-test-card.yaml: Pay an invoice by card (6 assertions, 1 variant)reschedule-minimal.yaml: Reschedule a dentist appointment (1 assertion, 0 variants)spanish-language.yaml: Spanish line - order status (4 assertions, 1 variant)transfer-to-human.yaml: Escalate to a human agent (5 assertions, 1 variant)