# Dry-run a flow

> Run a flow's read-only steps to check its wiring, and stop before the first step that would change something.

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

- **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](https://docs.kindgi.com/v0.1.8/guides/flows/dry-run-a-flow.md#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.

## Dry-run it

Start the run with `--dry-run`:

- **TypeScript**

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

- **Python**

  ```sh
  uv run kindgi runs start --flow=acme.handle-order --input='{"orderId":"A-100"}' --dry-run
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=acme.handle-order --input='{"orderId":"A-100"}' --dry-run
  ```

- **Scala**

  ```sh
  ./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](https://docs.kindgi.com/v0.1.8/guides/flows/dry-run-a-flow.md#what-runs) shows. It prints the run, which stopped at the first step that writes:

```json excerpt
{
  "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:

```json excerpt
{"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:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}' --dry-run
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}' --dry-run
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}' --dry-run
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=acme.check-order-stock --input='{"orderId":"A-200"}' --dry-run
  ```

```json excerpt
{
  …
  "status": "completed",
  "dryRun": true,
  …
  "output": {
    "items": [
      …
    ]
  },
  …
}
```

## What runs

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

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

- **TypeScript**

  `scripts/dry-run.ts`

  ```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 },
  });
  ```

- **Python**

  `scripts/dry_run.py`

  ```python
  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}
  )
  ```

- **Java**

  `src/main/java/com/acme/app/DryRun.java`

  ```java
  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());
    }
  }
  ```

- **Scala**

  `src/main/scala/acme/app/DryRun.scala`

  ```scala
  package acme.app

  import com.kindgi.client.Kindgi
  import com.kindgi.client.models.StartRunBody
  import 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](https://docs.kindgi.com/v0.1.8/guides/webhooks/receive-run-finished.md) leave dry runs out unless their filter says `includeDryRuns: true`.

## Next

- [Write a flow](https://docs.kindgi.com/v0.1.8/guides/flows/write-a-flow.md)
- [Read a run's journal](https://docs.kindgi.com/v0.1.8/guides/runs/read-the-journal.md)
- [Branch a flow](https://docs.kindgi.com/v0.1.8/guides/flows/branch-a-flow.md)
