Mock nodes while authoring a flow
Declare per-node fixtures in a flow.yaml mocks: section so a flow whose nodes cost money, need a key, or have side effects still runs end to end — and so failure paths can be exercised on demand.
View as MarkdownSome nodes should not be called while you are still wiring a flow up. A connector that carries your own API key spends real quota on every run; a node that sends an email, files a ticket, or charges a card has a real side effect the tenth time you run it too. And a node that always succeeds gives you no way to check that your failure edges and retry policy do what you meant.
A mock answers one node from a fixture you wrote, inside an otherwise completely real run. Everything else is unchanged: the same compiler, the same worker, the same edge expressions, joins, conditions, retries, error routing, and checkpoints. Only that node's body is replaced.
Mocks live in the flow's source, so they are explicit, reviewable, and loud — every surface that touches a mocked flow says it is mocked, and a mocked flow cannot be published.
Prerequisites: a flow.yaml you can compile — see
Publish a flow to the marketplace for the
compile/run/publish lifecycle.
Declare a mock in flow.yaml
mocks: is a top-level section keyed by node alias — the same alias the node
carries in nodes: and in edges:. Each entry sets exactly one of three
fields:
| Field | Meaning |
|---|---|
output | The node's response, inline, as its declared output message in JSON (snake_case field names — the same dialect axiom invoke prints). For an Instance node this is the facade output shape you wire against, not the underlying generic port. |
file | Path to a JSON fixture with that same content, relative to the flow.yaml. The CLI inlines the file's contents when you compile. |
error | {code, message} — a typed failure instead of a response, so retry policies and on_error / rollback edges can be exercised. |
A support-triage flow whose classifier is a keyed connector:
# support-triage.flow.yaml
name: me/support-triage
version: 0.1.0
nodes:
- alias: classify
package: me/openai-connector@0.1.0
node: ChatCompletion
- alias: notify
package: me/slack-connector@0.1.0
node: PostMessage
edges:
- from: classify
to: notify
adapter:
text: "value.content"
mocks:
classify:
output:
content: "category: billing"
model: "gpt-4o-mini"
usage:
total_tokens: 42
notify:
file: mocks/notify-ok.jsonEach fixture is checked against the node's real output message when you compile: an unknown or misspelled field fails the compile and names the field path, so a wrong-shaped mock is never a runtime surprise.
A mocked node needs no secret. The node never runs, so its required secrets are moot — the flow above runs end to end with no OpenAI key configured at all. That is the point: verify the wiring first, then set the secret and go live.
Compile a mocked flow
A flow.yaml that has a mocks: section compiles to a mocked dev
artifact by default — you wrote the fixtures to use them:
axiom flow compile support-triage.flow.yaml✓ compiled flow "me/support-triage"
artifact: 01J8Z6K3W9XQ4M7C2YB5N0A1RT
▲ MOCKED dev artifact — 2/2 node(s) served from fixtures: classify, notify
not publishable; compile with --no-mocks to mint the live artifact--no-mocks compiles the same file ignoring the section, which is how you mint
the live artifact without editing anything:
axiom flow compile support-triage.flow.yaml --no-mocks✓ compiled flow "me/support-triage"
artifact: 01J9A1M6P2QR7T3D5XC8V4B0KE
✓ compiled LIVE (mocks: section ignored via --no-mocks)Mockedness is a property of the artifact, decided once at compile time — not a run-time flag. An artifact id either serves fixtures or it doesn't, so there is no way to run one live by accident, and no way for a caller to mock someone else's flow.
axiom flow pull of a mocked artifact restores the mocks: section into the
regenerated flow.yaml (in its inline output: form). A live artifact carries
no trace of the fixtures at all. axiom flow save also carries the section into
the saved flow document, so a draft you reopen in the editor still has its
fixtures — and so publishing that draft is refused (see
Mocked flows cannot be published).
Run a mocked flow
axiom flow run is unchanged — the artifact is mocked or it isn't — but the
run announces it before anything else:
axiom flow run 01J8Z6K3W9XQ4M7C2YB5N0A1RT -d '{"text":"card declined twice"}'▲ MOCKED RUN — 2 node(s) served from authoring fixtures: classify, notify
✓ succeeded (execution 4bf92f3577b34da6a3ce929d0e0e4736)
{
"channel": "#support",
"ts": "1735689600.000100"
}The invoke response carries the same fact as data: mocked_nodes lists the
aliases served from fixtures (see the
HTTP API reference). Scripts and
agents should read that field and refuse to treat a mocked run as proof the
flow works against the real services — it proves the wiring works, nothing
more. A run whose mocked_nodes is absent is a live run.
Executions stay fully inspectable: a mocked run records checkpoints and debug events like any other, and the mocked nodes are marked as such on the execution detail page (Debug a flow).
Mock a failure to test error paths
An error: mock makes a node fail on demand, which is the only way to exercise
failure topology without a real node that actually breaks:
mocks:
charge_card:
error:
code: TRANSPORT
message: "simulated upstream reset"The synthesized failure enters the ordinary failure ladder — the node's retry
policy, then on_error edges, then rollback edges, then the join's failure
policy — exactly as a real one would, so a run with this mock tells you whether
your compensation path is wired correctly.
code is one of USER, TIMEOUT, TRANSPORT, PANIC, or CANCELLED; any
other value is rejected when you validate or compile. TRANSPORT and TIMEOUT
are the retryable codes, so use one of them to watch a
retry policy back off
and re-dispatch; use USER to watch a deterministic failure route straight to
an on_error edge without retrying.
Record a fixture instead of writing one
Hand-writing a large response is the worst part of mocking, so the CLI can capture one for you:
# Capture what this alias actually returned in a past execution.
axiom flow mock add classify --from-execution 01K1G3ZC9H4T8B7Q2M6R0XSJDV
# …or supply it directly.
axiom flow mock add classify --data '{"content":"category: billing"}'
axiom flow mock add classify --file fixtures/classify.json
axiom flow mock add charge_card --error TRANSPORT:"simulated upstream reset"
# Drop one mock, or the whole section.
axiom flow mock rm classify
axiom flow mock rm --all--from-execution and --data write mocks/<alias>.json next to the
flow.yaml and add a file: entry pointing at it; --file references the path
you gave as-is. Both commands take an optional flow.yaml path as their last
argument and default to flow.yaml in the current directory. See
axiom flow mock add and
axiom flow mock rm.
This is the record-once, replay-forever loop: one deliberate live call — or one past live run you already paid for — seeds a fixture that costs nothing thereafter and never expires.
Assert on a mocked run with axiom flow test
Everything above gets you a flow that runs on fixtures. It does not check
that the run did what you meant — axiom flow run prints a result, it does
not compare it to anything. axiom flow test (ADR-200) is the assertion
layer over the same mocks: tests/<name>.test.yaml files beside flow.yaml
declare cases — an input, an optional per-case mock overlay, and typed
expectations — and each case compiles and invokes through the exact same
real compile+invoke path this guide already walks through. There is no
separate offline evaluator: a case's green means the real worker produced
what you expected, not that a second, simpler interpreter agreed with itself.
# tests/triage.test.yaml
cases:
- name: billing category routes to notify
input: {text: "card declined twice"}
mocks:
classify: {output: {content: "category: billing", model: "gpt-4o-mini"}}
expect:
status: success
nodes:
executed: [classify, notify]
- name: upstream reset fails the run
input: {text: "card declined twice"}
mocks:
classify: {error: {code: TRANSPORT}}
expect:
status: failure
error: {node: classify, code: TRANSPORT}axiom flow test support-triage.flow.yamlA case's mocks: overlay replaces flow.yaml's own mock for that alias —
never a field-by-field merge — and is never written back to the file, so the
fixtures you keep in mocks: stay your everyday "make it run" defaults while
tests/ pins the specific inputs/outputs you don't want to regress.
Boundary — read this before trusting a green suite: it proves the
graph behaves (edges, conditions, retries, error routing, output shape) —
it does not prove the deployed app works. It cannot catch routing,
secret delivery, cold starts, real-database constraints, or cross-app
descriptor collisions, because a mocked node never dispatches to begin with.
Keep a live axiom flow run (or your publish-time smoke test) for those.
Full schema and the error.code-classification boundary:
Test a flow.
Mocked flows cannot be published
axiom flow save carries the mocks: section into the saved flow document —
the fixtures are part of the flow's source, so a draft round-trips intact — and
that is exactly what arms the publish gate. Publishing is then rejected at both
ends:
- Publishing a saved document whose source declares
mocks:is refused, naming how many mocks the source declares. - Publishing a compiled artifact whose graph embeds fixtures is refused too, even if the source looks clean.
Publish never strips mocks silently: a published flow must run its real nodes, so going live is an explicit act on your side. Both messages appear in full in the error catalog.
Take a mocked flow live
Remove the fixtures from the file, save that, and publish:
axiom flow mock rm --all support-triage.flow.yaml # or delete the mocks: section by hand
axiom flow save support-triage.flow.yaml # → graph id, now mock-free
axiom flow publish <graph-id>--no-mocks is the other half of the story and does a different job: it mints a
live compiled artifact from a flow.yaml that still has its fixtures, for
running the real thing directly without editing the file. Use it to check the
flow against the real services; use mock rm --all + save when you are ready
to publish.
axiom flow compile support-triage.flow.yaml --no-mocks
axiom flow run <artifact-id> -d '{"text":"card declined twice"}' # no MOCKED bannerWhat cannot be mocked
- Facade boundary nodes (
@flow_input/@flow_output) — they are pass-throughs with no body to replace. Mock the real node next to them. - Subflow nodes — declare the mock inside the subflow's own
flow.yamland compile that flow instead. - Flows in pipeline mode — a streaming node answers with a frame sequence,
not a single response, so
mocks:is rejected forpipeline_mode: trueflows.
axiom flow validate catches all three locally, before any registry
round-trip, along with unknown aliases, an entry that sets more than one of
output/file/error, an unknown error code, and a fixture file that is
missing or is not valid JSON.
Related pages
- Test a flow — the assertion layer over these mocks
(
axiom flow test): typed expectations, per-case mock overlays, and the bug classes it catches. - Publish a flow to the marketplace — the compile → publish lifecycle a mocked artifact is deliberately excluded from.
- Manage secrets in a flow — set the key the mocked node
did not need, before you compile with
--no-mocks. - Debug a flow — inspect a mocked run's per-node payloads.
- Error catalog — the exact compile and publish messages, with fixes.