Shape the world for a test
The record your test needs, the call that has to fail.
This page is executed as written by packages/cli/src/journeys/tutorials.test.ts; the recording
is made from the same run.

There are two kinds of thing a test wants from a vendor, and Volter has one rule for each:
If the twin stores it, create it through the vendor's own API. Everything else, judgment, lookups, faults, is a handler.
Data: create it through the API
A customer, an issue, a channel, a file: anything the vendor stores, the twin stores, and the way to put it there is the way your app would. In a test's setup, with the vendor's SDK:
const customer = await stripe.customers.create({ email: 'ada@example.com' });
The record is real to the twin: it has an id, it appears in lists, it can be updated and deleted, and the twin enforces the vendor's rules about it. A record the vendor would refuse is refused here too, which is the point. For state every test shares, put it in the seed instead; see seed and reset.
Behavior: write a handler
Some things a vendor does are not records. A model's answer, a search's ranking, a rate limit, a timeout, a 500 on the third call: these are judgment and faults, and the twin has no way to know what your test wants them to be. A handler tells it.
This page's app talks to Anthropic, and asks it questions with a small script:
{ "name": "agent", "private": true, "dependencies": { "@anthropic-ai/sdk": "^0.30" } }
ANTHROPIC_API_KEY=
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, maxRetries: 0 });
try {
const message = await anthropic.messages.create({ model: 'claude-sonnet-4-5', max_tokens: 64, messages: [{ role: 'user', content: process.argv[2] }] });
console.log(200, message.content.find((c) => c.type === 'text')?.text ?? '');
} catch (error) {
console.log(error.status, error.error?.error?.type ?? error.message);
}
npm install
npm install -g @volter/world
npm install -D @volter/twin-anthropic
volter world init
Handlers live in .volter/handlers/<vendor>.json, one file per twin; init copied the
anthropic twin's starter there. The grammar is small: match a request, respond, or inject a fault.
{
"handlers": [
{ "id": "triage-answer", "on": { "userTextIncludes": "classify this ticket" }, "respond": { "text": "billing" } },
{ "id": "flaky-once", "on": { "userTextIncludes": "summarize" }, "once": true, "fault": { "kind": "status", "status": 529, "message": "Overloaded" } }
]
}
onis what the handler matches: a phrase in the user's text, a tool the caller offered, a path, a query. Each twin documents its matchers atGET <twin-url>/twin.respondis what the twin answers: text, or a tool call, in the vendor's own shape.faultinstead ofrespondinjects a failure:{ "kind": "status", "status": 529 }answers the vendor's own error envelope with that status,{ "kind": "slow", "ms": 3000 }delays,{ "kind": "drop" }holds the socket.oncefires the handler one time and then lets the twin's default behavior through, which is how a test gets "fails, then succeeds".
Handlers are data, not code, so they are deterministic, diffable and committed with the world. A twin reads them when it starts:
volter world up --no-seed
acme-web 1 twin up
The scripted answer, where the handler matches:
volter world run -- node ask.mjs "classify this ticket: my card was charged twice"
200 billing
The fault, once, then the default behavior, which for a generative twin is a labeled
deterministic stub. The script sets maxRetries: 0 to show the fault; with the SDK's default
retries on, it would retry the 529 and print the stub on the second try, exactly as production
code would, which is what a once-fault is for: it exercises the retry path.
volter world run -- node ask.mjs "summarize the thread"
529 overloaded_error
volter world run -- node ask.mjs "summarize the thread"
200
See what matched
volter world run -- sh -c 'curl -s "$ANTHROPIC_TWIN_URL/twin/scenario"'
"id":"triage-answer","matches":1
"id":"flaky-once","once":true,"matches":1
A miss is a request no handler matched, and the recent ones carry enough of the request to write
the handler you were missing. Unmatched asks are also in the world's log, so volter world log
shows them in order with everything else the app did.
volter world down
Time
A test that needs "thirty days later" sets the world's clock rather than waiting or mocking
Date. Every twin stamps records from it: volter world clock advance 30d. See
seed and reset.
Faults that are not handlers
Some faults are the twin's own surface, because the vendor's are. A twin in --read-only mode
refuses every write with the vendor's own refusal shape, which exercises your error paths. A
vendor with rate limits enforces them as the vendor documents them. Each twin's README says which
of these it models.
Playback
Each command above, as the recording shows it.
npm install

npm install -g @volter/world

npm install -D @volter/twin-anthropic

volter world init

volter world up --no-seed

volter world run -- node ask.mjs "classify this ticket: my card was charged twice"

volter world run -- node ask.mjs "summarize the thread"

volter world run -- node ask.mjs "summarize the thread"

volter world run -- sh -c 'curl -s "$ANTHROPIC_TWIN_URL/twin/scenario"'

volter world down
