Skip to content

Decide an approval

A run that waits for a person (a gated tool call or a long conversation) creates an approval. A reviewer decides it, and the run moves on.

Only reviewers see approvals. Until you register, every approvals command is refused:

Terminal window
kindgi approvals list
Error [auth]: This token was not provisioned with a reviewer role; approvals surface is reviewer-only.

Register yourself, the user your API token belongs to, with a role:

Terminal window
kindgi reviewers register --spec='{"role":"standard","displayName":"Ada Lovelace"}'
{
"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. Each approval has a requiredRole, and a reviewer sees and decides the approvals that need their role or a lower one. An approval that needs a higher role doesn't show up in the list, and fetching or deciding it answers not-found.

Registering again changes the role. kindgi reviewers list shows the reviewers; kindgi reviewers unregister <reviewer-id> removes one (their past decisions stay).

Terminal window
kindgi approvals list --status=pending
kindgi approvals get <approval-id>

Without --status, the list includes decided approvals too. In each approval:

  • subjectKind is what waits: tool-call:pending is a tool call (the subjectRef has the toolId and the arguments); agent-turn:session-hitl-gate is a conversation that reached its turn limit (the subjectRef has the conversationId and the turnCount).
  • requiredRole is the lowest role that may decide it.
  • provenanceRef.runId is the run that waits.
Terminal window
kindgi approvals complete <approval-id> --decision=approve --rationale="Confirmed with the on-call engineer"

--rationale is optional and is kept with the decision. The four decisions:

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 Keeps waiting; cancel it with kindgi runs cancel <run-id>.

An approve or reject resumes the run within the same call, so the run has moved on by the time the command returns. A decided approval can't be decided again.

A standard reviewer who wants a second opinion escalates:

Terminal window
kindgi approvals complete 2134897a-d176-4ca8-b000-8bc00d79ad04 --decision=escalate --rationale="Needs sign-off from the incident lead"
{
"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 closes an approval without deciding what it asked. The run keeps waiting, so cancel it:

Terminal window
kindgi approvals complete <approval-id> --decision=withdraw
kindgi runs cancel <run-id>

Cancelling a run doesn't close its approval either: withdraw the approval of a run you cancelled, so it leaves the pending list.

Each approval has a deadline, its expiresAt: 24 hours after it's asked for, or the agent's own hitl.timeoutMs (one hour: conversationPolicy: { hitl: { tools: {}, timeoutMs: 3_600_000 } }). When the deadline passes with no decision:

  • Below admin, the approval becomes escalated, and a new one asks the next role up (standard, then senior, then admin), with a deadline as long again. The run keeps waiting.

  • At admin, the approval becomes expired, and the run's turn fails:

    "error":{"code":"hitl-cancelled","message":"Tool-call HITL cancelled for acme-desk.echo: timeout","reason":"timeout"}

A decision made before the deadline always wins. In 0.1 a deadline always escalates this way: hitl.onTimeout (auto-approve, auto-reject) isn't applied yet.

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.

review.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.items) {
console.log(approval.id, approval.title, approval.subjectRef);
}
const first = pending.items[0];
if (first) {
const result = await kindgi.approvals.decide(first.id, {
decision: 'approve',
rationale: 'Checked the dashboard',
});
console.log(result.approval.status, result.waitpointResolved);
}
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