Dry-run a flow
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.
Before you start
Section titled “Before you start”- A flow, registered on a runtime such as
kindgi dev, with Kindgi 0.1's CLI. This guide usesacme.handle-orderandacme.check-order-stock. - Tools declared truthfully:
mutating: falseon 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_URLandKINDGI_API_TOKENset.
Dry-run it
Section titled “Dry-run it”Start the run with --dry-run:
pnpm exec kindgi runs start --flow=acme.handle-order --input='{"orderId":"A-100"}' --dry-runuv run kindgi runs start --flow=acme.handle-order --input='{"orderId":"A-100"}' --dry-run./kindgiw runs start --flow=acme.handle-order --input='{"orderId":"A-100"}' --dry-run./kindgiw 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", …}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"}, …}A flow whose steps all only read completes, with its output:
pnpm exec kindgi runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}' --dry-runuv run kindgi runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}' --dry-run./kindgiw runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}' --dry-run./kindgiw runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}' --dry-run{ … "status": "completed", "dryRun": true, … "output": { "items": [ … ] }, …}What runs
Section titled “What runs”- A tool step runs if the tool is declared read-only, with
mutating: false, and itseffectsdeclare nowrites,deletes,spawns-run,emits-eventorexternal-side-effect. A tool that leavesmutatingout counts as one that changes something. The first other tool stops the run withdry-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.
From your app
Section titled “From your app”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 }:
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 },});from kindgi.client import Kindgi
kindgi = Kindgi() # KINDGI_API_URL, KINDGI_API_TOKEN
run = kindgi.runs.start( flow="acme.check-order-stock", input={"orderId": "A-200"}, options={"dryRun": True})package com.acme.app;
import com.kindgi.client.Kindgi;import com.kindgi.client.models.Run;import com.kindgi.client.models.StartRunBody;import java.util.Map;
public final class DryRun { public static void main(String[] args) { Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN
Run run = kindgi.runs().start(StartRunBody.WithFlow.builder() .flow("acme.check-order-stock") .input(Map.of("orderId", "A-200")) .options(StartRunBody.WithFlow.Options.builder().dryRun(true).build()) .build()); }}package acme.app
import com.kindgi.client.Kindgiimport com.kindgi.client.models.StartRunBodyimport scala.jdk.CollectionConverters._
object DryRun { def main(args: Array[String]): Unit = { val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN
val run = kindgi.runs().start(StartRunBody.WithFlow.builder() .flow("acme.check-order-stock") .input(Map("orderId" -> "A-200").asJava) .options(StartRunBody.WithFlow.Options.builder().dryRun(true).build()) .build()) }}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.