# Ask before a tool runs

> Make an agent wait for a person's approval before it calls a tool that changes something.

This guide makes an agent wait for a person before it calls a tool that changes something, then approves or rejects the call.

An agent turns on approval gates in its `conversationPolicy`. When the model asks for a gated tool, the run stops before the tool is called and waits for a reviewer. Approve, and the tool runs. Reject, and it doesn't.

## Before you start

- **A pack** on a runtime such as `kindgi dev`, with Kindgi 0.2's CLI. This guide uses the pack `acme-ops`, with a read-only tool, `acme-ops.greet`.
- **A model the agent can route to,** with `tool-use`, as [Models](https://docs.kindgi.com/v0.5.0/guides/models.md) shows.

## Gate a tool

`acme-ops.post-update` posts to a public status page. It changes something outside your app, so it doesn't say `mutating: false`. Ask your coding agent: "I want a tool that posts an update to our public status page." Kindgi's skills in your pack tell it how ([How it knows Kindgi](https://docs.kindgi.com/v0.5.0/start/coding-agents.md)). For reference, the tool looks like this:

- **TypeScript**

  `tools/post-update/index.ts`

  ```ts
  import { defineTool } from '@kindgi/sdk/define';
  import type { ToolId } from '@kindgi/sdk/types';
  import { z } from 'zod';

  const defined = defineTool({
    id: 'acme-ops.post-update' as ToolId,
    description: 'Posts a message to the public status page.',
    version: '0.1.0',
    input: z.object({ message: z.string().min(1).max(500) }),
    output: z.object({ posted: z.boolean(), postedAt: z.string() }),
    effects: [{ kind: 'external-side-effect', resource: 'external:status-page' }],
    handler: async () => ({ posted: true, postedAt: new Date().toISOString() }),
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);
  export default defined.value;
  ```

- **Python**

  `tools/post_update.py`

  ```python
  from datetime import UTC, datetime

  from pydantic import BaseModel, Field

  from kindgi import tool


  class Update(BaseModel):
      message: str = Field(min_length=1, max_length=500)


  class Posted(BaseModel):
      posted: bool
      posted_at: str = Field(alias="postedAt")


  @tool(id="acme-ops.post-update", effects=[{"kind": "external-side-effect", "resource": "external:status-page"}])
  def post_update(input: Update) -> Posted:
      """Posts a message to the public status page."""
      return Posted(posted=True, postedAt=datetime.now(UTC).isoformat())
  ```

- **Java**

  `src/main/java/acmeops/tools/PostUpdate.java`

  ```java
  package acmeops.tools;

  import com.kindgi.pack.Tool;
  import jakarta.validation.constraints.Size;
  import java.time.Instant;

  public final class PostUpdate {
    public record Input(@Size(min = 1, max = 500) String message) {}

    public record Output(boolean posted, String postedAt) {}

    public static final Tool<Input, Output> TOOL = Tool.define("acme-ops.post-update")
        .description("Posts a message to the public status page.")
        .version("0.1.0")
        .input(Input.class)
        .output(Output.class)
        .effect("external-side-effect", "external:status-page")
        .handler((input, ctx) -> new Output(true, Instant.now().toString()));

    private PostUpdate() {}
  }
  ```

- **Scala**

  `src/main/scala/acmeops/tools/PostUpdate.scala`

  ```scala
  package acmeops.tools

  import com.kindgi.pack.scaladsl._
  import jakarta.validation.constraints.Size
  import java.time.Instant

  object PostUpdate {
    final case class Input(@Size(min = 1, max = 500) message: String)
    final case class Output(posted: Boolean, postedAt: String)

    val tool: Tool[Input, Output] = Tool[Input, Output]("acme-ops.post-update")
      .description("Posts a message to the public status page.")
      .version("0.1.0")
      .effect("external-side-effect", "external:status-page")
      .handler((_, _) => Output(posted = true, postedAt = Instant.now().toString))
  }
  ```

This agent posts status updates with it, and every post waits for a person. Ask your coding agent: "I want an agent that posts status updates, and asks a person before every post." For reference, the agent looks like this:

- **TypeScript**

  `agents/status-agent/index.ts`

  ```ts
  import { defineAgent } from '@kindgi/sdk/define';
  import type { AgentId, Semver } from '@kindgi/sdk/types';

  const defined = defineAgent({
    id: 'acme-ops.status-agent' as AgentId,
    version: '0.1.0' as Semver,
    name: 'Status Agent',
    description: 'Posts status updates.',
    instructions: 'Post the update the user gives you with acme-ops.post-update.',
    capabilities: [{ needs: [{ feature: 'tool-use' as const }] }],
    tools: [
      { id: 'acme-ops.post-update', version: '0.1.0' },
      { id: 'acme-ops.greet', version: '0.1.0' },
    ],
    retrieval: [],
    guardrails: [],
    parameters: [],
    conversationPolicy: {
      hitl: {
        tools: {
          overrides: { 'acme-ops.post-update': 'always_ask' },
        },
      },
    },
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);
  export default defined.value;
  ```

- **Python**

  `agents/status_agent.py`

  ```python
  from kindgi import Agent

  from ..tools.greet import greet
  from ..tools.post_update import post_update

  status_agent = Agent(
      id="acme-ops.status-agent",
      version="0.1.0",
      name="Status Agent",
      description="Posts status updates.",
      instructions="Post the update the user gives you with acme-ops.post-update.",
      capabilities=[{"needs": [{"feature": "tool-use"}]}],
      tools=[post_update, greet],
      conversation_policy={
          "hitl": {
              "tools": {
                  "overrides": {"acme-ops.post-update": "always_ask"},
              },
          },
      },
  )
  ```

- **Java**

  `src/main/java/acmeops/agents/StatusAgent.java`

  ```java
  package acmeops.agents;

  import acmeops.tools.Greet;
  import acmeops.tools.PostUpdate;
  import com.kindgi.pack.Agent;
  import java.util.List;
  import java.util.Map;

  public final class StatusAgent {
    public static final Agent AGENT = Agent.define("acme-ops.status-agent")
        .version("0.1.0")
        .name("Status Agent")
        .description("Posts status updates.")
        .instructions("Post the update the user gives you with acme-ops.post-update.")
        .capability(Map.of("needs", List.of(Map.of("feature", "tool-use"))))
        .tool(PostUpdate.TOOL)
        .tool(Greet.TOOL)
        .set("conversationPolicy", Map.of("hitl", Map.of("tools", Map.of(
            "overrides", Map.of("acme-ops.post-update", "always_ask")))))
        .build();

    private StatusAgent() {}
  }
  ```

- **Scala**

  `src/main/scala/acmeops/agents/StatusAgent.scala`

  ```scala
  package acmeops.agents

  import acmeops.tools.{Greet, PostUpdate}
  import com.kindgi.pack.scaladsl._

  object StatusAgent {
    val agent: Agent = Agent("acme-ops.status-agent")
      .version("0.1.0")
      .name("Status Agent")
      .description("Posts status updates.")
      .instructions("Post the update the user gives you with acme-ops.post-update.")
      .capability(Map("needs" -> Seq(Map("feature" -> "tool-use"))))
      .tool(PostUpdate.tool)
      .tool(Greet.tool)
      .set("conversationPolicy", Map("hitl" -> Map("tools" -> Map(
        "overrides" -> Map("acme-ops.post-update" -> "always_ask")))))
      .build()
  }
  ```

`hitl.tools` turns tool gates on for this agent. Each tool then gets a gate:

- **`always_ask`**: every call waits for a decision.
- **`never_ask`**: calls run without asking.
- **No override:** a tool declared read-only, with `mutating: false`, runs without asking. Any other tool asks. So `hitl: { tools: {} }` asks before every tool that can change something. Here `acme-ops.greet`, which is read-only, never asks.

An agent without `hitl.tools` has no tool gates, unless your workspace has [approval rules](https://docs.kindgi.com/v0.5.0/guides/approvals/approval-rules-for-every-agent.md). In an Ask from a person a site signs in, every call of a tool that isn't read-only waits for a yes, whatever `hitl.tools` says (see [What an Ask may do](https://docs.kindgi.com/v0.5.0/sites/wordpress.md#what-an-ask-may-do)).

### Someone other than whoever asked

**A tool call's approval names who asked:** the person the turn runs for, in `requestedBy` (`user:<id>`, or `service_account:<id>` for an app's key). It stays the same when someone else's decision resumes the turn.

**By default, that person may approve it too.** `hitl.separateApprover` makes the approval need someone else (four eyes):

- **`true`** applies to every gated tool.
- **Per tool,** set `overrides`, keyed by tool id as in `tools`. An optional `default` covers the tools you don't name.

```ts
conversationPolicy: {
  hitl: {
    tools: { overrides: { 'acme-ops.post-update': 'always_ask' } },
    separateApprover: { overrides: { 'acme-ops.post-update': true } },
  },
},
```

Every pack language sets it the same way: Python's `conversation_policy`, and the map Java and Scala pass to `set("conversationPolicy", …)`, take the same keys, `separateApprover` included.

**When four eyes is on:**

- **The person who asked isn't offered Approve.** That holds on the console's approval page, in its inbox's keyboard decisions ("You asked for this, so someone else approves it."), and on the Kindgi plugin for WordPress's Approvals tab.

- **The API refuses their approve:**

  ```json excerpt
  {
    "error": {
      "code": "separate-approver-required",
      "message": "Approval ecaaa545-… was asked for by you: someone else must approve it",
      "details": { "approvalId": "ecaaa545-…" },
      "requestId": "…"
    }
  }
  ```

- **They may still reject their own request.**

- **A turn that's already waiting** keeps the rule it parked with.

- **It compares people.** A turn started with an app's key names the key's service account (`service_account:<id>`), so a person holding that key can still approve the call as themselves.

- **With `ask_on_first_use`,** only a tool's first call is asked, and someone other than whoever asked approves that one.

- **The template's tools:** the store agent sets `separateApprover` per tool, and `defineAgent` refuses an override naming a tool the agent doesn't have, so a mistyped id never leaves the real tool without four eyes.

**The `woocommerce` template's store agent turns it on** for `<pack>.woo.refund-large`, `<pack>.woo.set-price` and `<pack>.woo.publish-product` (`acme-shop.woo.refund-large` in a pack named `acme-shop`).

## When no person started the run

**A tool can ask for a person's yes on every call no person is there to see:** it declares `needsApprovalWhenUnattended: true`. Declare it on a tool that moves money or changes something people rely on. Then, in a run no person started:

- **An agent's call of the tool waits** for a reviewer, as `always_ask` does. The agent's `hitl` can't lift it, nor can your workspace's [approval rules](https://docs.kindgi.com/v0.5.0/guides/approvals/approval-rules-for-every-agent.md): agents are reused, and the tool's author knows what it does. Only the schedule, webhook trigger or mail trigger that started the run can, by listing the tool ([Tools allowed unattended](https://docs.kindgi.com/v0.5.0/guides/approvals/ask-before-a-tool-runs.md#tools-allowed-unattended)).
- **A flow's tool step that calls it fails,** as [In a flow](https://docs.kindgi.com/v0.5.0/guides/approvals/ask-before-a-tool-runs.md#in-a-flow) shows.

In a run a person started, the agent's own gates decide, as for any tool. Absent means `false`. Here `acme-ops.post-update`, which posts in public, declares it:

- **TypeScript**

  ```ts excerpt
  // in tools/post-update/index.ts
    effects: [{ kind: 'external-side-effect', resource: 'external:status-page' }],
    needsApprovalWhenUnattended: true,
  ```

- **Python**

  ```python excerpt
  # in tools/post_update.py
  @tool(
      id="acme-ops.post-update",
      effects=[{"kind": "external-side-effect", "resource": "external:status-page"}],
      needs_approval_when_unattended=True,
  )
  ```

- **Java**

  ```java excerpt
  // in src/main/java/acmeops/tools/PostUpdate.java
        .effect("external-side-effect", "external:status-page")
        .needsApprovalWhenUnattended(true)
  ```

- **Scala**

  ```scala excerpt
  // in src/main/scala/acmeops/tools/PostUpdate.scala
      .effect("external-side-effect", "external:status-page")
      .needsApprovalWhenUnattended(true)
  ```

**How a run started** is on the run, as `startedBy`:

| `startedBy`                            | Who started it                                                                                            | Counts as a person |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------ |
| `person`                               | A person: signed in (the console, the chat of the Kindgi plugin for WordPress), or with their own API key | Yes                |
| `key`                                  | A service account's key                                                                                   | No                 |
| `schedule`, `webhook`, `event`, `mail` | A trigger                                                                                                 | No                 |

- **A key acts as its person.** A script on your own key starts runs as you, and they count as yours. For automation, use a [service account's key](https://docs.kindgi.com/v0.5.0/deploy/people-and-keys.md#service-accounts): its runs count as unattended.
- **A flow's agent step, and any run a run starts,** started the way its run did.
- **A run from before your runtime recorded it** reads as its trigger's kind when a trigger started it, and has no `startedBy` otherwise. Either way it counts as unattended: it never reads as a person's.
- **The `woocommerce` template declares it** on `<pack>.woo.refund`, `<pack>.woo.refund-large`, `<pack>.woo.set-price` and `<pack>.woo.publish-product`. A staff member's refund in the store chat runs as the agent says; one an order webhook's run asks for waits for a person.

### Tools allowed unattended

A schedule, a webhook trigger or a mail trigger can list tools its runs call without waiting for a person's yes: its tools allowed unattended (`toolsAllowedUnattended`, runtime 0.2.0 or later). List a tool there when the trigger is there to run it, such as a nightly status post. A mail trigger's list needs more care, since anyone who can write to the mailbox writes its runs' input ([List a tool](https://docs.kindgi.com/v0.5.0/guides/runs/start-runs-from-new-mail.md#list-a-tool)):

```sh
pnpm exec kindgi schedules update <schedule-id> --allow-unattended=acme-ops.post-update
```

- **It lifts only the wait for a person.** In the trigger's runs, a listed tool's call goes as the agent's own `hitl` and your workspace's [approval rules](https://docs.kindgi.com/v0.5.0/guides/approvals/approval-rules-for-every-agent.md) say, as in a run a person started: a rule that asks before the tool runs still asks. The run still acts as the trigger's owner, so the call is checked against their access. It covers both kinds of tool that ask: one that declares `needsApprovalWhenUnattended`, and one that uses a person's connection, reads included ([Use a connection in a tool](https://docs.kindgi.com/v0.5.0/guides/connections/use-a-connection.md#approvals)). Such a run still can't wait for anyone to connect ([When the connection is missing](https://docs.kindgi.com/v0.5.0/guides/connections/use-a-connection.md#when-the-connection-is-missing)).
- **Each entry is one tool's id,** with no wildcard or pattern, at most 100 of them. Each must be a tool the trigger's agent or flow calls: the agent's tools, or a flow's tool steps and its agent steps' tools, its subflows' included. That's checked when you set it, and a tool it doesn't call is refused (`400 bad-input`). A schedule that starts [Improve runs](https://docs.kindgi.com/v0.5.0/guides/improve/improve-an-agent.md) calls no tools, so its list stays empty.
- **Only the trigger's runs have it.** A run takes the list as it is when the trigger fires, and the runs that run starts, such as a flow's agent steps, take the same list. A run that waits and resumes keeps the list it started with, so a change reaches the next fire. Any other run has none: a person's, a key's, an event trigger's. `POST /v1/runs` can't set one.
- **Setting it takes `admin` on the trigger's project,** clearing it too. A caller with only `write` is answered `403 permission-denied`.
- **With the CLI,** it's `--allow-unattended=<tool-id>`, once for each tool, on `kindgi schedules create` and `update`, on `kindgi webhooks register` and `update`, and on `kindgi mail-triggers create` and `update`. On `update` it replaces the list, `--allow-unattended=none` clears it, and leaving it out keeps it. From your app, it's `toolsAllowedUnattended` on `register` and `update` of `schedules`, `webhooks` and `mailTriggers` (`tools_allowed_unattended` in Python). `kindgi schedules get`, `kindgi webhooks get` and `kindgi mail-triggers get` show it when it isn't empty. A schedule's or webhook trigger's page in the console shows it as **Tools allowed unattended**, or "None". The console doesn't change it.
- **A new owner keeps it.** After a take-over, the trigger's runs call the tools it lists as the new owner, with their access, and the CLI says so, with the line that clears it. A take-over can't change the list: use `update`. Changing the list keeps the owner, as every change does ([Shared or personal](https://docs.kindgi.com/v0.5.0/guides/runs/run-on-a-schedule.md#shared-or-personal)).
- **A schedule pointed at another agent or flow** must name its list again, as it is or `none` (`[]` from your app), or the change is refused (`400 bad-input`). A new version of the same one keeps the list. Pointed at Improve runs, it must name `none`, since an Improve run calls no tools.
- **Each change is recorded as evidence,** `trigger-tools-allowed-unattended-set`, with the list before and after and what the trigger runs. It's kept for good, on [legal hold](https://docs.kindgi.com/v0.5.0/deploy/retention.md#on-legal-hold). A take-over's `trigger-owner-set` records the list it kept.

## Become a reviewer

Approvals are decided by **reviewers**. Under `kindgi dev` you're one already, an `admin`. On any other runtime, register yourself once, as the user your API token belongs to. It covers any token of yours, a console sign-in too:

- **TypeScript**

  ```sh
  pnpm exec kindgi reviewers register --spec='{"role":"standard","displayName":"Ada Lovelace"}'
  ```

- **Python**

  ```sh
  uv run kindgi reviewers register --spec='{"role":"standard","displayName":"Ada Lovelace"}'
  ```

- **Java**

  ```sh
  ./kindgiw reviewers register --spec='{"role":"standard","displayName":"Ada Lovelace"}'
  ```

- **Scala**

  ```sh
  ./kindgiw reviewers register --spec='{"role":"standard","displayName":"Ada Lovelace"}'
  ```

`--spec` is the reviewer: its `role` and, optionally, a `displayName`. It prints the registered reviewer, as JSON.

With authorization on, registering needs `admin` on the workspace ([Who can do what](https://docs.kindgi.com/v0.5.0/deploy/people-and-keys.md#who-can-do-what)): if you aren't a tenant admin, a tenant admin registers you, with your `userId`.

[Decide an approval](https://docs.kindgi.com/v0.5.0/guides/approvals/decide-an-approval.md) covers reviewers and their roles.

## Run it

Start a turn that asks for the tool. `--agent` is the agent, and `--input` its input:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --agent=acme-ops.status-agent --input='{"userMessage":"Checkout is back to normal."}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --agent=acme-ops.status-agent --input='{"userMessage":"Checkout is back to normal."}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --agent=acme-ops.status-agent --input='{"userMessage":"Checkout is back to normal."}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --agent=acme-ops.status-agent --input='{"userMessage":"Checkout is back to normal."}'
  ```

```json excerpt
{
  "id": "0c01376f-1566-4a0d-ba6f-95f4efff4758",
  …
  "flowId": "agent.turn",
  "flowVersion": "1.1.0",
  "status": "suspended",
  …
}
```

The run is `suspended`: the model asked for `acme-ops.post-update`, and the call waits. `kindgi runs start` returns as soon as the run waits. It doesn't block until someone decides.

## See what it waits for

List the pending approvals. `--status=pending` lists only the ones still `pending`:

- **TypeScript**

  ```sh
  pnpm exec kindgi approvals list --status=pending
  ```

- **Python**

  ```sh
  uv run kindgi approvals list --status=pending
  ```

- **Java**

  ```sh
  ./kindgiw approvals list --status=pending
  ```

- **Scala**

  ```sh
  ./kindgiw approvals list --status=pending
  ```

```json excerpt
{
  "data": [
    {
      "id": "416c22ba-73f9-45b7-999d-c1d5938758f7",
      …
      "subjectKind": "tool-call:pending",
      "subjectRef": {
        …
        "toolId": "acme-ops.post-update",
        "agentId": "acme-ops.status-agent",
        …
        "arguments": {
          "message": "Checkout is back to normal."
        },
        …
      },
      "requiredRole": "standard",
      "status": "pending",
      "title": "HITL review: acme-ops.post-update",
      …
      "provenanceRef": {
        "runId": "0c01376f-1566-4a0d-ba6f-95f4efff4758"
      },
      …
    }
  ],
  "hasMore": false
}
```

The approval names the tool and the exact arguments the model chose, and `provenanceRef.runId` is the run that waits.

## Approve it

Decide it by the approval's `id`. `--decision=approve` lets the tool run, and `--rationale` is kept with the decision:

- **TypeScript**

  ```sh
  pnpm exec kindgi approvals complete 416c22ba-73f9-45b7-999d-c1d5938758f7 --decision=approve --rationale="Confirmed with the on-call engineer"
  ```

- **Python**

  ```sh
  uv run kindgi approvals complete 416c22ba-73f9-45b7-999d-c1d5938758f7 --decision=approve --rationale="Confirmed with the on-call engineer"
  ```

- **Java**

  ```sh
  ./kindgiw approvals complete 416c22ba-73f9-45b7-999d-c1d5938758f7 --decision=approve --rationale="Confirmed with the on-call engineer"
  ```

- **Scala**

  ```sh
  ./kindgiw approvals complete 416c22ba-73f9-45b7-999d-c1d5938758f7 --decision=approve --rationale="Confirmed with the on-call engineer"
  ```

```json excerpt
{
  "kind": "terminal",
  "approval": {
    "id": "416c22ba-73f9-45b7-999d-c1d5938758f7",
    …
    "status": "approved",
    …
  },
  "decision": {
    …
    "decision": "approve",
    "rationale": "Confirmed with the on-call engineer",
    "reviewerRoleAtDecision": "standard",
    …
  },
  "waitpointResolved": true,
  "resume": { "kind": "ok" }
}
```

The run continues in the same call: the tool runs and the agent finishes its turn. The tool runs at the version the turn started with, even when a newer version in the agent's range appeared while it waited.

If that version is gone by then, the turn can't go on. Under `kindgi dev`, that happens when the tool's file now says another version. The approval still stands, its answer says so in `resume`, and the run fails:

```json excerpt
"resume": {
  "kind": "failed",
  "code": "tool-version-unresolvable",
  "message": "Tool \"acme-ops.post-update\": this turn started with version 0.1.0, which is no longer registered; it doesn't run another version mid-turn."
}
```

Its journal says who decided which approval. `kindgi runs journal <run-id>` shows it:

```json excerpt
{"sequence": 27, "kind": "wait.resumed", "nodeId": "agent-loop", "payload": {"value": {"decided": "approve", "decidedBy": "user:26fc414a-b49f-4e1f-a022-162865770178", "rationale": "Confirmed with the on-call engineer", "approvalId": "a281a631-b48b-4868-b138-213a433e8c40"}, …}, …}
```

Get the run by its id to see the tool's result:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs get 0c01376f-1566-4a0d-ba6f-95f4efff4758
  ```

- **Python**

  ```sh
  uv run kindgi runs get 0c01376f-1566-4a0d-ba6f-95f4efff4758
  ```

- **Java**

  ```sh
  ./kindgiw runs get 0c01376f-1566-4a0d-ba6f-95f4efff4758
  ```

- **Scala**

  ```sh
  ./kindgiw runs get 0c01376f-1566-4a0d-ba6f-95f4efff4758
  ```

```json excerpt
{
  "id": "0c01376f-1566-4a0d-ba6f-95f4efff4758",
  …
  "status": "completed",
  …
      {
        "role": "tool",
        "content": {
          "posted": true,
          "postedAt": "2026-10-03T20:05:54.473Z"
        },
        …
```

## Reject it

With `--decision=reject`, the tool doesn't run. The agent gets the rejection as the tool's result, with your rationale, and finishes its turn. A real model can tell the user why nothing was posted. `<approval-id>` is the approval's `id`:

- **TypeScript**

  ```sh
  pnpm exec kindgi approvals complete <approval-id> --decision=reject --rationale="Not confirmed yet"
  ```

- **Python**

  ```sh
  uv run kindgi approvals complete <approval-id> --decision=reject --rationale="Not confirmed yet"
  ```

- **Java**

  ```sh
  ./kindgiw approvals complete <approval-id> --decision=reject --rationale="Not confirmed yet"
  ```

- **Scala**

  ```sh
  ./kindgiw approvals complete <approval-id> --decision=reject --rationale="Not confirmed yet"
  ```

It prints the recorded decision, as approving does. The run's messages then hold the rejection as the tool's result:

```json excerpt
      {
        "role": "tool",
        "content": {
          "status": "rejected",
          "rationale": "Not confirmed yet"
        },
        …
```

The run completes; it doesn't fail. The turn's [provenance](https://docs.kindgi.com/v0.5.0/guides/observability/trace-an-answer.md) keeps the rejected call: its `tool-call`, and a `tool-result` that is the rejection the model read.

## In a flow

When the agent is a step of a flow, the flow waits with it: the flow run is `suspended` too, and its journal shows the step waiting. `<flow-run-id>` is the flow run's id:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs journal <flow-run-id>
  ```

- **Python**

  ```sh
  uv run kindgi runs journal <flow-run-id>
  ```

- **Java**

  ```sh
  ./kindgiw runs journal <flow-run-id>
  ```

- **Scala**

  ```sh
  ./kindgiw runs journal <flow-run-id>
  ```

```json excerpt
  {
    "sequence": 6,
    "kind": "wait.suspended",
    "nodeId": "respond",
    …
  }
```

The approval's `provenanceRef.runId` is the agent step's own run, not the flow run. Once the approval is decided, the agent's turn finishes and the flow continues from that step.

**A tool step can't wait for a person.** In a run no person started, a step whose tool needs a person's yes fails, and the tool never runs: one that declares `needsApprovalWhenUnattended`, or one that uses a person's connection ([Use a connection in a tool](https://docs.kindgi.com/v0.5.0/guides/connections/use-a-connection.md#approvals)). The run's `failureMessage` says why:

```text
needs-a-person: tool "acme-ops.post-update" needs a person's yes when no person started the run (needsApprovalWhenUnattended), and a flow's tool step can't wait for one. Call it from an agent step instead: there the call waits for a person to approve it.
```

For a tool that uses a person's connection, the reason in parentheses reads `it uses a person's connection`. Call such a tool from an agent step, whose call waits for a reviewer. In a schedule's, a webhook trigger's or a mail trigger's run, the message adds that the trigger could list the tool instead ([Tools allowed unattended](https://docs.kindgi.com/v0.5.0/guides/approvals/ask-before-a-tool-runs.md#tools-allowed-unattended)). A schedule's run's message ends:

```text
Or a project admin can list it in the schedule's toolsAllowedUnattended, so its runs call it without one.
```

In a mail trigger's run, two more reasons can stop a step: `a mail started the run, and it changes things` (the tool isn't `mutating: false`), and `a mail started the run, and it uses a connection of the run's own mailbox` ([What waits for a yes](https://docs.kindgi.com/v0.5.0/guides/runs/start-runs-from-new-mail.md#what-waits-for-a-yes)).

## Next

- [Decide an approval](https://docs.kindgi.com/v0.5.0/guides/approvals/decide-an-approval.md): escalate, withdraw, roles, and deciding from your app.
- [Set approval rules for every agent](https://docs.kindgi.com/v0.5.0/guides/approvals/approval-rules-for-every-agent.md).
- [CLI reference: `kindgi approvals`](https://docs.kindgi.com/v0.5.0/reference/cli/approvals.md).
