Author scenarios
Select authored scenarios explicitly
Scenarios a person wrote (ess-scenario/* documents) join the generated ones only when
--scenarios names them. It takes one file or one directory:
--scenarios names | What is read |
|---|---|
| a file | that file, whatever its extension |
a directory with an ess-inputs.yaml | exactly the files its scenarios: list names, nested or not, of any extension |
| a directory without one | its immediate lowercase .yaml and .yml files; subdirectories are not searched |
| nothing | no authored scenarios, even when the model's ess-inputs.yaml lists some |
More columns: swipe horizontally, or focus the table and use the arrow keys.
An empty selection is refused before anything is written or run. The mixed-layout example keeps the model and the scenarios in one directory, so the same directory serves both options:
$ ess verify conform author --path . --scenarios . --suite-format 5 --out output/authored.json
ess-inputs.yaml refuses duplicate paths, a path listed as both a specification and a scenario,
a path that leaves the directory, and a symlink. A coverage suite (--suite-format 5) records each
scenario's relative path and source text: moving the directory or reordering the list leaves the
suite unchanged, and changing line endings changes it. Its coverage describes the selected
scenarios, not every scenario file below the directory.
ess verify conform run --suite FILE runs the committed suite as it is and selects nothing.
Compile authored scenarios on their own
ess verify conform synthesize --scenarios compiles authored scenarios beside the generated ones.
ess verify conform author compiles only the authored ones, and nothing the specification obliges:
$ ess verify conform author --path examples/billing --scenarios examples/billing-scenarios \
--out target/authored.json
1 authored scenario(s) from 1 file(s), 0 refusal(s), suite ess-conformance/4, written to target/authored.json
$ ess verify conform author --path examples/billing --scenarios examples/billing-scenarios \
--suite-format 5 --out target/authored-coverage.json
1 selected scenario(s), 1 authored source(s), 0 refusal occurrence(s)
Every command, actor, outcome, event, error, view, entity, field, enum variant and lifecycle state
a scenario names is resolved against the model here, so a name the model does not declare is
refused now rather than at the first run that reaches it. The act checks on this page
(ESS-AUTHOR-037 and the others) apply the same way. Use it in a pre-commit or CI step that only
checks scenario files, and run the result like any other suite.
A key written twice in one mapping, such as label twice in a step's input: or $instance
twice inside a contains: field, is refused as ESS-AUTHOR-047, as a model file's repeat is:
YAML keeps only one of the two values, so the other would never be sent or checked. The refusal
names the mapping's path, the key, and the line of the mapping.
Run a chosen subset of a coverage suite
ess verify conform select narrows a coverage suite (--suite-format 5, or the /7 and /9
versions some constructs select) to scenario IDs you list. --ids names a JSON file holding a
sorted array of distinct IDs; [] selects none, explicitly:
$ ess verify conform synthesize --path examples/billing --suite-format 5 --out target/coverage-suite.json
32 selected scenario(s), 0 authored source(s), 0 refusal occurrence(s)
$ cat create-only.json
["billing.invoice.CreateInvoice/outcome/accepted","billing.invoice.CreateInvoice/outcome/rejected"]
$ ess verify conform select --suite target/coverage-suite.json --ids create-only.json \
--out target/create-only.json
$ ess verify conform run --target billing --suite-input target/create-only.json --report-format 2
billing v3 against billing-reference 0.51.0 — passed
passed billing.invoice.CreateInvoice/outcome/accepted
passed billing.invoice.CreateInvoice/outcome/rejected
2 scenarios: 2 passed, 0 failed, 0 error, 0 unsupported
The output is an ess-conformance-input/1 carrier that keeps the full parent suite, so a report on
the subset still shows what was left out. Narrow it again with --suite-input in place of
--suite. select refuses, writing nothing:
| Input | Refusal |
|---|---|
| IDs out of order, or repeated | explicit IDs must be sorted and distinct |
| an ID the parent does not hold | explicit ID absent from parent |
| a string that is not a scenario ID | invalid scenario id identifier, listing the ID shapes |
an ordinary suite (/4) | input/1 requires coverage suite/5, suite/7 or suite/9 |
More columns: swipe horizontally, or focus the table and use the arrow keys.
A subset passing is not the whole suite passing: only a nonempty, complete, all-pass selection qualifies as conformance (Opt into declared coverage).
Expect the branch the input selects
From type: ess-scenario/6 a literal input its declared type refuses, at any depth, is
ESS-AUTHOR-015: a decoder refuses it before the command runs. Below /6 only its shape is checked.
validate reads each act's literal input against the command's when: guards, in the order a
conforming target answers them: input-guarded refusals first, the first declared of them; then
accepting when: and external branches in declaration order; the default only where no when:
holds. An act is refused with ESS-AUTHOR-041 when that order decidedly does not take the branch
it expects under outcome:. Where no outcome: is written, the check applies to the branches that
report its error:, and the act is refused only when none of them is taken.
# `id-required: ticket_id == ""` is an input-guarded refusal, so it answers before `closed`.
timeline:
- at: 2026-01-05T09:00:00Z
command: demo.tickets.SetTicketOpen
input: {ticket_id: "", open: false}
outcome: closed
The refusal names the branch that answers first and its guard, or the expected branch's own guard
that the input refutes. Only what the input decides is read. What a branch reads beyond the input
(a held state, a stored or related row, an external answer, the target's clock) is not decided, and
neither is a guard over a field sent as {$instance: …} or another reference. A guard over now
is decided only where it reads the same at every run, such as a start already in the past when the
operand was introduced. Where any of these leaves the answer open, the act is accepted.
Expect an external branch in an authored scenario
No input decides a branch declared external:, so an authored act that expects one names it under
outcome::
timeline:
- at: 2026-01-05T09:00:00Z
command: billing.email.SendEmail
input: {recipient: nobody@example.test, template: welcome}
outcome: failed
error: {name: billing.email.Undeliverable}
The act compiles into a configure_external_outcome for failed immediately before its
execute_command, as a generated scenario does, so the target is told which answer to give for
that call.
That is the only external answer an act can state. An act is refused with ESS-AUTHOR-037
when one of its claims holds only on an external answer it does not state, whether or not
outcome: is written. The check covers the act's error, its direct response, each event it
claims published and each event it claims absent. The answers it considers are those of the act's
own command and of every command a binding invokes from what the act publishes, however many
bindings along. A binding's escalation event needs its invoked command to fail. The refusal names
every such branch as command/branch.
For example, an act on billing.invoice.CreateInvoice that claims
billing.email.DeliveryEscalated is refused naming billing.email.SendEmail/failed. The binding's
SendEmail call fails only on that external branch, and an authored act has no key for the
answer a binding's call gives. Generated scenarios cover the escalation. An event that some
command's input-decided branch publishes exempts a claim only when the act reaches that command.
Expect the refusal an ungranted actor gets
An act sent as an actor the specification does not grant its command is refused with
ESS-AUTHOR-009, because it checks a system the model does not describe. To claim that such an
actor is refused, write refused: not_granted on the act, in type: ess-scenario/4:
timeline:
- at: 2026-01-05T09:00:00Z
command: billing.invoice.CreateInvoice
actor: billing.invoice.Auditor
input:
account_id: 00000000-0000-4000-8000-000000000001
customer_email: buyer@example.test
amount: {amount: 10, currency: EUR}
refused: not_granted
no_events: [billing.invoice.InvoiceCreated]
The act compiles into its execute_command, sent as that actor, and an expect_not_granted step
naming it. A target passes the step when it refuses the command before running it with the
standard refusal naming that actor: on a served surface, 403
{"refused": "not granted", "actor": "billing.invoice.Auditor"}. The step is suite/26 vocabulary,
so the suite is written at ess-conformance/26 or later.
A refused command takes no branch and publishes nothing, so no_events: is the only claim it can
carry. It is checked against the target's whole event log: the log may hold no more of each listed event
after the send than just before it, counting repeats, and a refusal that hands back events fails.
So a target that runs the command and only then refuses it fails. The generated runners perform
these observations through ObserveEvents / observeEvents. A custom runner must do the same.
On reaching the act's execute_command, it looks ahead to the expect_not_granted that follows.
Before sending, it observes each no_events: event in the scenario's correlation and counts the
occurrences. After the refusal, it observes each event again in the same correlation. The step
fails if any count grew. A target that cannot observe its log leaves the scenario unsupported,
never passed. The command's answer and view comparisons cannot replace these observations. See
the target interface for the full
contract. No extra authored step is needed. The act is refused:
| When | Refusal |
|---|---|
| its actor holds the grant | ESS-AUTHOR-038 |
it names no actor:, or carries outcome:, error:, response:, events: or capture: | ESS-AUTHOR-039 |
the document is ess-scenario/1 to /3 | ESS-AUTHOR-001, naming refused: not_granted |
More columns: swipe horizontally, or focus the table and use the arrow keys.
Where the specification serves a component (reached_by: network), synthesis already witnesses the
refusal once per command, as <command>/grant/denied, for every
command some declared actor lacks the grant for. An authored act adds a particular input or a
particular actor.
A served command no declared actor is granted is refused to every caller, so an act that sends it
with no actor: and expects it to run is refused with ESS-AUTHOR-040. Grant the command to an
actor and send the act as that actor, or expect the refusal as above.
State the caller an act is sent as
A command may read an attribute of its caller: {caller: account_id} in sets: or payload:,
caller.agent_id in a guard. A generated scenario picks those values itself. An authored act
states them under caller:, in type: ess-scenario/5:
type: ess-scenario/5
arrange:
- instance: account
entity: ledger.notes.Account
setup:
identity: 3f1d5b7e-0000-4000-8000-000000000001
fields: {label: main}
state: Active
timeline:
- at: 2026-01-05T09:00:00Z
command: ledger.notes.OpenNote
actor: ledger.notes.Member
caller: {account_id: {$instance: account}}
input: {title: first}
outcome: opened
Each value is a literal of the attribute's declared type, or {$instance: name} for an instance
arranged with setup:, whose identity the file states. The act's execute_command carries the
values as its caller, and the target sends the command authenticated as that caller, exactly as
it does for a generated suite. An act with no caller: sends none, and its suite keeps its bytes.
| When | Refusal |
|---|---|
caller: names an attribute the act's actor: does not declare, or the act names no actor: | ESS-AUTHOR-013, naming the attribute, the actor and the command |
| the command reads a required attribute the act does not state, or the act states a caller and leaves one of the actor's required attributes out | ESS-AUTHOR-014, naming the attribute and the command |
a value is not of the attribute's type, or {$instance: name} names an instance captured at run time | ESS-AUTHOR-015, naming the attribute |
the document is ess-scenario/1 to /4 | ESS-AUTHOR-001, naming type: ess-scenario/5 |
More columns: swipe horizontally, or focus the table and use the arrow keys.
A captured identity is chosen by the target while the scenario runs, and a caller's values are
fixed when the suite is written, so an instance named under caller: has to be arranged with
setup:. A suite that reaches a caller attribute nothing supplied, such as one written by an
older ess, reports the scenario unsupported with a reason naming the attribute.
Claim one member of a nested value
A nested value in a claim is compared whole: response: {checkpoint: {checkpoint_id: cp-1}} is
refused for the members it leaves out, named at their path (ESS-AUTHOR-014). From
type: ess-scenario/6 a claim names one member alone by its dotted path,
response: {checkpoint.checkpoint_id: cp-1}, in an act's response:, an event's payload:, an
error's fields: and a view row's contains, excludes and at. Each path is compared alone at
the member's declared type, through Optional and newtypes; a member absent or null along it
matches only null, and a path ending at a struct compares that struct whole.
A path naming no member is ESS-AUTHOR-013 with the full path, as is any dotted key under
ess-scenario/1 to /5. A path through a list, map, union, enum or primitive, a path in input:,
setup:, caller: or params: (all sent whole), and a path beside a whole field claiming the same
member are ESS-AUTHOR-015; a path holding a reference is ESS-AUTHOR-021. Event, error and row
paths compile to suite/26 dotted keys, so their suites keep their format; a response path compiles
to expected under ess-conformance/62, which an older reader refuses by its version.
Establish backend state in an authored scenario
ess-scenario/2, introduced in 0.23.0, supports typed setup for entities whose rows arrive
from an upstream system. A scenario can establish those rows and query their
view without inventing a creator command:
type: ess-scenario/2
domain: calls.history
scenario: recent-call
summary: A stored call appears in history.
arrange:
- instance: recent
entity: calls.history.CallRecord
setup:
identity: 00000000-0000-4000-8000-000000000001
fields: {started_at: '2026-01-05T09:00:00Z', duration_seconds: 12}
state: Completed
assert:
- view: calls.history.CallHistory
contains: {call_id: {$instance: recent}, duration_seconds: 12}
The model must declare that entity, its field types, lifecycle state and view. Setup validates identity, required fields, nested values and invariants. Duplicate qualified identities, null identities and inconclusive invariants refuse.
The adapter must establish actual isolated backend state and make it visible before acknowledging setup. Setup emits no command or event and claims no lifecycle path. Rust and generated Go expose an optional setup capability; unsupported adapters produce a non-passing result. These steps use suite/6 or coverage suite/7 and require explicit report/2. Source scenario/1 refuses setup.
Name several instances in one input
{$instance: name} stands wherever the declared type at that position is the instance's
identity type: a whole input field, a list element, a map value, a struct member or the
payload of a union variant whose type is the identity, at any depth. A rollout over three release rings names them in order:
input:
ring_sequence: [{$instance: a}, {$instance: b}]
by_stage: {canary: {$instance: b}, general: {$instance: c}}
pair: {primary: {$instance: c}, note: first}
Each reference resolves to the identity the run bound for that instance, and the command
receives the list or mapping with those identities in place. A reference at a position of
any other type is refused as ESS-AUTHOR-022, naming the position (labels[1],
pair.note, tags[owner], target.value); one at a member the model does not declare, or
inside a value of the wrong shape, is refused naming the member or the position. One nested
inside an event payload or an error is refused as ESS-AUTHOR-021. A suite carrying such a
value is suite/32 (/33 with coverage); Rust, Go and TypeScript resolve it with report/2.
Compare an event's identity with a captured instance
An expected event may name a captured instance for a payload field typed as that instance's identity, so an implementation that drops the identity, or publishes another one, fails:
events:
- event: billing.invoice.InvoicePaid
payload: {invoice_id: {$instance: invoice}, amount: {amount: 10, currency: EUR}}
The reference resolves to the identity the run bound, as it does in a command input, and the
event must carry exactly that value. The act compiles into an expect_event_values step, so the
suite is written at ess-conformance/18 or later; Rust, Go and TypeScript resolve it, and browser
replay shows it as a declaration. A reference at a field of any other type, including another
entity's identity, an error field, or a position nested inside a payload field, is refused as
ESS-AUTHOR-021.
Synthesis makes the same comparison wherever the arrangement determines an identity an event
carries: an input sent as a captured instance, the subject's own identity ({subject: <identity>}),
and a related row's identity read through {related: …}.
Leave a generated value to the implementation
A field the outcome marks {generated: true} is minted by the implementation, so an act that
claims one exact value for it passes only where the implementation happens to mint that value. It
is refused as ESS-AUTHOR-042, naming the field and the branch that mints it:
outcome: selected
events:
- event: relay.desk.EntryAgentSelected
payload: {agent_id: a1, connect_seq: 7} # refused: the outcome mints connect_seq
The same holds for an error's fields, for a member of a struct the outcome fills member by member,
for {input: f, else: {generated: true}} where the act sends no f, for {generated: true, present: true}, and for a view row's stored field that every branch the scenario reaches mints,
including one a sets: entry reads from a minted payload field with {payload: <Event>.<field>}.
In an event's payload or an error's fields, drop the field from the expectation, and the suite
still checks its presence and type there, or relate it to another value with capture: on the
event. A view read checks only what the scenario claims of a row, so in a view row drop the field
and assert satisfies: defined(<field>) on the view, which requires it on every row and at least
one row, or state its relation to an event's value with {$observed: {event: …, field: …}}. A
value a setup: states, or one a branch fills from the input, keeps its exact claim.
From ess/30: a minted value the scenario determines
Some minted values are fixed by the scenario's own earlier steps: a revision number is the count of
commits the scenario made, a cursor is the offset after the page it asked for, a total is the size
of the graph it built. From format: ess/30 an act may state such a value, and the claim is the
scenario's own. The act is accepted, and the suite checks the value exactly, as it checks a value
the input fills:
outcome: committed
events:
- event: demo.ledger.RevisionCommitted
payload: {revision: 2} # the second commit this scenario made
An implementation that mints another value fails the scenario; one that mints the stated value
passes. Every place listed above takes the claim alike: an event's payload, an error's fields, a
struct member and a view row's stored field. The model interpreter mints its own counter, so on
--target ir, and in the baseline mutate --scenarios runs, such a scenario passes only where the
counter happens to mint the stated value; elsewhere the baseline is refused as ESS-MUTATE-001.
Synthesis never pins a minted value at any format. Below ess/30 the act is refused as
ESS-AUTHOR-042, its message unchanged and its help naming ess/30.
Where the observation sections went
What a suite observes beyond an act's own answer is on Observations. Each section that used to be on this page is listed here under its old anchor, so an older link still finds it.