Approval
Add approval to any step to pause execution after the step completes and wait for human review.
Usage
steps:
- id: deploy_staging
command: ./deploy.sh staging
approval:
prompt: "Verify staging deployment before production"
- id: deploy_prod
depends: [deploy_staging]
command: ./deploy.sh productionThe 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:
type: graph
steps:
- id: generate_plan
command: ./generate-migration-plan.sh
approval:
prompt: "Review migration plan"
input: [APPROVED_BY, MAINTENANCE_WINDOW]
required: [APPROVED_BY]
- id: execute_migration
depends: [generate_plan]
command: ./migrate.sh --approver "${APPROVED_BY}" --window "${MAINTENANCE_WINDOW}"APPROVED_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:
type: graph
steps:
- id: run_integration_tests
call: integration-test-suite
approval:
prompt: "Review test results before deploying"
- id: deploy
depends: [run_integration_tests]
command: ./deploy.sh productionThe 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:
type: graph
steps:
- id: review_config
command: ./validate-deploy-config.sh production
approval:
prompt: "Config validated. Approve production deployment?"
input: [DEPLOY_VERSION]
required: [DEPLOY_VERSION]
- id: deploy_pipeline
depends: [review_config]
call: production-deploy
params: "deploy_version=${DEPLOY_VERSION}"validate-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
command: ./prepare-report.sh
- id: draft_report
depends: [prepare_report]
command: ./draft-report.sh
approval:
prompt: "Review the draft report"
input: [FEEDBACK]
rewind_to: prepare_report
- id: publish_report
depends: [draft_report]
command: ./publish-report.shIf 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 history
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.
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 Special Environment Variables for the runtime variable reference.
Example
A step queries metrics and outputs a summary. The approver can push back with different parameters until the output looks right:
steps:
- id: query_metrics
script: |
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
depends: [query_metrics]
command: ./send-to-slack.shFirst 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 claude -p in a command step to generate content, then push back with feedback to regenerate:
steps:
- id: draft_changelog
script: |
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
depends: [draft_changelog]
command: ./publish-changelog.shFirst 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
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}/rejectEmail 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:
command: |
echo "Waiting steps: ${DAG_WAITING_STEPS}"
curl -X POST https://slack.com/webhook \
-d '{"text": "Approval required for ${DAG_NAME}"}'
steps:
- id: deploy
command: ./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) |
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 Reference — All available step types
