# Wait for an approval

> Stop a flow until a person approves what an agent step is about to do, then continue on their decision.

This guide stops a flow until a person approves what an agent step is about to do, then continues on their decision.

A flow waits for a person through an agent step. When the agent asks for a tool that needs approval, its turn stops, and the flow stops with it. A reviewer decides, and the flow continues from where it stopped.

## Before you start

- **The tools** `acme.get-order`, from [Write a flow](https://docs.kindgi.com/v0.1.9/guides/flows/write-a-flow.md), `acme.hold-order`, from [Pass data between steps](https://docs.kindgi.com/v0.1.9/guides/flows/pass-data-between-steps.md), and `acme.email-customer`, from [Join branches](https://docs.kindgi.com/v0.1.9/guides/flows/join-branches.md).
- **A runtime such as `kindgi dev`,** with Kindgi 0.1's CLI.
- **A registered model** that calls tools.

`acme.escalate-order` has an agent put an order on hold, then emails the customer if the hold went through. Every hold waits for a person.

## The agent

The agent's tool call is what waits. Ask your coding agent: "I want an agent that puts an order on hold, but only once a person approves." Kindgi's skills in your pack tell it how ([How it knows Kindgi](https://docs.kindgi.com/v0.1.9/start/coding-agents.md)). For reference, the agent looks like this. It gates `acme.hold-order` with `always_ask`, and returns a typed answer the flow can branch on:

- **TypeScript**

  `agents/order-holder/index.ts`

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

  const defined = defineAgent({
    id: 'acme.order-holder' as AgentId,
    version: '0.1.0' as Semver,
    name: 'Order holder',
    description: 'Puts an order on hold, after a person approves.',
    instructions:
      'Put order {{ input.orderId }} on hold with the acme.hold-order tool, reason "{{ input.reason }}". ' +
      'Set held to true if the tool ran, false if the call was rejected, and say why in summary.',
    capabilities: [{ needs: [{ feature: 'tool-use' as const }] }],
    tools: [{ id: 'acme.hold-order', version: '0.1.0' }],
    retrieval: [],
    guardrails: [],
    parameters: [],
    conversationPolicy: {
      hitl: { tools: { overrides: { 'acme.hold-order': 'always_ask' } } },
    },
    output: {
      schema: {
        type: 'object',
        properties: { held: { type: 'boolean' }, summary: { type: 'string' } },
        required: ['held', 'summary'],
      },
    },
    budget: { maxSteps: 4, maxCostUsd: 0.05, maxWallMs: 60_000 },
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);

  export default defined.value;
  ```

- **Python**

  `tools/hold_order.py`

  ```python
  from typing import Literal

  from pydantic import BaseModel, Field

  from kindgi import tool


  class HoldInput(BaseModel):
      order_id: str = Field(alias="orderId")
      reason: str


  class Held(BaseModel):
      order_id: str = Field(alias="orderId")
      status: Literal["held"] = "held"


  @tool(id="acme.hold-order")  # no mutating=False: it writes, so a dry run stops before it
  def hold_order(input: HoldInput) -> Held:
      """Puts an order on hold for a person to review."""
      return Held(orderId=input.order_id)
  ```

  `agents/order_holder.py`

  ```python
  from pydantic import BaseModel

  from kindgi import Agent

  from ..tools.hold_order import hold_order


  class HoldResult(BaseModel):
      held: bool
      summary: str


  order_holder = Agent(
      id="acme.order-holder",
      version="0.1.0",
      name="Order holder",
      description="Puts an order on hold, after a person approves.",
      instructions=(
          'Put order {{ input.orderId }} on hold with the acme.hold-order tool, reason "{{ input.reason }}". '
          "Set held to true if the tool ran, false if the call was rejected, and say why in summary."
      ),
      capabilities=[{"needs": [{"feature": "tool-use"}]}],
      tools=[hold_order],
      conversation_policy={"hitl": {"tools": {"overrides": {"acme.hold-order": "always_ask"}}}},
      output=HoldResult,
      budget={"maxSteps": 4, "maxCostUsd": 0.05, "maxWallMs": 60_000},
  )
  ```

- **Java**

  `src/main/java/acme/agents/OrderHolder.java`

  ```java
  package acme.agents;

  import com.kindgi.pack.Agent;
  import java.util.List;
  import java.util.Map;

  public final class OrderHolder {
    public static final Agent AGENT = Agent.define("acme.order-holder")
        .version("0.1.0")
        .name("Order holder")
        .description("Puts an order on hold, after a person approves.")
        .instructions("Put order {{ input.orderId }} on hold with the acme.hold-order tool, reason \"{{ input.reason }}\". "
            + "Set held to true if the tool ran, false if the call was rejected, and say why in summary.")
        .capability(Map.of("needs", List.of(Map.of("feature", "tool-use"))))
        .tool("acme.hold-order", "0.1.0")
        .set("conversationPolicy",
            Map.of("hitl", Map.of("tools", Map.of("overrides", Map.of("acme.hold-order", "always_ask")))))
        .set("output", Map.of("schema", Map.of(
            "type", "object",
            "properties", Map.of("held", Map.of("type", "boolean"), "summary", Map.of("type", "string")),
            "required", List.of("held", "summary"))))
        .set("budget", Map.of("maxSteps", 4, "maxCostUsd", 0.05, "maxWallMs", 60_000))
        .build();

    private OrderHolder() {}
  }
  ```

- **Scala**

  `src/main/scala/acme/agents/OrderHolder.scala`

  ```scala
  package acme.agents

  import com.kindgi.pack.scaladsl._

  object OrderHolder {
    val agent: Agent = Agent("acme.order-holder")
      .version("0.1.0")
      .name("Order holder")
      .description("Puts an order on hold, after a person approves.")
      .instructions("Put order {{ input.orderId }} on hold with the acme.hold-order tool, reason \"{{ input.reason }}\". " +
        "Set held to true if the tool ran, false if the call was rejected, and say why in summary.")
      .capability(Map("needs" -> Seq(Map("feature" -> "tool-use"))))
      .tool("acme.hold-order", "0.1.0")
      .set("conversationPolicy",
        Map("hitl" -> Map("tools" -> Map("overrides" -> Map("acme.hold-order" -> "always_ask")))))
      .set("output", Map("schema" -> Map(
        "type" -> "object",
        "properties" -> Map("held" -> Map("type" -> "boolean"), "summary" -> Map("type" -> "string")),
        "required" -> List("held", "summary"))))
      .set("budget", Map("maxSteps" -> 4, "maxCostUsd" -> 0.05, "maxWallMs" -> 60000))
      .build()
  }
  ```

[Ask before a tool runs](https://docs.kindgi.com/v0.1.9/guides/approvals/ask-before-a-tool-runs.md) covers the gates: `always_ask`, `never_ask`, and the default for tools that aren't read-only.

## The flow

Ask your coding agent: "I want a flow that runs that agent, then emails the customer if the order went on hold." For reference, the flow runs the agent as a step, then branches on its answer:

- **TypeScript**

  `flows/escalate-order/index.ts`

  ```ts
  import { defineFlow } from '@kindgi/sdk/define';

  const isHeld = {
    op: 'eq',
    left: { path: 'nodeOutputs.hold.output.held' },
    right: { literal: true },
  } as const;

  const defined = defineFlow({
    id: 'acme.escalate-order',
    version: '0.1.0',
    name: 'Escalate an order',
    description: 'Has an agent put an order on hold, once a person approves, then tells the customer.',
    nodes: [
      {
        id: 'order',
        kind: 'tool',
        ref: 'acme.get-order',
        inputMapping: { orderId: { path: 'runInput.orderId' } },
      },
      {
        id: 'hold',
        kind: 'agent',
        ref: 'acme.order-holder',
        inputMapping: {
          orderId: { path: 'runInput.orderId' },
          reason: { path: 'runInput.reason' },
        },
      },
      {
        id: 'email',
        kind: 'tool',
        ref: 'acme.email-customer',
        inputMapping: {
          customer: { path: 'nodeOutputs.order.customer' },
          status: { literal: 'held' },
        },
      },
    ],
    edges: [
      { id: 'e1', from: '$start', to: 'order' },
      { id: 'e2', from: 'order', to: 'hold' },
      { id: 'e3', from: 'hold', to: 'email', when: isHeld },
      { id: 'e4', from: 'hold', to: '$end', when: { op: 'not', child: isHeld } },
      { id: 'e5', from: 'email', to: '$end' },
    ],
    output: {
      mapping: {
        held: { path: 'nodeOutputs.hold.output.held' },
        summary: { path: 'nodeOutputs.hold.output.summary' },
        notified: { path: 'nodeOutputs.email.to' },
      },
    },
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);

  export default defined.value;
  ```

- **Python**

  `flows/escalate_order.py`

  ```python
  from kindgi import Flow

  IS_HELD = {
      "op": "eq",
      "left": {"path": "nodeOutputs.hold.output.held"},
      "right": {"literal": True},
  }

  escalate_order = Flow(
      id="acme.escalate-order",
      version="0.1.0",
      name="Escalate an order",
      description="Has an agent put an order on hold, once a person approves, then tells the customer.",
      nodes=[
          {
              "id": "order",
              "kind": "tool",
              "ref": "acme.get-order",
              "inputMapping": {"orderId": {"path": "runInput.orderId"}},
          },
          {
              "id": "hold",
              "kind": "agent",
              "ref": "acme.order-holder",
              "inputMapping": {
                  "orderId": {"path": "runInput.orderId"},
                  "reason": {"path": "runInput.reason"},
              },
          },
          {
              "id": "email",
              "kind": "tool",
              "ref": "acme.email-customer",
              "inputMapping": {
                  "customer": {"path": "nodeOutputs.order.customer"},
                  "status": {"literal": "held"},
              },
          },
      ],
      edges=[
          {"id": "e1", "from": "$start", "to": "order"},
          {"id": "e2", "from": "order", "to": "hold"},
          {"id": "e3", "from": "hold", "to": "email", "when": IS_HELD},
          {"id": "e4", "from": "hold", "to": "$end", "when": {"op": "not", "child": IS_HELD}},
          {"id": "e5", "from": "email", "to": "$end"},
      ],
      output={
          "mapping": {
              "held": {"path": "nodeOutputs.hold.output.held"},
              "summary": {"path": "nodeOutputs.hold.output.summary"},
              "notified": {"path": "nodeOutputs.email.to"},
          }
      },
  )
  ```

- **Java**

  `src/main/java/acme/flows/EscalateOrder.java`

  ```java
  package acme.flows;

  import com.kindgi.pack.Flow;
  import java.util.Map;

  public final class EscalateOrder {
    private static final Map<String, Object> IS_HELD = Map.of(
        "op", "eq",
        "left", Map.of("path", "nodeOutputs.hold.output.held"),
        "right", Map.of("literal", true));

    public static final Flow FLOW = Flow.define("acme.escalate-order")
        .version("0.1.0")
        .set("name", "Escalate an order")
        .set("description", "Has an agent put an order on hold, once a person approves, then tells the customer.")
        .node(Map.of("id", "order", "kind", "tool", "ref", "acme.get-order",
            "inputMapping", Map.of("orderId", Map.of("path", "runInput.orderId"))))
        .node(Map.of("id", "hold", "kind", "agent", "ref", "acme.order-holder",
            "inputMapping", Map.of(
                "orderId", Map.of("path", "runInput.orderId"),
                "reason", Map.of("path", "runInput.reason"))))
        .node(Map.of("id", "email", "kind", "tool", "ref", "acme.email-customer",
            "inputMapping", Map.of(
                "customer", Map.of("path", "nodeOutputs.order.customer"),
                "status", Map.of("literal", "held"))))
        .edge("e1", "$start", "order")
        .edge("e2", "order", "hold")
        .edge(Map.of("id", "e3", "from", "hold", "to", "email", "when", IS_HELD))
        .edge(Map.of("id", "e4", "from", "hold", "to", "$end", "when", Map.of("op", "not", "child", IS_HELD)))
        .edge("e5", "email", "$end")
        .set("output", Map.of("mapping", Map.of(
            "held", Map.of("path", "nodeOutputs.hold.output.held"),
            "summary", Map.of("path", "nodeOutputs.hold.output.summary"),
            "notified", Map.of("path", "nodeOutputs.email.to"))))
        .build();

    private EscalateOrder() {}
  }
  ```

- **Scala**

  `src/main/scala/acme/flows/EscalateOrder.scala`

  ```scala
  package acme.flows

  import com.kindgi.pack.scaladsl._

  object EscalateOrder {
    private val isHeld = Map(
      "op" -> "eq",
      "left" -> Map("path" -> "nodeOutputs.hold.output.held"),
      "right" -> Map("literal" -> true))

    val flow: Flow = Flow("acme.escalate-order")
      .version("0.1.0")
      .set("name", "Escalate an order")
      .set("description", "Has an agent put an order on hold, once a person approves, then tells the customer.")
      .node(Map("id" -> "order", "kind" -> "tool", "ref" -> "acme.get-order",
        "inputMapping" -> Map("orderId" -> Map("path" -> "runInput.orderId"))))
      .node(Map("id" -> "hold", "kind" -> "agent", "ref" -> "acme.order-holder",
        "inputMapping" -> Map(
          "orderId" -> Map("path" -> "runInput.orderId"),
          "reason" -> Map("path" -> "runInput.reason"))))
      .node(Map("id" -> "email", "kind" -> "tool", "ref" -> "acme.email-customer",
        "inputMapping" -> Map(
          "customer" -> Map("path" -> "nodeOutputs.order.customer"),
          "status" -> Map("literal" -> "held"))))
      .edge("e1", "$start", "order")
      .edge("e2", "order", "hold")
      .edge(Map("id" -> "e3", "from" -> "hold", "to" -> "email", "when" -> isHeld))
      .edge(Map("id" -> "e4", "from" -> "hold", "to" -> "$end", "when" -> Map("op" -> "not", "child" -> isHeld)))
      .edge("e5", "email", "$end")
      .set("output", Map("mapping" -> Map(
        "held" -> Map("path" -> "nodeOutputs.hold.output.held"),
        "summary" -> Map("path" -> "nodeOutputs.hold.output.summary"),
        "notified" -> Map("path" -> "nodeOutputs.email.to"))))
      .build()
  }
  ```

Nothing in the flow says "wait": the step waits because the agent's tool call does. After the decision, `e3` or `e4` reads the agent's answer, `nodeOutputs.hold.output.held`.

## Run it

Only reviewers can decide approvals. Under `kindgi dev` you're one already. On any other runtime, register yourself once:

- **TypeScript**

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

- **Python**

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

- **Java**

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

- **Scala**

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

`--spec` is the reviewer, as JSON: `role` is `standard`, `senior` or `admin`. It prints the registered reviewer, as JSON.

Then start the flow:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=acme.escalate-order --input='{"orderId":"A-200","reason":"Address looks wrong"}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=acme.escalate-order --input='{"orderId":"A-200","reason":"Address looks wrong"}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=acme.escalate-order --input='{"orderId":"A-200","reason":"Address looks wrong"}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=acme.escalate-order --input='{"orderId":"A-200","reason":"Address looks wrong"}'
  ```

`--flow` is the flow, and `--input` its input: the order and why to hold it. It prints the run, waiting:

```json excerpt
{
  "id": "7111dde4-bdd3-40a1-a6e8-2a550d0ad318",
  …
  "flowId": "acme.escalate-order",
  "flowVersion": "0.1.0",
  "status": "suspended",
  …
}
```

The run is `suspended`, and the command returns as soon as it waits. Nothing keeps running while it waits. The journal records where the run stopped, and the decision picks it up from there. `kindgi runs journal <run-id>` prints it:

```json excerpt
{"sequence": 5, "kind": "step.started", "nodeId": "hold", "payload": {"input": {"reason": "Address looks wrong", "orderId": "A-200"}}, …}
{"sequence": 6, "kind": "wait.suspended", "nodeId": "hold", "payload": {"nodeId": "hold", "tokenId": "child:5722bdd7-9c8c-4e57-a74c-ff5d1d3c46e2:25"}, …}
```

## Decide

The approval is what the reviewer decides. List the ones waiting:

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

`--status=pending` lists the ones nobody is assigned to yet. It prints the agent turn's approval:

```json excerpt
{
  "data": [
    {
      "id": "0eaa2820-0afe-4147-a017-fc8a4d1657cc",
      …
      "subjectKind": "tool-call:pending",
      "subjectRef": {
        …
        "toolId": "acme.hold-order",
        "agentId": "acme.order-holder",
        …
        "arguments": {
          "reason": "Address looks wrong",
          "orderId": "A-200"
        },
        …
      },
      "requiredRole": "standard",
      "status": "pending",
      …
      "provenanceRef": {
        "runId": "5722bdd7-9c8c-4e57-a74c-ff5d1d3c46e2"
      },
      …
    }
  ],
  "hasMore": false
}
```

`provenanceRef.runId` is the agent's turn, the step's own run. Its `parentRunId` is the flow's run. Approve it:

- **TypeScript**

  ```sh
  pnpm exec kindgi approvals complete 0eaa2820-0afe-4147-a017-fc8a4d1657cc --decision=approve --rationale="Checked with the customer"
  ```

- **Python**

  ```sh
  uv run kindgi approvals complete 0eaa2820-0afe-4147-a017-fc8a4d1657cc --decision=approve --rationale="Checked with the customer"
  ```

- **Java**

  ```sh
  ./kindgiw approvals complete 0eaa2820-0afe-4147-a017-fc8a4d1657cc --decision=approve --rationale="Checked with the customer"
  ```

- **Scala**

  ```sh
  ./kindgiw approvals complete 0eaa2820-0afe-4147-a017-fc8a4d1657cc --decision=approve --rationale="Checked with the customer"
  ```

The id is the approval's, `--decision=approve` approves it (`reject` turns it down), and `--rationale` is kept with it. It prints the recorded decision as JSON: the approval, the decision, and what it did to the run.

The decision resumes the agent's turn and then the flow, within the same call. By the time the command returns, the flow has finished. Read the flow's run, with its id:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs get 7111dde4-bdd3-40a1-a6e8-2a550d0ad318
  ```

- **Python**

  ```sh
  uv run kindgi runs get 7111dde4-bdd3-40a1-a6e8-2a550d0ad318
  ```

- **Java**

  ```sh
  ./kindgiw runs get 7111dde4-bdd3-40a1-a6e8-2a550d0ad318
  ```

- **Scala**

  ```sh
  ./kindgiw runs get 7111dde4-bdd3-40a1-a6e8-2a550d0ad318
  ```

```json excerpt
{
  …
  "status": "completed",
  …
  "output": {
    "held": true,
    "summary": "Order A-200 has been successfully placed on hold with the reason 'Address looks wrong'. The hold was accepted and processed.",
    "notified": "grace@example.com"
  }
}
```

## When the reviewer rejects

A rejection doesn't fail the step. The tool doesn't run, and the agent gets the rejection, with the rationale, as the tool's result. Then it answers. Here it answers `held: false`, so `e4` ends the run without the email:

```json
{
  "held": false,
  "summary": "The hold request was rejected. The system determined that order A-100 is not a duplicate order, so the hold could not be placed with the reason 'Duplicate order.'"
}
```

That's why the agent returns a typed answer: the flow decides what comes next from a field, not from the wording of a sentence.

## Stop waiting

`kindgi runs cancel <run-id>` on the flow's run cancels it and the agent's turn. It also withdraws the approval the turn waited on, as [Cancel a run](https://docs.kindgi.com/v0.1.9/guides/runs/cancel-a-run.md#approvals-of-a-cancelled-run) describes.

## Next

- [Decide an approval](https://docs.kindgi.com/v0.1.9/guides/approvals/decide-an-approval.md)
- [Ask before a tool runs](https://docs.kindgi.com/v0.1.9/guides/approvals/ask-before-a-tool-runs.md)
- [Branch a flow](https://docs.kindgi.com/v0.1.9/guides/flows/branch-a-flow.md)
