Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
Documentation menu

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 Markdown

Some 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:

FieldMeaning
outputThe 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.
filePath 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.json

Each 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.yaml

A 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 banner

What 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.yaml and compile that flow instead.
  • Flows in pipeline mode — a streaming node answers with a frame sequence, not a single response, so mocks: is rejected for pipeline_mode: true flows.

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.

  • 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.