Skip to content

Approval Nodes

DAG workflow nodes support an approval field that pauses workflow execution until a human approves or rejects the gate. Use approval nodes to insert human review steps between AI-driven nodes — for example, reviewing a generated plan before committing to expensive implementation work.

Web UI users: Add interactive: true at the workflow level. Without it, the workflow dispatches to a background worker and approval gate messages won’t appear in your chat window. See Web Execution Mode.

name: plan-approve-implement
description: Plan, get approval, then implement
interactive: true # Required for Web UI: ensures approval gates appear in chat
nodes:
- id: plan
prompt: |
Analyze the codebase and create a detailed implementation plan.
$USER_MESSAGE
- id: review-gate
approval:
message: "Review the plan above before proceeding with implementation."
depends_on: [plan]
- id: implement
command: implement
depends_on: [review-gate]

When execution reaches review-gate, the workflow pauses and sends a message to the user on whatever platform they’re using (CLI, Slack, GitHub, etc.). On the Web UI, interactive: true is required for the message to appear in your chat.

  1. Pause: The executor sets the workflow run status to paused and stores the approval context (node ID and message) in the run’s metadata.
  2. Notify: A message is sent to the user with the approval prompt and instructions for approving or rejecting.
  3. Wait: The workflow stays paused until the user takes action. Paused runs block the worktree path guard (no other workflow can start on the same path).
  4. Approve: The user approves, which writes a node_completed event for the approval node and transitions the run to resumable. Natural-language messages (recommended) and the CLI auto-resume immediately. The explicit /workflow approve command records the approval; send a follow-up message to resume.
  5. Reject: The user rejects.
    • Without on_reject: The workflow is cancelled immediately.
    • With on_reject: The executor runs the on_reject.prompt via AI (with $REJECTION_REASON substituted), then re-pauses at the same gate. This repeats until the user approves or on_reject.max_attempts is reached, at which point the workflow is cancelled.
- id: gate-name
approval:
message: "Human-readable prompt shown to the user"
capture_response: true # optional: store comment as $gate-name.output
on_reject: # optional: AI rework on rejection instead of cancel
prompt: "Fix based on feedback: $REJECTION_REASON"
max_attempts: 3 # optional: default 3, range 1–10
depends_on: [upstream-node] # optional
when: "$plan.output != ''" # optional condition
trigger_rule: all_success # optional (default: all_success)
FieldTypeRequiredDescription
approval.messagestringYesThe message shown to the user when the workflow pauses
approval.capture_responsebooleanNoWhen true, the user’s approval comment is stored as $<node-id>.output for downstream nodes. Default: false
approval.on_reject.promptstringNoPrompt template run via AI when the user rejects. $REJECTION_REASON is substituted with the reject reason. After running, the workflow re-pauses at the same gate
approval.on_reject.max_attemptsintegerNoMax times the on_reject prompt runs before the workflow is cancelled. Range: 1–10. Default: 3

Approval nodes do not support AI-specific fields (model, provider, context, output_format, allowed_tools, denied_tools, hooks, mcp, skills, idle_timeout) since they don’t invoke an AI agent. (The on_reject.prompt runs as a separate AI node using the workflow’s default provider.)

Standard DAG fields (id, depends_on, when, trigger_rule, retry) work as expected.

Just type your answer in the same conversation. The system detects the paused workflow and treats your message as the approval response:

User: "Looks good, but add error handling for the edge cases"
→ System auto-approves, resumes workflow with your message as $gate.output
(only if capture_response: true is set)

This works on all platforms (Web, Slack, Telegram, Discord, GitHub).

To reject instead, use /workflow reject <run-id>.

The CLI is non-interactive — use explicit commands:

Terminal window
# Approve (resumes the workflow immediately)
bun run cli workflow approve <run-id>
bun run cli workflow approve <run-id> --comment "Looks good, proceed"
# Reject
# Without on_reject: cancels the workflow
# With on_reject: records feedback, triggers AI rework, re-pauses
bun run cli workflow reject <run-id>
bun run cli workflow reject <run-id> --reason "Plan needs more test coverage"
/workflow approve <run-id> looks good
/workflow reject <run-id> needs changes

Paused workflows show an amber pulsing badge on the dashboard. Click Approve or Reject directly on the workflow card.

Terminal window
# Approve
curl -X POST http://localhost:3090/api/workflows/runs/<run-id>/approve \
-H "Content-Type: application/json" \
-d '{"comment": "Approved"}'
# Reject
curl -X POST http://localhost:3090/api/workflows/runs/<run-id>/reject \
-H "Content-Type: application/json" \
-d '{"reason": "Needs revision"}'

By default, the user’s approval comment is not available downstream — $<node-id>.output will be an empty string. To capture the comment as node output, set capture_response: true:

nodes:
- id: gate
approval:
message: "Any special instructions for implementation?"
capture_response: true # Makes the user's comment available as $gate.output
depends_on: [plan]
- id: implement
prompt: |
Implement the plan. User instructions: $gate.output
depends_on: [gate]

Without capture_response: true, downstream nodes should not reference $gate.output — it will be an empty string.

When on_reject is configured, a rejection does not cancel the workflow — instead, the executor runs an AI prompt with the rejection reason and re-pauses at the same gate.

- id: review-gate
approval:
message: "Review the implementation plan."
capture_response: true
on_reject:
prompt: |
The reviewer rejected the plan with this feedback: $REJECTION_REASON
Revise the plan to address the feedback, then summarize the changes.
max_attempts: 3 # After 3 rejections, the workflow is cancelled. Default: 3.
depends_on: [plan]

The $REJECTION_REASON variable is substituted with the --reason text provided by the rejecting user. After the AI rework, the workflow re-pauses so the reviewer can approve or reject again.

  1. Workflow pauses at approval gate
  2. Reviewer rejects: rejection_count incremented, rejection_reason stored
  3. If rejection_count < max_attempts: on_reject.prompt runs via AI, workflow re-pauses
  4. If rejection_count >= max_attempts: workflow cancelled
  • Multiple approval nodes: Supported. Each pauses the workflow independently.
  • Approval in parallel layer: Other nodes in the same layer complete normally; the workflow pauses at the layer boundary.
  • Server restart while paused: The run persists in the database. The user can still approve or reject after restart.
  • Abandoning a paused run: Use /workflow abandon <id> or the Abandon button on the dashboard.

Approval nodes reuse the existing resume infrastructure (from workflow lifecycle PR #871). When approved, the run transitions through failed status briefly so that findResumableRun picks it up — this avoids duplicating resume logic. The metadata.approval_response field distinguishes approved-then-resumed from genuinely-failed runs.