Approval
Add approval to any step to pause execution after the step completes and wait for human review.
If the workflow should wait without running a command first, use a human task instead.
| Behavior | Human task | Approval gate |
|---|---|---|
| Definition | Standalone action: human.task step | approval attached to an executable step |
| Before waiting | Runs no process | Runs the step first |
| Result | Completion succeeds the step | Approve, reject, or push back |
| Rewind | Not supported | Supported with rewind_to |
| Collected values | Typed form properties become step outputs | Inputs become approval or push-back environment values |
Usage
steps:
- id: deploy_staging
run: ./deploy.sh staging
approval:
prompt: "Verify staging deployment before production"
- id: deploy_prod
run: ./deploy.sh production
depends: deploy_stagingThe deploy_staging step runs ./deploy.sh staging, then enters Waiting status. The deploy_prod step remains Not Started until the approval is resolved.
Configuration
| Field | Type | Description |
|---|---|---|
prompt | string | Message displayed to the approver |
input | string[] | Parameter names to collect from the approver |
required | string[] | Parameters that must be provided (subset of input) |
rewind_to | string | Optional step name or ID to restart from on push-back |
All fields are optional. A bare approval: {} is valid — no prompt, no inputs, just a pause.
Validation: every entry in required must also appear in input. The build fails otherwise. If rewind_to is set, it must reference the current step or one of its upstream dependencies. Push-back input names are not special-cased. FEEDBACK is common, but any key can be used. If input is omitted, Dagu preserves all provided push-back keys.
How It Works
- The step executes normally (command runs, produces stdout/stderr)
- After successful execution, the step enters
Waitingstatus - The DAG status becomes
Waiting - Dependent steps remain
Not Started - A human reviews the step output and chooses one of:
- Approve — step succeeds, dependents execute
- Push back — step resets to
Not Startedand re-executes (see Push-back) - Reject — step enters
Rejectedstatus, DAG becomesRejected, dependents are aborted
Examples
Collecting Inputs
Approved inputs become environment variables in subsequent steps:
steps:
- id: generate_plan
run: ./generate-migration-plan.sh
approval:
prompt: "Review migration plan"
input: [APPROVED_BY, MAINTENANCE_WINDOW]
required: [APPROVED_BY]
- id: execute_migration
run: ./migrate.sh --approver "${env.APPROVED_BY}" --window "${env.MAINTENANCE_WINDOW}"
depends: generate_planAPPROVED_BY must be provided (it's in required). MAINTENANCE_WINDOW is optional. Both are injected as environment variables into execute_migration after approval.
Gating a Sub-DAG
Use call with approval to gate a multi-step workflow behind a single approval point. The sub-DAG runs to completion first, then the step waits for review:
steps:
- id: run_integration_tests
action: dag.run
with:
dag: integration-test-suite
approval:
prompt: "Review test results before deploying"
- id: deploy
run: ./deploy.sh production
depends: run_integration_testsThe integration-test-suite DAG (which may contain many steps internally) executes fully. Once finished, run_integration_tests enters Waiting. The approver reviews the sub-DAG's results before deploy proceeds.
This pattern is useful when you want human review over a complex operation that involves multiple internal steps — tests, builds, migrations — without adding approval to each individual sub-step.
Approval Before a Sub-DAG
The reverse pattern: approve first, then trigger multi-step execution. Place approval on the step before a call:
steps:
- id: review_config
run: ./validate-deploy-config.sh production
approval:
prompt: "Config validated. Approve production deployment?"
input: [DEPLOY_VERSION]
required: [DEPLOY_VERSION]
- id: deploy_pipeline
action: dag.run
with:
dag: production-deploy
params: "deploy_version=${env.DEPLOY_VERSION}"
depends: review_configvalidate-deploy-config.sh runs and shows the configuration diff. The approver reviews it, provides DEPLOY_VERSION, and approves. Then production-deploy (a full deployment pipeline with its own steps) executes with the approved version.
Push-back
Push-back resets a waiting step to Not Started and re-executes it. By default, the approval step itself is rerun. If approval.rewind_to is configured, Dagu resets that earlier step and all of its transitive dependents instead. This is useful when a step's output needs revision and the reviewer wants to restart from an earlier preparation step.
Push-back is only available on steps with the approval field.
How Push-back Works
- A step executes and enters
Waiting - The approver reviews the output and pushes back with input parameters
- The configured restart point resets to
Not Started - All transitive downstream dependents of that restart point also reset to
Not Started - Every step that is reset by the push-back receives push-back context when it executes again
- The
approvalIterationcounter increments (starts at 0, becomes 1 after first push-back) - The step enters
Waitingagain — the approver can approve, push back again, or reject
Example: Rewind to an Earlier Step
steps:
- id: prepare_report
run: ./prepare-report.sh
- id: draft_report
run: ./draft-report.sh
approval:
prompt: "Review the draft report"
input: [FEEDBACK]
rewind_to: prepare_report
depends: prepare_report
- id: publish_report
run: ./publish-report.sh
depends: draft_reportIf the reviewer pushes draft_report back, Dagu resets prepare_report, draft_report, and publish_report to Not Started. prepare_report reruns immediately, draft_report reruns and waits for approval again, and publish_report runs later after approval is granted. Each rewound step receives the push-back context when it executes.
Push-back Environment
Every step re-executed because of push-back receives:
- Each provided push-back input as an environment variable, such as
FEEDBACKorSINCE DAG_PUSHBACK, a JSON payload describing the latest push-back and the full historyDAG_PUSHBACK_ITERATION, the current push-back count as a plain integer stringDAG_PUSHBACK_PREVIOUS_STDOUT_FILE, when the current step had a stdout log before it was reset
If the current step declares approval.input, only those declared keys are exposed on that step. Steps without an input allowlist receive all provided push-back keys.
For action: chat.completion and action: harness.run, Dagu also passes the push-back context directly to the executor. This means reviewer feedback is incorporated without adding DAG glue code that references ${FEEDBACK} manually. If approval.rewind_to restarts an upstream AI step, that rewound AI step receives the context even if it does not declare its own approval.
Harness steps receive the previous stdout as a log file path only. Dagu does not inline the previous stdout content into the prompt or environment because harness output can be large.
DAG_PUSHBACK looks like this:
{
"iteration": 2,
"by": "reviewer",
"at": "2026-04-26T06:18:43Z",
"inputs": {
"FEEDBACK": "Tighten the executive summary",
"FORMAT": "markdown"
},
"history": [
{
"iteration": 1,
"by": "reviewer",
"at": "2026-04-26T06:12:10Z",
"inputs": {
"FEEDBACK": "Add error counts",
"FORMAT": "markdown"
}
},
{
"iteration": 2,
"by": "reviewer",
"at": "2026-04-26T06:18:43Z",
"inputs": {
"FEEDBACK": "Tighten the executive summary",
"FORMAT": "markdown"
}
}
]
}Field meanings:
iteration: Current push-back count for this runby: Authenticated user who submitted the latest push-back, when availableat: Server-generated UTC timestamp in RFC3339 formatinputs: Latest push-back inputs visible to the current stephistory: Chronological push-back history, oldest first
See Runtime Context and Variables for the push-back variable reference.
Example
A step queries metrics and outputs a summary. The approver can push back with different parameters until the output looks right:
tools:
- jqlang/jq@jq-1.7.1
steps:
- id: query_metrics
run: |
SINCE="${SINCE:-7d}"
GROUPING="${GROUPING:-daily}"
echo "Querying error rates (since=$SINCE, grouping=$GROUPING)"
curl -s "https://metrics.internal/api/errors?since=$SINCE&group=$GROUPING" \
| jq '.[] | "\(.date): \(.count) errors (\(.rate)%)"'
approval:
prompt: "Review error rate summary. Push back to adjust query parameters."
input: [SINCE, GROUPING]
- id: send_report
run: ./send-to-slack.sh
depends: query_metricsFirst run: SINCE and GROUPING are unset, so the script defaults to 7d and daily. The approver reviews the output and pushes back with SINCE=30d and GROUPING=weekly. The step re-runs with those values, producing a different summary. The approver can push back again or approve.
Example: Iterating on LLM Output
Use action: chat.completion or action: harness.run when you want Dagu to pass approval feedback to the AI step automatically. For shell commands that call an LLM CLI directly, wire the feedback environment variables into the command yourself:
steps:
- id: draft_changelog
run: |
PROMPT="Generate a changelog from these git commits for a public release blog post."
if [ -n "$FEEDBACK" ]; then
PROMPT="$PROMPT Incorporate this feedback: $FEEDBACK"
fi
git log --oneline v1.2.0..HEAD | claude -p "$PROMPT"
approval:
prompt: "Review the draft changelog. Push back with feedback to revise."
input: [FEEDBACK]
- id: publish
run: ./publish-changelog.sh
depends: draft_changelogFirst run: Claude generates a changelog from the git log. The reviewer reads the output in the Approval tab and pushes back with FEEDBACK="Make it more concise and group by feature area". The step re-runs, this time passing the feedback into the prompt. This loop continues until the reviewer approves.
REST API
curl -X POST "http://localhost:8080/api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/push-back" \
-H "Content-Type: application/json" \
-d '{
"inputs": {
"SINCE": "30d",
"GROUPING": "weekly"
}
}'Response:
{
"dagRunId": "...",
"stepName": "query_metrics",
"approvalIteration": 1,
"resumed": true
}| Field | Type | Description |
|---|---|---|
approvalIteration | integer | How many times this step has been pushed back |
resumed | boolean | Whether the DAG run was re-enqueued for execution |
For sub-DAG runs, use the sub-DAG endpoint:
POST /api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs/{subDAGRunId}/steps/{stepName}/push-backApproval and Rejection
Web UI
When steps enter Waiting status, an Approval tab appears in the DAG run view. The tab shows:
- Each waiting step with its name and prompt
- The step's stdout output inline
- Approve and Retry (push-back) buttons per step
- The current approval iteration count (if pushed back)
- Push-back history entries with iteration, actor, timestamp, and filtered inputs
The run details show the actor name for approval, rejection, and push-back operations. When an actor ID is available, hover over the name to view it.
To reject all waiting steps at once, use the Reject button in the DAG run action bar (replaces the Stop button when the DAG is in Waiting status). An optional rejection reason can be provided.
REST API
Approve a Step
curl -X POST "http://localhost:8080/api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/approve" \
-H "Content-Type: application/json" \
-d '{
"inputs": {
"APPROVED_BY": "john@example.com"
}
}'Reject a Step
curl -X POST "http://localhost:8080/api/v1/dag-runs/{name}/{dagRunId}/steps/{stepName}/reject" \
-H "Content-Type: application/json" \
-d '{
"reason": "Deployment blocked due to pending security review"
}'The reason field is optional.
For sub-DAG runs, use the sub-DAG variants:
POST /api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs/{subDAGRunId}/steps/{stepName}/approve
POST /api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs/{subDAGRunId}/steps/{stepName}/rejectDAG-run node responses expose actor names and IDs in approvedBy/approvedById, rejectedBy/rejectedById, and each pushBackHistory entry's by/byId. IDs may be absent on older runs or when authentication is disabled.
Email Notifications
Configure email notifications when a workflow enters wait status:
mail_on:
wait: true
wait_mail:
from: dagu@example.com
to:
- approvers@example.com
prefix: "[APPROVAL REQUIRED]"See Email Notifications for details.
Wait Handler
Execute custom logic when the workflow enters wait status:
handler_on:
wait:
run: |
echo "Waiting steps: $DAG_WAITING_STEPS"
curl -X POST https://slack.com/webhook \
-d '{"text": "Approval required for ${context.dag.name}"}'
steps:
- id: deploy
run: ./deploy.sh
approval:
prompt: "Approve deployment"The DAG_WAITING_STEPS environment variable contains a comma-separated list of waiting step names.
See Lifecycle Handlers for details.
Rejection Behavior
When a step is rejected:
- The step status changes to
Rejected - The overall DAG status becomes
Rejected - All dependent steps are marked as
Abortedand will not execute - The
onFailurehandler is executed (if configured)
The following information is recorded:
| Field | Description |
|---|---|
rejectedAt | Timestamp of the rejection |
rejectedBy | Username of the person who rejected (if authenticated) |
rejectedById | ID of the person who rejected (if authenticated) |
rejectionReason | Optional reason provided during rejection |
Limitations
- Steps with
approvalcannot useworker_selector(distributed execution) because approval state is stored locally
See Also
- Lifecycle Handlers — Execute handlers on wait status
- Email Notifications — Configure wait status emails
- Step Types — Built-in execution types

