Usage Guide: Approvals¶
Overview¶
This guide shows how to attach a human-approval gate to a graph node and resolve it programmatically. The shape matches examples/20_approval_gate.py: a HumanApprovalNode on the graph pauses the run into RunStatus.WAITING_APPROVAL, a reviewer calls POST /deployments/{ref}/approvals/{id}/resolve (or the in-process ApprovalService.resolve(...)), and the orchestrator resumes from the exact pause point.
Minimal example¶
# Slice from examples/20_approval_gate.py — approval gate, resolved in-process.
# ``build_demo_graph`` is a tutorial helper that ships in the repository, not in
# the wheel. Run this from the repository root, or copy the function into your code.
import sys
sys.path.insert(0, "examples")
from quickstart import build_demo_graph
from zeroth.contracts.graph import GraphRepository
# 1. Author a graph that contains a HumanApprovalNode.
graph = await graph_repository.create(build_demo_graph(include_approval=True))
await graph_repository.publish(graph.graph_id, graph.version)
deployment = await deployment_service.deploy("demo-approval", graph.graph_id, graph.version)
# 2. Drive the orchestrator — it pauses on the approval node.
paused = await bootstrap.orchestrator.run_graph(
bootstrap.graph,
{"message": "Say hello from zeroth-core."},
deployment_ref=deployment.deployment_ref,
)
assert paused.status.value == "waiting_approval"
# 3. Find the pending ApprovalRecord for the run.
pending = await bootstrap.approval_service.list_pending(
run_id=paused.run_id,
deployment_ref=deployment.deployment_ref,
)
approval_id = pending[0].approval_id
# 4. Resolve it over the real HTTP endpoint the curl docs show.
resolve = await client.post(
f"/deployments/{deployment.deployment_ref}/approvals/{approval_id}/resolve",
headers={"X-API-Key": "demo-operator-key"},
json={"decision": "approve"},
)
resolve.raise_for_status()
print(resolve.json()["run"]["status"]) # -> completed
That is the full pattern: enqueue (the orchestrator creates the pending record when it hits the node) and decide (the reviewer POSTs a decision; the orchestrator resumes).
Common patterns¶
- Allow edits. Set
allow_edits=Trueon the node'sapproval_policy_configand the reviewer can POST{"decision": "edit_and_approve", "edited_payload": {...}}to patch the payload in flight. - SLA + escalation. Set
sla_deadlineon theHumanApprovalNode; theApprovalSLACheckerwill mark overdue recordsESCALATEDand fire webhooks. - List pending for a dashboard.
GET /deployments/{ref}/approvals?status=pendingdrives operator UIs. - Reject cleanly.
{"decision": "reject"}terminates the run withfailure_state.reason == "approval_rejected"; the rejection is audit-logged viaApprovalActionRecord.
Pitfalls¶
- Resolving an already-resolved approval.
ApprovalService.resolveraises if the record is notPENDING— always checklist_pendingfirst. - Wrong role. The
X-API-Keycredential must carryServiceRole.OPERATORorServiceRole.REVIEWER; admin-only keys cannot resolve approvals. - Forgetting the deployment_ref.
list_pending(run_id=..., deployment_ref=...)needs both to scope within a tenant. - Sensitive payloads in summaries. The
summaryandrationalefields are shown verbatim to reviewers — keep secrets incontext_excerpt, which runs throughPayloadSanitizer. - Assuming synchronous completion. Over HTTP,
POST /runsreturns the current state; if the orchestrator is durable-worker-backed, the run may still be mid-flight when the response arrives.
Reference cross-link¶
See the Python API reference for zeroth.governance.approvals.
- Example source:
examples/20_approval_gate.py(full runnable version). - Related: Concept: approvals, Usage Guide: policy, Usage Guide: audit, Tutorial: governance walkthrough.