Quickstart Guide
Go from sign-in to a recorded allow, warn, require_approval, and block decision, a Decision Desk approval, and a sealed evidence bundle in about 30 minutes.
This guide takes a developer workstation from sign-in to one recorded decision for each policy outcome: allow, warn, require_approval, and block. You register a KYC onboarding agent and its tool, publish a starter KYC policy, call it from the governance SDK with a tenant API key, have a second person decide the require_approval case in the Decision Desk, and find every decision in the Audit Trail. The commands target the KLA dev environment at https://api.dev.kla.digital. Plan about 30 minutes.
You need two people in the tenant: you, a tenant administrator who creates policies, and a reviewer with the kla:approver role. KLA refuses a review or approval by the person who made the request.
Run the KLA CLI
The KLA CLI currently runs from this monorepo. From the repository root, build it with Node.js 20.x:
pnpm install --frozen-lockfile
pnpm kla --help
alias kla='pnpm --silent kla'
The repository wrapper rebuilds the CLI when a source file is newer than its generated output. The steps below also use jq.
Step 1: Sign In
Create a CLI context for your tenant, then sign in through the browser. The CLI uses the public kla-cli client with PKCE and stores no client secret.
export KLA_TENANT_ID='<tenant external ID>'
kla context set "$KLA_TENANT_ID" \
--api-url https://api.dev.kla.digital \
--tenant "$KLA_TENANT_ID" \
--issuer "https://auth.dev.kla.digital/realms/$KLA_TENANT_ID" \
--client-id kla-cli \
--use
kla auth login
kla auth test
kla auth test prints the tenant your credential resolves to. The reviewer runs the same commands in their own terminal and signs in as themselves. See Authentication.
Step 2: Register the Agent and Its Tool
Write an agent manifest. governanceIntent.environment declares where the agent is governed: development, staging, or production.
cat > kyc-agent.yaml <<'YAML'
name: KYC Onboarding Agent
description: Screens retail account applications and recommends an onboarding decision.
model:
provider: openai
name: gpt-4o
pipeline:
- type: llm_generate
name: screen_application
config:
template: Review the application and recommend approve, review, or decline with reasons.
owners:
- email: [email protected]
tools:
- open_customer_account
humanOversightRequired: true
governanceIntent:
environment: development
YAML
kla agent create --file kyc-agent.yaml > agent.json
export AGENT_ID=$(jq -r '.id' agent.json)
A policy scoped to this agent (scope.agentIds) can enter review only when its scope.environments names the same environment. Set governanceIntent at registration: a later Draft cannot change it.
KLA decides only on tools listed in the tenant's Tool Catalog. Register the tool the agent calls, with the fields the policy reads:
cat > open-customer-account.tool.json <<'JSON'
{
"key": "open_customer_account",
"name": "Open customer account",
"description": "Opens a retail account after KYC screening.",
"risk_class": "high",
"input_schema": {
"type": "object",
"properties": {
"sanctions_hit": { "type": "boolean" },
"pep_match": { "type": "boolean" },
"country_risk": { "type": "string", "enum": ["standard", "high"] },
"risk_score": { "type": "number", "minimum": 0, "maximum": 100 }
},
"required": ["sanctions_hit", "pep_match", "country_risk", "risk_score"]
}
}
JSON
kla api tools create --input @open-customer-account.tool.json
When the tenant already has the tool, tools.create answers CONFLICT and the existing entry stays in place.
Step 3: Write the Policy and Simulate Four Outcomes
Policy rules read tool arguments at context.action.toolArgs.<field>. Agent ingress exposes action.parameters at that same path. Validated inputs also support tool.<toolId>.input.<field>.
The starter KYC policy refuses a sanctions hit, sends a politically exposed person (PEP) or a high-risk country to a second person, flags medium risk, and opens low-risk accounts. Any other request, such as a risk score above 70 or a missing field, resolves to the default decision block.
cat > kyc-onboarding-gate.template.json <<'JSON'
{
"policyId": "kyc-onboarding-gate",
"name": "KYC onboarding gate",
"description": "Opens low-risk accounts, flags medium risk, sends PEP and high-risk countries to a second person, and refuses sanctions hits.",
"version": "1.0.0",
"scope": { "tools": [], "environments": ["development"], "agentIds": ["AGENT_ID"] },
"defaultDecision": "block",
"rules": [
{
"ruleId": "block-sanctions-hit",
"name": "Refuse a sanctions hit",
"interceptionPoint": "tool_call",
"when": { "expression": { "and": [
{ "==": [{ "var": "context.action.toolName" }, "open_customer_account"] },
{ "==": [{ "var": "context.action.toolArgs.sanctions_hit" }, true] }
] } },
"then": {
"decision": "block",
"reason": "Sanctions screening returned a hit.",
"reasonCodes": ["sanctions_hit"],
"remediation": { "summary": "Refuse the account and follow the sanctions procedure.", "steps": [] }
},
"execution": { "mode": "deterministic" },
"evidence": { "artifactRefs": [], "evaluatedFields": [
"context.action.toolName", "context.action.toolArgs.sanctions_hit"
] }
},
{
"ruleId": "review-pep-or-high-risk-country",
"name": "Second review for a PEP or a high-risk country",
"interceptionPoint": "tool_call",
"when": { "expression": { "and": [
{ "==": [{ "var": "context.action.toolName" }, "open_customer_account"] },
{ "==": [{ "var": "context.action.toolArgs.sanctions_hit" }, false] },
{ "or": [
{ "==": [{ "var": "context.action.toolArgs.pep_match" }, true] },
{ "==": [{ "var": "context.action.toolArgs.country_risk" }, "high"] }
] }
] } },
"then": {
"decision": "require_approval",
"reason": "A PEP match or a high-risk country needs a second person.",
"reasonCodes": ["pep_or_high_risk_country"],
"approverGroup": "kla:approver",
"remediation": { "summary": "Approve or reject the account in the Decision Desk.", "steps": [] }
},
"execution": { "mode": "deterministic" },
"evidence": { "artifactRefs": [], "evaluatedFields": [
"context.action.toolName", "context.action.toolArgs.sanctions_hit",
"context.action.toolArgs.pep_match", "context.action.toolArgs.country_risk"
] }
},
{
"ruleId": "warn-medium-risk",
"name": "Flag medium risk",
"interceptionPoint": "tool_call",
"when": { "expression": { "and": [
{ "==": [{ "var": "context.action.toolName" }, "open_customer_account"] },
{ "==": [{ "var": "context.action.toolArgs.sanctions_hit" }, false] },
{ "==": [{ "var": "context.action.toolArgs.pep_match" }, false] },
{ "==": [{ "var": "context.action.toolArgs.country_risk" }, "standard"] },
{ ">": [{ "var": "context.action.toolArgs.risk_score" }, 30] },
{ "<=": [{ "var": "context.action.toolArgs.risk_score" }, 70] }
] } },
"then": {
"decision": "warn",
"reason": "Medium customer risk: open the account and schedule enhanced monitoring.",
"reasonCodes": ["medium_risk"],
"remediation": { "summary": "Schedule enhanced monitoring for this customer.", "steps": [] }
},
"execution": { "mode": "deterministic" },
"evidence": { "artifactRefs": [], "evaluatedFields": [
"context.action.toolName", "context.action.toolArgs.sanctions_hit",
"context.action.toolArgs.pep_match", "context.action.toolArgs.country_risk",
"context.action.toolArgs.risk_score"
] }
},
{
"ruleId": "allow-low-risk",
"name": "Open a low-risk account",
"interceptionPoint": "tool_call",
"when": { "expression": { "and": [
{ "==": [{ "var": "context.action.toolName" }, "open_customer_account"] },
{ "==": [{ "var": "context.action.toolArgs.sanctions_hit" }, false] },
{ "==": [{ "var": "context.action.toolArgs.pep_match" }, false] },
{ "==": [{ "var": "context.action.toolArgs.country_risk" }, "standard"] },
{ ">=": [{ "var": "context.action.toolArgs.risk_score" }, 0] },
{ "<=": [{ "var": "context.action.toolArgs.risk_score" }, 30] }
] } },
"then": {
"decision": "allow",
"reason": "Low customer risk with clear screening.",
"reasonCodes": ["low_risk"]
},
"execution": { "mode": "deterministic" },
"evidence": { "artifactRefs": [], "evaluatedFields": [
"context.action.toolName", "context.action.toolArgs.sanctions_hit",
"context.action.toolArgs.pep_match", "context.action.toolArgs.country_risk",
"context.action.toolArgs.risk_score"
] }
},
{
"ruleId": "allow-account-opening-result",
"name": "Return the authorized account-opening result",
"interceptionPoint": "tool_result",
"appliesTo": { "toolKeys": ["open_customer_account"] },
"when": { "expression": { "==": [{ "var": "context.action.toolName" }, "open_customer_account"] } },
"then": { "decision": "allow", "reason": "The authorized account-opening result can be returned.", "reasonCodes": ["account_opening_result"] },
"execution": { "mode": "deterministic" }
}
]
}
JSON
export POLICY_ID='kyc-onboarding-gate'
jq --arg agent "$AGENT_ID" --arg policy "$POLICY_ID" \
'.policyId = $policy | .scope.agentIds = [$agent]' \
kyc-onboarding-gate.template.json > kyc-onboarding-gate.json
kla policy registry create --file kyc-onboarding-gate.json | jq '.lint'
lint.ok is true. lint.issues includes a POLICY_BAND_GAP warning for scores above 70. Review the lint issues and the policy's default block decision before submitting the draft. POLICY_ID must be new in the tenant; if kla policy registry get "$POLICY_ID" already finds a policy, choose another ID.
Simulate one application per outcome. A Simulation evaluates the draft without recording a live decision, and review requires a stored Simulation of the exact draft:
simulate() {
jq -n --arg tenant "$KLA_TENANT_ID" --arg agent "$AGENT_ID" --argjson args "$2" '{
context: {
occurredAt: (now | todate),
identifiers: { workspaceId: $tenant, agentId: $agent, environment: "development" },
action: { actionType: "tool_call", toolName: "open_customer_account", toolArgs: $args }
}
}' > "simulate-$1.json"
kla policy registry simulate "$POLICY_ID" 1.0.0 --input "@simulate-$1.json" \
| jq -c '{case: "'"$1"'", decision: .decision.decision, rule: .decision.primaryBasis.ruleId}'
}
simulate low '{"sanctions_hit":false,"pep_match":false,"country_risk":"standard","risk_score":15}'
simulate medium '{"sanctions_hit":false,"pep_match":false,"country_risk":"standard","risk_score":55}'
simulate pep '{"sanctions_hit":false,"pep_match":true,"country_risk":"standard","risk_score":20}'
simulate sanctions '{"sanctions_hit":true,"pep_match":false,"country_risk":"standard","risk_score":10}'
{"case":"low","decision":"allow","rule":"allow-low-risk"}
{"case":"medium","decision":"warn","rule":"warn-medium-risk"}
{"case":"pep","decision":"require_approval","rule":"review-pep-or-high-risk-country"}
{"case":"sanctions","decision":"block","rule":"block-sanctions-hit"}
Submit the draft for review:
kla policy registry submit-review "$POLICY_ID" 1.0.0 --comment 'Starter KYC onboarding gate.'
Step 4: Review and Publish the Policy
The reviewer approves the version from their own terminal:
kla policy registry review kyc-onboarding-gate 1.0.0 \
--decision approve --comment 'Four outcomes match the KYC procedure.' --yes
You publish it. Publication starts now unless you pass --effective-from, and it opens a Decision Request for the reviewer:
kla policy registry publish "$POLICY_ID" 1.0.0 --reason 'First KYC onboarding gate.' --yes
The reviewer opens Decision Desk in the Console at https://kla.digital, opens Publish KYC onboarding gate 1.0.0?, opens the Policy version diff and the Lint report, chooses Approve publish, gives a reason, and selects Record decision. The version is then published.
Step 5: Create an API Key for the Agent
The agent authenticates with a tenant API key. A key with the policy:evaluate scope can request decisions and nothing else:
kla api secrets createApiKey \
--input '{"name":"kyc-onboarding-agent","permissions":["policy:evaluate"],"expiresAt":"2027-01-08"}' \
> api-key.json
export KLA_API_KEY=$(jq -r '.key' api-key.json)
jq '.effectiveAccess' api-key.json
KLA returns the key once. Store it in your secret manager, keep apiKey.id for revocation, and delete api-key.json. A tenant administrator can also create the key in Settings > API keys.
Step 6: Ask for Decisions from the Governance SDK
The governance SDK asks KLA for a decision before the agent calls a tool. KLA records each decision under your run and gate identifiers and runs nothing; the agent acts on the result. A require_approval decision opens a Decision Request in the Decision Desk.
Create an identity file once for this batch of actions and keep it for retries. Both examples read its original timestamp; they use separate run identifiers for Python and Node.js. Use a new file for a new batch.
if [ ! -f kyc-action-identity.env ]; then
printf 'export KLA_ACTION_BATCH_ID=%s\nexport KLA_ACTION_OCCURRED_AT=%s\n' \
"$(node -e "console.log(require('node:crypto').randomUUID())")" \
"$(date -u +'%Y-%m-%dT%H:%M:%SZ')" > kyc-action-identity.env
fi
. ./kyc-action-identity.env
Python
pip install kla-governance
import os
from kla_governance import PolicyClient
client = PolicyClient(
base_url="https://api.dev.kla.digital",
workspace_id=os.environ["KLA_TENANT_ID"],
api_key=os.environ["KLA_API_KEY"],
)
applications = {
"case-1001": {"sanctions_hit": False, "pep_match": False, "country_risk": "standard", "risk_score": 15},
"case-1002": {"sanctions_hit": False, "pep_match": False, "country_risk": "standard", "risk_score": 55},
"case-1003": {"sanctions_hit": False, "pep_match": True, "country_risk": "standard", "risk_score": 20},
"case-1004": {"sanctions_hit": True, "pep_match": False, "country_risk": "standard", "risk_score": 10},
}
for case_id, fields in applications.items():
result = client.evaluate_tool_call(
run_id=f"{case_id}-python-{os.environ['KLA_ACTION_BATCH_ID']}",
occurred_at=os.environ["KLA_ACTION_OCCURRED_AT"],
gate_id="open-account",
tool_name="open_customer_account",
tool_args=fields,
agent_id=os.environ["AGENT_ID"],
environment="development",
policy_id=os.environ["POLICY_ID"],
)
print(case_id, result.decision, result.rule_id, result.approval.approval_id if result.approval else "")
Node.js
npm install @kla-digital/governance
import { KLAPolicyClient } from '@kla-digital/governance';
const client = new KLAPolicyClient({
baseUrl: 'https://api.dev.kla.digital',
workspaceId: process.env.KLA_TENANT_ID!,
apiKey: process.env.KLA_API_KEY!,
});
const applications = {
'case-1001': { sanctions_hit: false, pep_match: false, country_risk: 'standard', risk_score: 15 },
'case-1002': { sanctions_hit: false, pep_match: false, country_risk: 'standard', risk_score: 55 },
'case-1003': { sanctions_hit: false, pep_match: true, country_risk: 'standard', risk_score: 20 },
'case-1004': { sanctions_hit: true, pep_match: false, country_risk: 'standard', risk_score: 10 },
};
for (const [caseId, toolArgs] of Object.entries(applications)) {
const result = await client.evaluateToolCall({
runId: `${caseId}-node-${process.env.KLA_ACTION_BATCH_ID}`,
occurredAt: process.env.KLA_ACTION_OCCURRED_AT!,
gateId: 'open-account',
toolName: 'open_customer_account',
toolArgs,
caseReference: caseId,
agentId: process.env.AGENT_ID,
environment: 'development',
policyId: process.env.POLICY_ID,
});
console.log(caseId, result.decision, result.ruleId, result.approval?.approvalId ?? '');
}
Either program prints one decision per application:
case-1001 allow allow-low-risk
case-1002 warn warn-medium-risk
case-1003 require_approval review-pep-or-high-risk-country <Decision Request ID>
case-1004 block block-sanctions-hit
Retrying the same action requires the original runId, gateId, occurredAt, tool arguments, Agent, policy, and environment. The SDK retains the body during its automatic retries. A separate SDK call without an explicit timestamp generates a new occurredAt; reusing the same run and gate then returns 409. Keep kyc-action-identity.env and the original inputs when rerunning these examples. Give each distinct action in a run a new gateId. Sending an identical body returns the recorded decision. A 403 with api_key_scope_denied means the key lacks the policy:evaluate scope.
Step 7: Decide the Review in the Decision Desk
The reviewer opens Decision Desk, opens the request whose case starts with case-1003, chooses Approve decision or Reject decision, gives a reason, confirms, and selects Record decision. KLA records the decision with the request and in the Audit Trail.
In this guide the agent stops at require_approval and leaves the account unopened. Sending the same request again returns the recorded require_approval decision. For an agent that waits for the reviewer and then continues, use the action ingress in the last section of this page.
Step 8: Find the Decisions in the Audit Trail
Open Audit Trail in the Console and search for case-1003. The recorded sequence shows the policy decision with the API key as the actor, the Decision Request, and the reviewer's decision, each with its entry checksum and proof root.
Step 9: Export a Sealed Evidence Bundle
Export governed decisions, approvals, and audit entries with the kla CLI. The bundle is signed and lists the hash of every file it contains.
# Export the last 30 days to a local directory
kla evidence export --tenant $KLA_TENANT_ID --days 30 --out ./evidence
# Export an exact time window
kla evidence export --tenant $KLA_TENANT_ID \
--start-date 2026-07-01 \
--end-date 2026-07-31 \
--out ./evidence
Verify the bundle offline before you hand it to an auditor:
kla evidence verify ./evidence/<bundle file>.zip
Next: Let KLA Run the Tool
In this guide the agent runs its own tool after each decision. To have KLA run the tool through a registered connector, the agent sends each action to POST /v1/agent-actions with KLAClient and an API key with the actions:propose scope. That path needs an approved, deployed Release and an active mandate, which states the tools, operations, destinations, and purposes the agent may act on:
PRODUCTION_AGENT_ID='<registered production Agent UUID>'
kla agent mandate create "$PRODUCTION_AGENT_ID" --file mandate.yaml
kla agent mandate submit <mandate ID>
kla agent mandate get <mandate ID>
kla agent mandate approve <mandate ID> --version <version> --reason 'Scope matches the onboarding procedure.'
kla agent mandate activate <mandate ID>
This Quickstart registered a development Agent. A production Release requires a separate Agent registered with governanceIntent.environment: production, plus a published policy scoped to production that permits that Agent. Govern an Agent End-to-End shows that registration and Release flow. Keep the development registration unchanged. Use the production Agent’s ID for its mandate; a different reviewer approves the exact version returned by get. Governance SDK Quickstart (Python) covers the mandate file and first governed action.
