---
title: "Mock nodes while authoring a flow"
description: "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."
category: guide
surfaces: [cli]
related: [guides/publish-a-flow, guides/manage-secrets, guides/debug-a-flow, guides/test-a-flow, reference/error-catalog, concepts/execution-model]
last_reviewed: 2026-08-17
---

# Mock nodes while authoring a flow

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](publish-a-flow.md) 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:

```yaml
# 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](manage-secrets.md) 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:

```bash
axiom flow compile support-triage.flow.yaml
```

```text
✓ 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:

```bash
axiom flow compile support-triage.flow.yaml --no-mocks
```

```text
✓ 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](#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:

```bash
axiom flow run 01J8Z6K3W9XQ4M7C2YB5N0A1RT -d '{"text":"card declined twice"}'
```

```text
▲ 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](../reference/http-api.md#invoke-a-flow)). 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](debug-a-flow.md)).

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

```yaml
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](flow-configs.md#configure-per-node-retry-and-backoff) 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:

```bash
# 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](../reference/cli/axiom-flow-mock-add.md) and
[axiom flow mock rm](../reference/cli/axiom-flow-mock-rm.md).

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.

```yaml
# 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}
```

```bash
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](test-a-flow.md).

## 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](../reference/error-catalog.md#authoring-mock-errors).

### Take a mocked flow live

Remove the fixtures from the file, save that, and publish:

```bash
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.

```bash
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.

## Related pages

- [Test a flow](test-a-flow.md) — 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](publish-a-flow.md) — the compile → publish
  lifecycle a mocked artifact is deliberately excluded from.
- [Manage secrets in a flow](manage-secrets.md) — set the key the mocked node
  did not need, before you compile with `--no-mocks`.
- [Debug a flow](debug-a-flow.md) — inspect a mocked run's per-node payloads.
- [Error catalog](../reference/error-catalog.md) — the exact compile and publish
  messages, with fixes.
