Approvals (Runtime)
Park a node before execution until a human decides. require_approval_for patterns, REST contract, MCP tool, and an end-to-end example.
Approvals (Runtime)
The open-source JamJet runtime supports a park-and-resume approval model: a node matching a require_approval_for glob pattern stops before execution, parks the execution, and waits indefinitely until a reviewer approves or rejects it via the REST API or the jamjet_approve MCP tool.
Parked approvals are durable. The hold is recorded as a ToolApprovalRequired event in the event log, so a crash or restart does not lose the pending decision.
Gating nodes with a policy
Declare require_approval_for in a policy block inside your workflow YAML. The field takes a list of glob patterns matched against the tool name of each tool node before it executes.
workflow:
id: payment-processor
version: 0.1.0
start_node: fetch_details
nodes:
- id: fetch_details
kind: tool
config:
fn: fetch_payment_details
input_from_state: { payment_id: payment_id }
output_to_state: { details: payment_details }
edges:
- to: submit_payment
- id: submit_payment
kind: tool
config:
fn: payments.submit
input_from_state: { details: payment_details }
output_to_state: { result: result }
edges:
- to: end
- id: end
kind: end
policy:
require_approval_for:
- "payments.*"When the scheduler reaches submit_payment, it emits ToolApprovalRequired, parks the node, and leaves the execution in running state. The node does not execute. Nothing times out.
Within the same policy scope, blocked_tools wins over require_approval_for: a tool matched by both is refused outright rather than parked. Across scopes, the most specific scope's rules are evaluated first (node over workflow over tenant), so a node-level require_approval_for fires before a workflow-level blocked_tools.
You can also scope the policy to a single node rather than the whole workflow:
- id: submit_payment
kind: tool
policy:
require_approval_for:
- "payments.*"
config:
fn: payments.submit
...Node-level policy takes precedence over workflow-level policy for that node.
Inspecting pending approvals
GET /executions/:id/approvalsReturns the pending and decided approvals for an execution.
{
"pending": [
{
"node_id": "submit_payment",
"tool_name": "payments.submit",
"approver": "human",
"context": {},
"sequence": 4
}
],
"decided": []
}Each pending entry carries node_id, tool_name, and the sequence number of the ToolApprovalRequired event that created the hold.
Deciding an approval
POST /executions/:id/approve
Content-Type: application/json
{
"decision": "approved",
"node_id": "submit_payment",
"user_id": "alice",
"comment": "Verified payment amount, looks correct."
}decision must be "approved" or "rejected". node_id is optional when exactly one approval is pending; the runtime infers it. comment is optional. An optional state_patch object is merged into execution state before the node resumes.
Response codes:
| Code | Condition |
|---|---|
200 | Decision recorded and node scheduled to resume. |
400 | Multiple approvals pending and node_id was omitted. |
409 | Nothing is pending, or node_id does not match a pending node, or the execution is already in a terminal state (completed, failed, cancelled, or limit_exceeded). |
Rejection behavior. When you reject, the node fails and the workflow fails with a reason that includes the decider's user_id and comment. Rejection is terminal for that execution.
Approval behavior. When you approve, the ApprovalReceived event is appended and the scheduler picks up the node on its next tick. The node then runs through the normal scheduling path as if the hold never existed.
MCP tool: jamjet_approve
The runtime exposes a jamjet_approve MCP tool through its built-in MCP bridge. It follows the same rules as the REST endpoint: you pass execution_id, decision, and optionally node_id, user_id, and comment. This lets an agent or an MCP client decide approvals without direct HTTP access.
{
"name": "jamjet_approve",
"arguments": {
"execution_id": "exec_01JM4X8NKWP2",
"decision": "approved",
"node_id": "submit_payment",
"user_id": "alice",
"comment": "Verified amount."
}
}End-to-end example
The following sequence uses curl. Replace exec_01... with the execution ID printed by jamjet run.
1. Register and start the workflow.
# Register the workflow definition
curl -s -X POST http://localhost:8080/workflows \
-H "Content-Type: application/json" \
-d @workflow.json
# Start an execution
curl -s -X POST http://localhost:8080/executions \
-H "Content-Type: application/json" \
-d '{"workflow_id": "payment-processor", "input": {"payment_id": "pay_001"}}'2. Check that the node is parked.
The execution stays running while a node is waiting for approval.
curl -s http://localhost:8080/executions/exec_01JM4X8NKWP2/approvals{
"pending": [
{
"node_id": "submit_payment",
"tool_name": "payments.submit",
"sequence": 4
}
],
"decided": []
}3. Approve and resume.
curl -s -X POST http://localhost:8080/executions/exec_01JM4X8NKWP2/approve \
-H "Content-Type: application/json" \
-d '{
"decision": "approved",
"user_id": "alice",
"comment": "Amount verified."
}'4. Confirm the execution completed.
curl -s http://localhost:8080/executions/exec_01JM4X8NKWP2 | jq .status
# "completed"To reject instead, replace "approved" with "rejected". The execution status will be "failed" with the decider and comment in the failure reason.
Runnable examples
examples/02-human-approval contains the policy snippet used on this page. examples/hitl-approval demonstrates an alternative human-in-the-loop pattern built on the human_approval node kind with a timeout, which is a different mechanism from require_approval_for policy gating.