Decide an approval
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 or a long conversation. A reviewer decides it, and the run moves on. Approvals reference has every field, decision, code and console page.
Before you start
Section titled “Before you start”- A run that waits for an approval. Ask before a tool runs makes one.
kindgi devrunning 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_URLandKINDGI_API_TOKENset.
Reviewers and roles
Section titled “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:
pnpm exec kindgi reviewers listuv run kindgi reviewers list./kindgiw reviewers list./kindgiw reviewers list{ "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:
pnpm exec kindgi reviewers register --spec='{"role":"standard","displayName":"Ada Lovelace"}'uv run kindgi reviewers register --spec='{"role":"standard","displayName":"Ada Lovelace"}'./kindgiw reviewers register --spec='{"role":"standard","displayName":"Ada Lovelace"}'./kindgiw reviewers register --spec='{"role":"standard","displayName":"Ada Lovelace"}'--spec is the reviewer, as JSON:
roleisstandard,seniororadmin.displayNameis the name the console shows.
It prints the new reviewer:
{ "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.
Reviewer email
Section titled “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:
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
carries the same facts.
Find what's waiting
Section titled “Find what's waiting”The approval's id is what you decide it by.
List the approvals waiting for a reviewer, then read one:
pnpm exec kindgi approvals list --status=pendingpnpm exec kindgi approvals get <approval-id>uv run kindgi approvals list --status=pendinguv run kindgi approvals get <approval-id>./kindgiw approvals list --status=pending./kindgiw approvals get <approval-id>./kindgiw approvals list --status=pending./kindgiw approvals get <approval-id>--status=pendinglists 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'sid, from the list.
To see the approvals assigned to you, oldest first:
pnpm exec kindgi approvals list --status=assigned,in_review --assigned-to=me --order=ascuv run kindgi approvals list --status=assigned,in_review --assigned-to=me --order=asc./kindgiw approvals list --status=assigned,in_review --assigned-to=me --order=asc./kindgiw approvals list --status=assigned,in_review --assigned-to=me --order=asc--statustakes several statuses, comma-separated.--assigned-to=mekeeps the approvals assigned to you. Assigning one moves it toassigned.--order=asclists the oldest first.
Each approval says what waits, who may decide it and which run waits on it. An approval's fields lists them.
What a run waits for
Section titled “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:
pnpm exec kindgi runs resume 45d986f1-8a8d-4341-a786-5ddcf239bb93uv run kindgi runs resume 45d986f1-8a8d-4341-a786-5ddcf239bb93./kindgiw runs resume 45d986f1-8a8d-4341-a786-5ddcf239bb93./kindgiw runs resume 45d986f1-8a8d-4341-a786-5ddcf239bb93Run 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:
{ "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.
In the console, a reviewer's Inbox lists every approval they may decide, as The Inbox describes.
Decide
Section titled “Decide”Deciding the approval lets the run go on, or ends what it asked for.
pnpm exec kindgi approvals complete <approval-id> --decision=approve --rationale="Confirmed with the on-call engineer"uv run kindgi approvals complete <approval-id> --decision=approve --rationale="Confirmed with the on-call engineer"./kindgiw approvals complete <approval-id> --decision=approve --rationale="Confirmed with the on-call engineer"./kindgiw approvals complete <approval-id> --decision=approve --rationale="Confirmed with the on-call engineer"<approval-id>is the approval'sid.--decisionis 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 lists why.
In the console, a reviewer decides on the approval's page or in the Inbox, as Deciding in the console shows.
Escalate
Section titled “Escalate”Escalating asks a reviewer of the next role up instead. A standard reviewer
who wants a second opinion escalates:
pnpm exec kindgi approvals complete 2134897a-d176-4ca8-b000-8bc00d79ad04 --decision=escalate --rationale="Needs sign-off from the incident lead"uv run kindgi approvals complete 2134897a-d176-4ca8-b000-8bc00d79ad04 --decision=escalate --rationale="Needs sign-off from the incident lead"./kindgiw approvals complete 2134897a-d176-4ca8-b000-8bc00d79ad04 --decision=escalate --rationale="Needs sign-off from the incident lead"./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:
{ "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
Section titled “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:
pnpm exec kindgi approvals complete <approval-id> --decision=withdraw --rationale="Asked by mistake"uv run kindgi approvals complete <approval-id> --decision=withdraw --rationale="Asked by mistake"./kindgiw approvals complete <approval-id> --decision=withdraw --rationale="Asked by mistake"./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
Section titled “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:
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:
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 truefrom 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:
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 Truepackage 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:
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 truepackage acme.app
import com.kindgi.client.Kindgiimport com.kindgi.client.models.{ApprovalStatus, CompleteApprovalBody}import com.kindgi.client.resources.ApprovalsListParamsimport 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:
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 trueThe first pending approval is approved, and its run went on:
waitpointResolved is true. Listing approvals
has the list's other options, such as one project's approvals or one run's.