# Decide an approval

> Register reviewers, find what's waiting, and approve, reject, escalate or withdraw it from the CLI, the console or your app.

This guide gets a waiting approval decided, so the run that waits on it goes on.

A run that waits for a person creates an **approval**, such as a [gated tool call](https://docs.kindgi.com/v0.1.8/guides/approvals/ask-before-a-tool-runs.md) or a [long conversation](https://docs.kindgi.com/v0.1.8/guides/approvals/ask-after-n-turns.md). A **reviewer** decides it, and the run moves on. [Approvals reference](https://docs.kindgi.com/v0.1.8/guides/approvals/approvals-reference.md) has every field, decision, code and console page.

## Before you start

- **A run that waits for an approval.** [Ask before a tool runs](https://docs.kindgi.com/v0.1.8/guides/approvals/ask-before-a-tool-runs.md) makes one.
- **`kindgi dev` running** in your pack's directory, with Kindgi 0.1's CLI.
- **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.

## Reviewers and roles

Only reviewers see approvals, so whoever decides must be one.

Under `kindgi dev`, you're one already. When it starts on a database where you aren't a reviewer yet, it registers you, the user of its token, as an `admin`. List the reviewers:

- **TypeScript**

  ```sh
  pnpm exec kindgi reviewers list
  ```

- **Python**

  ```sh
  uv run kindgi reviewers list
  ```

- **Java**

  ```sh
  ./kindgiw reviewers list
  ```

- **Scala**

  ```sh
  ./kindgiw reviewers list
  ```

```json
{
  "data": [
    {
      "id": "6c11cfb1-d0c2-46dd-a3a0-e859a57b5da0",
      "tenantId": "9126df1e-094b-496d-9ea8-7daab93c3f4f",
      "userId": "26fc414a-b49f-4e1f-a022-162865770178",
      "role": "admin",
      "createdAt": "2026-10-05T20:10:59.747Z"
    }
  ],
  "hasMore": false
}
```

On any other runtime, register each person who reviews, with a role. Run it as that person: the reviewer is the user the API token belongs to. That covers any token of theirs, an API key or a console sign-in:

- **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, as JSON:

- `role` is `standard`, `senior` or `admin`.
- `displayName` is the name the console shows.

It prints the new reviewer:

```json
{
  "id": "b4bc07d5-1640-4c4c-9cbe-710aa6ca7d36",
  "tenantId": "3a9fb26d-9782-4bdb-a4f9-3c97d2644b72",
  "userId": "b9b6a8af-84ee-4514-a609-a76230750869",
  "role": "standard",
  "displayName": "Ada Lovelace",
  "createdAt": "2026-10-03T19:55:32.200Z"
}
```

The roles rank `standard`, `senior`, `admin`. A reviewer sees and decides the approvals that need their role or a lower one. Registering again changes the role. A tenant admin can also add reviewers in the console, on [the Reviewers page](https://docs.kindgi.com/v0.1.8/guides/approvals/approvals-reference.md#the-reviewers-page).

## Reviewer email

Email lets reviewers hear about an approval without watching the console. It's optional, and off by default.

Set `KINDGI_REVIEWER_EMAIL=on` in the runtime's environment. It sends through the emailed sign-in link's server, so `KINDGI_AUTH_EMAIL_SMTP_URL` and `KINDGI_AUTH_EMAIL_FROM` must be set too. Turned on without them, the runtime refuses to start.

The runtime then emails reviewers when an approval starts waiting, and when a waiting approval is assigned:

- **Who:** the people the approvals list would show it to: its assigned reviewer, or else every active reviewer of its required role or above. Only those who may read its project are emailed.
- **Assigned later:** a reviewer an approval is assigned to after it started waiting is emailed then, unless they were already emailed about it.
- **What:** its title, project, required role and when it was asked, and a link to it in the console. Never what it's about: that stays behind sign-in.
- **How often:** a reviewer's first email goes at once. Anything more for them within five minutes comes as one digest at the end of the five minutes. An approval decided in the meantime is left out.

One approval, as a reviewer gets it, with the ids elided:

```text excerpt
Subject: Approval needed: Refund over the limit

An approval is waiting for you.

Refund over the limit
  Project: Default
  Needs: a standard reviewer or above
  Asked: 2026-10-10 01:37 UTC
  …/console/approvals/…

Sign in to decide: what each approval is about is only in the console.

You're getting this because you're a reviewer in Kindgi.
```

Several in one digest have the subject "2 approvals are waiting for you", with the same lines for each. For a system of your own, an [`approval.requested` webhook](https://docs.kindgi.com/v0.1.8/guides/webhooks/webhook-events.md#approvals) carries the same facts.

## Find what's waiting

The approval's id is what you decide it by.

List the approvals waiting for a reviewer, then read one:

- **TypeScript**

  ```sh
  pnpm exec kindgi approvals list --status=pending
  pnpm exec kindgi approvals get <approval-id>
  ```

- **Python**

  ```sh
  uv run kindgi approvals list --status=pending
  uv run kindgi approvals get <approval-id>
  ```

- **Java**

  ```sh
  ./kindgiw approvals list --status=pending
  ./kindgiw approvals get <approval-id>
  ```

- **Scala**

  ```sh
  ./kindgiw approvals list --status=pending
  ./kindgiw approvals get <approval-id>
  ```

* `--status=pending` lists the approvals nobody is assigned to yet, for any reviewer of their role. Without `--status`, the list includes decided approvals too.
* `<approval-id>` is an approval's `id`, from the list.

To see the approvals assigned to you, oldest first:

- **TypeScript**

  ```sh
  pnpm exec kindgi approvals list --status=assigned,in_review --assigned-to=me --order=asc
  ```

- **Python**

  ```sh
  uv run kindgi approvals list --status=assigned,in_review --assigned-to=me --order=asc
  ```

- **Java**

  ```sh
  ./kindgiw approvals list --status=assigned,in_review --assigned-to=me --order=asc
  ```

- **Scala**

  ```sh
  ./kindgiw approvals list --status=assigned,in_review --assigned-to=me --order=asc
  ```

* `--status` takes several statuses, comma-separated.
* `--assigned-to=me` keeps the approvals assigned to you. Assigning one moves it to `assigned`.
* `--order=asc` lists the oldest first.

Each approval says what waits, who may decide it and which run waits on it. [An approval's fields](https://docs.kindgi.com/v0.1.8/guides/approvals/approvals-reference.md#an-approvals-fields) lists them.

### What a run waits for

From a run, `kindgi runs resume <run-id>` says what it waits for, and how it goes on. `<run-id>` is the run's id:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs resume 45d986f1-8a8d-4341-a786-5ddcf239bb93
  ```

- **Python**

  ```sh
  uv run kindgi runs resume 45d986f1-8a8d-4341-a786-5ddcf239bb93
  ```

- **Java**

  ```sh
  ./kindgiw runs resume 45d986f1-8a8d-4341-a786-5ddcf239bb93
  ```

- **Scala**

  ```sh
  ./kindgiw runs resume 45d986f1-8a8d-4341-a786-5ddcf239bb93
  ```

```text
Run 45d986f1-8a8d-4341-a786-5ddcf239bb93 waits for approval b3fcf729-0137-430e-92a3-2ff10f4a6bde ("HITL review required for Chat desk"), for a standard reviewer or above. It continues once the approval is decided: kindgi approvals complete b3fcf729-0137-430e-92a3-2ff10f4a6bde --decision=approve (or --decision=reject).
```

That sentence goes to stderr. Its standard output is the same answer as JSON, for a script:

```json
{
  "kind": "approval",
  "runId": "45d986f1-8a8d-4341-a786-5ddcf239bb93",
  "status": "suspended",
  "approvals": [
    {
      "id": "b3fcf729-0137-430e-92a3-2ff10f4a6bde",
      "title": "HITL review required for Chat desk",
      "requiredRole": "standard",
      "status": "pending"
    }
  ],
  "alsoWaitsOn": []
}
```

Its exit code says which:

| Exit | The run                                                                              |
| ---- | ------------------------------------------------------------------------------------ |
| `0`  | isn't waiting: `Run … is completed: it isn't waiting, so there's nothing to resume.` |
| `3`  | waits for an approval, named in the sentence                                         |
| `4`  | waits on the runtime: a queued start, a child run, a retry, a lease                  |

Once the approval is decided, the run goes on by itself. From your app, `runs.get` answers the same as the run's [`waitingFor`](https://docs.kindgi.com/v0.1.8/guides/approvals/approvals-reference.md#a-runs-waitingfor).

In the console, a reviewer's **Inbox** lists every approval they may decide, as [The Inbox](https://docs.kindgi.com/v0.1.8/guides/approvals/approvals-reference.md#the-inbox) describes.

## Decide

Deciding the approval lets the run go on, or ends what it asked for.

- **TypeScript**

  ```sh
  pnpm exec kindgi approvals complete <approval-id> --decision=approve --rationale="Confirmed with the on-call engineer"
  ```

- **Python**

  ```sh
  uv run kindgi approvals complete <approval-id> --decision=approve --rationale="Confirmed with the on-call engineer"
  ```

- **Java**

  ```sh
  ./kindgiw approvals complete <approval-id> --decision=approve --rationale="Confirmed with the on-call engineer"
  ```

- **Scala**

  ```sh
  ./kindgiw approvals complete <approval-id> --decision=approve --rationale="Confirmed with the on-call engineer"
  ```

* `<approval-id>` is the approval's `id`.
* `--decision` is one of the four decisions in the table.
* `--rationale` (optional) is kept with the decision, and goes into the audit trail.

| Decision   | The approval                                         | The run                                                                                                                               |
| ---------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `approve`  | `approved`                                           | Continues: the tool runs, or the turn runs.                                                                                           |
| `reject`   | `rejected`                                           | A tool call: the tool doesn't run, and the agent gets the rejection as the tool's result. A turn: the run fails with `hitl-rejected`. |
| `escalate` | `escalated`, and a new approval for the next role up | Keeps waiting for the new approval.                                                                                                   |
| `withdraw` | `withdrawn`                                          | Ends: its turn fails with `hitl-withdrawn`.                                                                                           |

An approve, reject or withdraw resolves the run within the same call. So the run has moved on by the time the command returns. A decision can be refused, such as on an approval that's no longer waiting: [Decisions](https://docs.kindgi.com/v0.1.8/guides/approvals/approvals-reference.md#decisions) lists why.

In the console, a reviewer decides on the approval's page or in the Inbox, as [Deciding in the console](https://docs.kindgi.com/v0.1.8/guides/approvals/approvals-reference.md#deciding-in-the-console) shows.

### Escalate

Escalating asks a reviewer of the next role up instead. A `standard` reviewer who wants a second opinion escalates:

- **TypeScript**

  ```sh
  pnpm exec kindgi approvals complete 2134897a-d176-4ca8-b000-8bc00d79ad04 --decision=escalate --rationale="Needs sign-off from the incident lead"
  ```

- **Python**

  ```sh
  uv run kindgi approvals complete 2134897a-d176-4ca8-b000-8bc00d79ad04 --decision=escalate --rationale="Needs sign-off from the incident lead"
  ```

- **Java**

  ```sh
  ./kindgiw approvals complete 2134897a-d176-4ca8-b000-8bc00d79ad04 --decision=escalate --rationale="Needs sign-off from the incident lead"
  ```

- **Scala**

  ```sh
  ./kindgiw approvals complete 2134897a-d176-4ca8-b000-8bc00d79ad04 --decision=escalate --rationale="Needs sign-off from the incident lead"
  ```

It prints the escalated approval and the new one:

```json excerpt
{
  "kind": "escalated",
  "approval": {
    "id": "2134897a-d176-4ca8-b000-8bc00d79ad04",
    …
    "requiredRole": "standard",
    "status": "escalated",
    …
  },
  …
  "nextApproval": {
    "id": "a65cd23c-1bdb-47d8-9b3a-aaec36475a4f",
    …
    "requiredRole": "senior",
    "status": "pending",
    …
  },
  "waitpointResolved": false
}
```

The new approval needs a `senior` reviewer, so the `standard` reviewer no longer sees it. A `senior` or `admin` reviewer approves or rejects `a65cd23c-…`, and the run continues.

### Withdraw

Withdrawing closes an approval without deciding what it asked, when the request is moot. The run waiting on it can't go on, so it ends:

- **TypeScript**

  ```sh
  pnpm exec kindgi approvals complete <approval-id> --decision=withdraw --rationale="Asked by mistake"
  ```

- **Python**

  ```sh
  uv run kindgi approvals complete <approval-id> --decision=withdraw --rationale="Asked by mistake"
  ```

- **Java**

  ```sh
  ./kindgiw approvals complete <approval-id> --decision=withdraw --rationale="Asked by mistake"
  ```

- **Scala**

  ```sh
  ./kindgiw approvals complete <approval-id> --decision=withdraw --rationale="Asked by mistake"
  ```

Its turn fails with `hitl-withdrawn`: "The approval for tool call acme-desk.echo was withdrawn".

## From your app

A reviewer usually decides in your app, not in a terminal. The client lists the pending approvals and records the decision. The reviewer is the user the API token belongs to.

Ask your coding agent: "In my app, let a reviewer see the pending approvals and approve one." For reference, the code looks like this, with what it prints:

- **TypeScript**

  `review.ts`

  ```ts
  import { createClient } from '@kindgi/sdk/client';

  const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

  const pending = await kindgi.approvals.list({ status: 'pending' });
  for (const approval of pending.data) {
    console.log(approval.id, approval.title, approval.subjectRef);
  }

  const first = pending.data[0];
  if (first) {
    const result = await kindgi.approvals.decide(first.id, {
      decision: 'approve',
      rationale: 'Checked the dashboard',
    });
    console.log(result.approval.status, result.waitpointResolved);
  }
  ```

  It prints each pending approval, then the decision's outcome:

  ```text excerpt
  46f0f9ad-f22d-4a93-8337-25b7c32a7473 HITL review: acme-ops.post-update {
    callId: 'dev-echo-call-1',
    toolId: 'acme-ops.post-update',
    agentId: 'acme-ops.status-agent',
    …
    arguments: { message: 'Checkout is slow for some customers.' },
    …
  }
  approved true
  ```

- **Python**

  `review.py`

  ```python
  from kindgi.client import Kindgi

  kindgi = Kindgi()  # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

  pending = kindgi.approvals.list(status="pending")
  for approval in pending.data:
      print(approval.id, approval.title, approval.subject_ref)

  if pending.data:
      result = kindgi.approvals.complete(
          pending.data[0].id, decision="approve", rationale="Checked the dashboard"
      )
      print(result.approval.status, result.waitpoint_resolved)
  ```

  It prints each pending approval, then the decision's outcome:

  ```text excerpt
  0afd1543-fe4e-45ca-af34-2effc1095bab HITL review: acme-ops.post-update {'callId': 'dev-echo-call-1', 'toolId': 'acme-ops.post-update', 'agentId': 'acme-ops.status-agent', …, 'arguments': {'message': 'Search is back.'}, …}
  approved True
  ```

- **Java**

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

  ```java
  package com.acme.app;

  import com.kindgi.client.Kindgi;
  import com.kindgi.client.models.Approval;
  import com.kindgi.client.models.ApprovalCollectionPage;
  import com.kindgi.client.models.ApprovalStatus;
  import com.kindgi.client.models.CompleteApprovalBody;
  import com.kindgi.client.models.CompleteApprovalResult;
  import com.kindgi.client.resources.ApprovalsListParams;
  import java.util.List;

  public final class Review {
    public static void main(String[] args) {
      Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

      ApprovalCollectionPage pending = kindgi.approvals().list(
          ApprovalsListParams.builder().status(List.of(ApprovalStatus.PENDING)).build());
      for (Approval approval : pending.data()) {
        System.out.println(approval.id() + " " + approval.title() + " " + approval.subjectRef());
      }

      if (!pending.data().isEmpty()) {
        CompleteApprovalResult result = kindgi.approvals().complete(pending.data().get(0).id(),
            CompleteApprovalBody.builder()
                .decision(CompleteApprovalBody.Decision.APPROVE)
                .rationale("Checked the dashboard")
                .build());
        System.out.println(result.approval().status() + " " + result.waitpointResolved());
      }
    }
  }
  ```

  It prints each pending approval, then the decision's outcome:

  ```text excerpt
  6faa8787-ff70-476e-b68b-74044b3711cc HITL review: acme-ops.post-update {callId=dev-echo-call-1, toolId=acme-ops.post-update, agentId=acme-ops.status-agent, …, arguments={message=Checkout is slow for some customers.}, …}
  approved true
  ```

- **Scala**

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

  ```scala
  package acme.app

  import com.kindgi.client.Kindgi
  import com.kindgi.client.models.{ApprovalStatus, CompleteApprovalBody}
  import com.kindgi.client.resources.ApprovalsListParams
  import scala.jdk.CollectionConverters._

  object Review {
    def main(args: Array[String]): Unit = {
      val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

      val pending = kindgi.approvals().list(
        ApprovalsListParams.builder().status(List(ApprovalStatus.PENDING).asJava).build()).data.asScala
      for (approval <- pending) println(s"${approval.id} ${approval.title} ${approval.subjectRef}")

      for (first <- pending.headOption) {
        val result = kindgi.approvals().complete(first.id,
          CompleteApprovalBody.builder()
            .decision(CompleteApprovalBody.Decision.APPROVE)
            .rationale("Checked the dashboard")
            .build())
        println(s"${result.approval.status} ${result.waitpointResolved}")
      }
    }
  }
  ```

  It prints each pending approval, then the decision's outcome:

  ```text excerpt
  d7ae0dec-8c01-41f9-bc18-df650233847c HITL review: acme-ops.post-update {callId=dev-echo-call-1, toolId=acme-ops.post-update, agentId=acme-ops.status-agent, …, arguments={message=Search is back.}, …}
  approved true
  ```

The first pending approval is approved, and its run went on: `waitpointResolved` is true. [Listing approvals](https://docs.kindgi.com/v0.1.8/guides/approvals/approvals-reference.md#listing-approvals) has the list's other options, such as one project's approvals or one run's.

## Next

- [Approvals reference](https://docs.kindgi.com/v0.1.8/guides/approvals/approvals-reference.md)
- [Ask before a tool runs](https://docs.kindgi.com/v0.1.8/guides/approvals/ask-before-a-tool-runs.md)
- [Cancel a run](https://docs.kindgi.com/v0.1.8/guides/runs/cancel-a-run.md)
- [Webhook events and deliveries](https://docs.kindgi.com/v0.1.8/guides/webhooks/webhook-events.md#approvals)
