# Mark a tool read-only

> Declare which tools only read, so they run in a dry run, pass approval gates without asking, and can run at the same time as each other.

This guide marks a tool that only reads, so dry runs run it, approval gates let it through, and an agent can run its calls at the same time as other read-only calls.

## Before you start

- **A tool,** in the quickstarts' pack, `my-pack`, with `kindgi dev` running and Kindgi 0.2's CLI.

## Mark it read-only

A tool declares whether it changes anything outside Kindgi. One that only reads, such as a lookup, a search or a calculation, says so. Ask your coding agent: "My order lookup tool only reads. Mark it read-only." Kindgi's skills in your pack tell it how ([How it knows Kindgi](https://docs.kindgi.com/v0.3.1/start/coding-agents.md)). For reference, the tool's definition then looks like this:

- **TypeScript**

  ```ts excerpt
  // in tools/lookup-order/index.ts
  const defined = defineTool({
    id: 'my-pack.lookup-order' as ToolId,
    // …
    effects: [],
    mutating: false,
    // …
  });
  ```

- **Python**

  ```python excerpt
  # in tools/lookup_order.py
  @tool(id="my-pack.lookup-order", mutating=False)
  def lookup_order(ref: OrderRef, ctx: ToolContext) -> Order:
      ...
  ```

- **Java**

  ```java excerpt
  // in src/main/java/mypack/tools/LookupOrder.java
    public static final Tool<Input, Output> TOOL = Tool.define("my-pack.lookup-order")
        // …
        .mutating(false)
        // …
  ```

- **Scala**

  ```scala excerpt
  // in src/main/scala/mypack/tools/LookupOrder.scala
    val tool: Tool[Input, Output] = Tool[Input, Output]("my-pack.lookup-order")
      // …
      .readOnly
      // …
  ```

Leaving `mutating` out is the same as `mutating: true`. That's the safe default: a tool counts as one that changes things until it says otherwise. It decides three things: whether the tool runs in a dry run, whether an approval gate asks before it, and whether an agent can run its calls at the same time as others.

## Dry runs

`--dry-run` runs a flow without changing anything. A tool step runs only if the tool is read-only. `--flow` is the flow, and `--input` its input:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run
  ```

```json excerpt
  "status": "completed",
  "dryRun": true,
  …
  "output": {
    "status": "shipped",
    "orderId": "ord_1001",
    "totalCents": 4200
  },
```

The first tool that isn't stops the run, before the tool is called:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run
  ```

```json excerpt
  "status": "failed",
  "dryRun": true,
  "failureMessage": "dry-run-effectful-tool: tool \"my-pack.add-order-note\" is not declared read-only (mutating: false), so it does not run in a dry run",
```

An agent step in a dry run skips its model call, so the agent calls no tools. The sample's `echo-flow` greets with a read-only tool, then hands the greeting to its agent:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run
  ```

```json excerpt
  "status": "completed",
  "dryRun": true,
  …
  "output": {
    "reply": "[dry-run: model call skipped]",
    "greeting": "Hello, Ada!"
  },
```

Use a dry run to check a flow's wiring before it touches real data.

## Effects

`effects` lists what a tool touches, for review and policy: `{ kind: 'writes', resource: 'external:orders' }`. The kinds are `reads`, `writes`, `deletes`, `network`, `spawns-run`, `emits-event`, `external-side-effect` and `sensitive-data-egress`.

A tool with a `writes`, `deletes`, `spawns-run`, `emits-event` or `external-side-effect` effect doesn't run in a dry run, and an agent runs each call to it on its own, even when it says `mutating: false`. Keep the two consistent: a tool that only reads declares `mutating: false` and, at most, `reads` effects.

## Approval gates

An agent can make a person approve tool calls, as [Ask before a tool runs](https://docs.kindgi.com/v0.3.1/guides/approvals/ask-before-a-tool-runs.md) shows. With its gates on and no rule for a tool, a read-only tool runs without asking. Any other tool waits for approval on its first use.

`mutating: false` doesn't lift every wait. In a run no person started, a read that uses a person's connection still waits for that person's yes ([Use a connection in a tool](https://docs.kindgi.com/v0.3.1/guides/connections/use-a-connection.md#approvals)). In a run a mail started, so does a read that uses a connection of the run's own mailbox, its app and whose it is ([What waits for a yes](https://docs.kindgi.com/v0.3.1/guides/runs/start-runs-from-new-mail.md#what-waits-for-a-yes)).

## Calls that run together

A model can ask for several tool calls in one step. An agent runs calls to read-only tools that come one after another at the same time, up to 8 of them, and a call to any other tool on its own, in the order the model asked (runtime 0.3.0 or later; earlier ones run every call on its own). A call to a read-only tool that may wait, for [another system's answer](https://docs.kindgi.com/v0.3.1/guides/tools/wait-for-another-system.md) or for the person who started the run to [connect an app](https://docs.kindgi.com/v0.3.1/guides/connections.md), runs on its own too. A call that a `before-tool` [guardrail](https://docs.kindgi.com/v0.3.1/guides/guardrails/configure-a-guardrail.md) judges doesn't start until the calls before it have finished, so its check sees them as calls that ran. The read-only calls after it can run alongside it. Once a call needs an approval, the step's calls that haven't started run one at a time. Either way, the results reach the conversation and the model in the order the model asked for them, and each call's events carry its own duration.

To run every call on its own, set `parallelToolCalls: false` on the agent (`parallel_tool_calls=False` in Python, `set("parallelToolCalls", false)` in Java and Scala). Do that when its read-only tools mustn't overlap, such as two lookups against an API that allows one request at a time. An earlier runtime accepts the setting and runs every call on its own anyway.

## Results from outside

A tool whose results are text from outside that nobody checked, such as a mail someone sent or a page a search found, says so: `resultIsOutsideData: true` (`result_is_outside_data=True` in Python, `.resultIsOutsideData(true)` in Java and Scala). An agent then reads each result of the tool as data, not instructions, in that turn and in every later turn's history. Absent means `false`. The ready-made mail reads set it ([Ready-made tools](https://docs.kindgi.com/v0.3.1/guides/connections/use-a-connection.md#ready-made-tools)).

It changes what the model is told, not what the tool may do: pair it with approvals for anything an agent could do with what it read.

## Getting it right

- Declare `mutating: false` on every tool that only reads. Otherwise a dry run stops at it, an approval gate asks before it, and an agent runs each call to it on its own.
- Never declare it on a tool that writes, sends or deletes. A dry run would then run it for real, and an agent could run it at the same time as other calls.
- A tool from an [MCP server](https://docs.kindgi.com/v0.3.1/guides/tools/mcp-servers.md) always counts as one that changes things: it doesn't run in a dry run.

## Next

- [Dry-run a flow](https://docs.kindgi.com/v0.3.1/guides/flows/dry-run-a-flow.md)
- [Ask before a tool runs](https://docs.kindgi.com/v0.3.1/guides/approvals/ask-before-a-tool-runs.md)
- [Use an MCP server's tools](https://docs.kindgi.com/v0.3.1/guides/tools/mcp-servers.md)
