Add a Human Approval Gate
Pause a single high-risk tool call for human sign-off using the KLA SDK checkpoint pattern, routed to the Decision Desk.
Some agent actions are too consequential to run unsupervised: deleting an account, processing a payment, releasing a document. This guide shows how to wrap one such call in a human approval gate using the KLA Control Plane SDK. KLA Control Plane is a govern-in-place runtime safety, audit, and governance layer: you instrument your existing agent code rather than re-platforming it. A single checkpoint is enough to make one tool call pause for a human, route to a reviewer, and resume only after explicit sign-off.
How a Gate Works
You wrap the risky call in a checkpoint. The checkpoint submits a Decision Request (the action plus its context) to the KLA policy engine, which resolves it to one of four outcomes in precedence order: allow, warn, require_approval, or block. When policy returns require_approval, execution pauses. KLA opens an Escalation (a paused unit of work waiting on a human) and routes it to the Decision Desk, the workspace where authorized reviewers approve or deny pending actions. The SDK call blocks until a reviewer decides, then returns control to your code.
sequenceDiagram
participant App as Your agent
participant KLA as KLA checkpoint
participant Desk as Decision Desk
App->>KLA: checkpoint("process_payment", context)
KLA->>KLA: Evaluate Decision Request
Note over KLA: outcome = require_approval
KLA->>Desk: Open Escalation
Desk-->>KLA: Reviewer approves or denies
KLA-->>App: Resolved decision
App->>App: Execute or handle denialAdd the Checkpoint
Below, a high-risk process_payment call is gated. The SDK blocks on the checkpoint until the Escalation is resolved at the Decision Desk. You attach business attributes (amount, customer tier) so policy can decide and so the reviewer sees real context.
Python
from kla_otel import KLACheckpoint, DecisionDenied
checkpoint = KLACheckpoint(agent_id="agt_9f81a7")
def process_payment(account_id: str, amount: float):
# Blocks here if policy returns require_approval, until a
# reviewer resolves the Escalation on the Decision Desk.
decision = checkpoint.evaluate_action(
action="process_payment",
context={"account_id": account_id, "amount": amount},
idempotency_key=f"pay-{account_id}-{amount}",
)
if decision.is_approved():
receipt = gateway.charge(account_id, amount)
checkpoint.log_success(receipt_id=receipt.id)
return receipt
# require_approval that was denied, or a hard block.
raise DecisionDenied(
f"Payment not authorized: {decision.reason_codes} ({decision.remediation})"
)
TypeScript
import { KLAClient, DecisionDenied } from '@kla-digital/otel-node';
const client = new KLAClient({ agentId: 'agt_9f81a7' });
async function processPayment(accountId: string, amount: number) {
// Awaits the reviewer's verdict if policy returns require_approval.
const decision = await client.checkpoint('process_payment', {
context: { accountId, amount },
idempotencyKey: `pay-${accountId}-${amount}`,
});
if (decision.approved) {
const receipt = await gateway.charge(accountId, amount);
await decision.logSuccess({ receiptId: receipt.id });
return receipt;
}
throw new DecisionDenied(
`Payment not authorized: ${decision.reasonCodes} (${decision.remediation})`,
);
}
Configure the SDK once, at startup, with your tenant and an access token. Every checkpoint call sends Authorization: Bearer <token> and x-tenant-id: <tenant> to https://api.kla.digital on your behalf.
Handle a Denial Gracefully
A reviewer can deny the Escalation, or policy can return a hard block. Both arrive as a resolved, non-approved decision, not a network error. Treat them as an expected branch:
- Read the reason codes. Each non-
allowdecision carries machine-readablereason_codes(for examplePAYMENT_OVER_THRESHOLD) and human-readableremediation. Branch on the codes; never parse the prose. - Surface, don't crash. Return a clear message to the calling user or upstream agent ("Payment requires manager approval and was declined"). The denial is already recorded as a Lineage Record, so you do not need to log it separately for audit.
- Do not retry blindly. A denied action should not loop back into the same checkpoint automatically. Escalate to a human path in your own product instead.
Before You Ship
Author the gating policy in the Policy Builder and run a Simulation: replay representative Decision Requests against the draft and confirm a high-value payment resolves to require_approval while a low-value one resolves to allow. Once validated, the policy compiles into a signed policy pack and goes live. From then on, every gated call produces a defensible trail: the request, the outcome, the reviewer's verdict, and the resulting action, all captured as a Lineage Record you can export later as a Sealed Evidence Bundle.
