Skip to content

Dry-run a flow

View as Markdown

This guide checks a flow's wiring against real data, by running its read-only steps and stopping before the first step that would change something.

A dry run runs a flow for real up to the first step that could change something, and stops there. It checks the flow's mappings, its conditions and the data each step gets, without the writes.

  • A flow, registered on a runtime such as kindgi dev, with Kindgi 0.1's CLI. This guide uses acme.handle-order and acme.check-order-stock.
  • Tools declared truthfully: mutating: false on every tool that only reads, and never on one that writes, as What runs explains.
  • For the app's half: Node 22.12 or later, Python 3.11 or later, or Java 17 or later, with Kindgi 0.1's client, and KINDGI_API_URL and KINDGI_API_TOKEN set.

Start the run with --dry-run:

Terminal window
pnpm exec kindgi runs start --flow=acme.handle-order --input='{"orderId":"A-100"}' --dry-run

--flow is the flow, and --input its input. --dry-run runs only the tools declared read-only, as What runs shows. It prints the run, which stopped at the first step that writes:

{
"id": "404da259-4a63-4e6d-a0af-15ad7d60dd8c",
…
"flowId": "acme.handle-order",
"flowVersion": "0.1.0",
"status": "failed",
"dryRun": true,
…
"failureMessage": "dry-run-effectful-tool: tool \"acme.email-customer\" is not declared read-only (mutating: false), so it does not run in a dry run",
…
}
Excerpt

acme.get-order ran, the branch was decided, and the run stopped at acme.email-customer, before calling it. Its journal, which kindgi runs journal <run-id> prints, shows how far it got. It shows the input the next step would have had:

{"sequence": 3, "kind": "step.completed", "nodeId": "order", "payload": {"output": {"items": [{"sku": "mug", "quantity": 2}], "total": 42.5, "orderId": "A-100", "customer": "ada@example.com"}}, …}
{"sequence": 4, "kind": "edge.evaluated", "payload": {"edgeId": "e2", "decision": false}, …}
{"sequence": 5, "kind": "edge.evaluated", "payload": {"edgeId": "e3", "decision": true}, …}
{"sequence": 6, "kind": "step.started", "nodeId": "email", "payload": {"input": {"customer": "ada@example.com"}}, …}
{"sequence": 7, "kind": "step.failed", "nodeId": "email", "payload": {"message": "dry-run-effectful-tool: tool \"acme.email-customer\" is not declared read-only (mutating: false), so it does not run in a dry run"}, …}
Excerpt

A flow whose steps all only read completes, with its output:

Terminal window
pnpm exec kindgi runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}' --dry-run
{
…
"status": "completed",
"dryRun": true,
…
"output": {
"items": [
…
]
},
…
}
Excerpt
  • A tool step runs if the tool is declared read-only, with mutating: false, and its effects declare no writes, deletes, spawns-run, emits-event or external-side-effect. A tool that leaves mutating out counts as one that changes something. The first other tool stops the run with dry-run-effectful-tool. Everything before it really ran.
  • An agent step runs its turn without calling the model: its answer is [dry-run: model call skipped], and it costs nothing. The steps after it get that text, so the dry run shows the wiring, not the agent's judgment.

So a dry run is only as safe as the tools' declarations. Declare mutating: false on every tool that only reads, and never on one that writes.

Ask your coding agent: "I want my app to start a dry run of my flow." For reference, your app starts a dry run with options: { dryRun: true }:

scripts/dry-run.ts
import { createClient } from '@kindgi/sdk/client';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN
const run = await kindgi.runs.start({
flow: 'acme.check-order-stock',
input: { orderId: 'A-200' },
options: { dryRun: true },
});

A dry run is a run like the others, marked dryRun: true, with a journal. Webhook endpoints leave dry runs out unless their filter says includeDryRuns: true.