Add a Human Approval Gate
Route a high-risk agent action through KLA for policy evaluation, human sign-off, and server-owned execution.
Some agent actions need accountable human sign-off: deleting an account, processing a payment, or releasing a document. This guide sends one such action through the KLA Control Plane SDK. KLA evaluates the Decision Request, routes a required Escalation to the Decision Desk, executes through the registered connector after authorization, and returns the recorded result.
How a Gate Works
executeAction in Node.js and execute_action in Python submit a Decision Request containing the action and its context. The KLA Policy Engine resolves it to allow, warn, require_approval, or block. A require_approval result opens an Escalation, a paused unit of work waiting for a human decision. The Decision Desk routes it to an authorized reviewer. The SDK keeps one proposal identity through polling and resume. KLA executes the approved action once through the registered connector and returns execution.result.
sequenceDiagram
participant App as Your agent
participant KLA as KLA checkpoint
participant Desk as Decision Desk
App->>KLA: executeAction("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->>KLA: Execute registered connector once
KLA-->>App: execution.result or typed denialAdd the Checkpoint
Below, a high-risk process_payment action uses kla-governance for Python and @kla-digital/governance for Node.js. The SDK polls until the Decision Desk resolves the Escalation. The approval binds the account, amount, agent Release, mandate, tool, operation, and destination to one proposal.
The governance packages are release candidates. The install commands become available after the
reviewed 0.1.0 packages are published to npm and PyPI.
Python
from kla_governance import ActionRequest, AgentIdentity, DecisionDenied, KLAClient
client = KLAClient(
base_url="https://api.kla.digital",
credential=get_access_token,
agent=AgentIdentity(agent_id="agt_9f81a7", release_version="1.4.2"),
mandate_id="mnd_payments",
)
def process_payment(account_id: str, amount: float):
try:
return client.execute_action(
ActionRequest(
tool="payments",
operation="charge",
destination="gateway:acct_main",
parameters={"account_id": account_id, "amount": amount},
),
proposal_id=stable_id_for(account_id, amount),
)
except DecisionDenied as denial:
raise PaymentNotAuthorized(
f"Payment not authorized: {denial.reason_codes} ({denial.remediation})"
)
TypeScript
import { KLAClient, DecisionDenied } from '@kla-digital/governance';
const client = new KLAClient({
baseUrl: 'https://api.kla.digital',
credential: () => getAccessToken(),
agent: { agentId: 'agt_9f81a7', releaseVersion: '1.4.2' },
mandateId: 'mnd_payments',
});
async function processPayment(accountId: string, amount: number) {
try {
return await client.executeAction(
{
tool: 'payments',
operation: 'charge',
destination: 'gateway:acct_main',
parameters: { accountId, amount },
},
{ proposalId: stableIdFor(accountId, amount) }
);
} catch (error) {
if (error instanceof DecisionDenied) {
throw new PaymentNotAuthorizedError(
`Payment not authorized: ${error.reasonCodes} (${error.remediation})`
);
}
throw error;
}
}
Configure the client at startup with the control-plane base URL, a client-credentials access token, the registered agent id, its pinned Release version, and an active mandate id. Register the tool and connector in the Tool Catalog. The mandate must cover the tool, operation, and destination. Tenant identity comes from the verified token. The SDK sends Authorization: Bearer <token> with each action request.
Use resolve_binding() in Python or resolveBinding() in Node.js as a read-only startup check. It
confirms the current approved Release and active mandate revision, including the validity window and
allowed actions, before the worker accepts governed tasks.
Compose Governance with OpenTelemetry
Use both SDKs in an instrumented agent. They have separate responsibilities:
| Package | Responsibility |
|---|---|
@kla-digital/governance or kla-governance |
Sends the Decision Request, waits for policy and human review, and returns the server-recorded execution result. This call is the control point for the action. |
@kla-digital/otel-node or kla-otel-python |
Records the surrounding model, framework, and application activity as OpenTelemetry spans for Lineage Explorer. |
Initialize the OpenTelemetry SDK at application startup, then call the governance SDK at each
consequential action boundary. Pass the active OpenTelemetry trace and span identifiers through
the governance call's trace option when they are available. This correlates the Decision Request
and server execution with the surrounding Lineage Record. A resolved governance call authorizes
the registered connector execution. Span export records activity and has no authorization effect.
For Node.js:
npm install @kla-digital/governance @kla-digital/otel-node @opentelemetry/api
import '@kla-digital/otel-node';
import { trace } from '@opentelemetry/api';
const activeSpan = trace.getActiveSpan()?.spanContext();
const result = await client.executeAction(action, {
proposalId,
trace: activeSpan
? { traceId: activeSpan.traceId, spanId: activeSpan.spanId }
: undefined,
});
For Python:
pip install kla-governance kla-otel-python
from opentelemetry import trace
span_context = trace.get_current_span().get_span_context()
correlation = None
if span_context.is_valid:
correlation = {
"trace_id": format(span_context.trace_id, "032x"),
"span_id": format(span_context.span_id, "016x"),
}
result = client.execute_action(
action,
proposal_id=proposal_id,
trace=correlation,
)
See the Node.js OpenTelemetry SDK guide or Python OpenTelemetry SDK guide for framework instrumentation and collector setup.
Handle a Denial Gracefully
A reviewer can deny the Escalation, or policy can return a hard block. Both surface from the execution helper as DecisionDenied with the resolved decision attached. The outcome is recorded. Treat the exception 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 the denial. 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.
A require_approval action can wait as long as a reviewer takes. Run it on a worker or background task. Pass timeout (timeoutMs in Node.js) to bound the wait. ApprovalTimeout means the Decision Request remains pending. Call the execution helper again with the same proposal id to continue. Cancelling the local task leaves the server proposal open. Use cancel_action (cancelAction) to close a received or approval-pending proposal.
Pass a stable proposal_id (here, derived from account and amount). KLA binds the approval and execution ledger entry to that proposal. A retry after sign-off replays the recorded result and preserves one connector invocation.
If evidence sealing remains incomplete after the connector runs, the execution helper raises
ExecutionEvidencePending and withholds every result carried by that state. The error retains the
proposal's decision and receipt references for reconciliation. Keep that proposal identity; a new
identity represents a new logical action.
Both SDKs validate the full ingress response before releasing a decision or result. The schema
version, proposal and action identity, lifecycle states, result hash, and receipt references must
agree. A malformed response raises KLATransportError with response_schema_invalid.
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.
