Outer Loops
An outer loop is closed by a person. Work that passed the inner loop's checks reaches a human task. The person completes it, or pushes it back with feedback. A push-back reruns the coding agent and every step after it, with the feedback added to the agent's prompt, and the task opens again with the new result.
The person's attention is the scarcest thing in the loop. The workflow should spend it only on work that machines have already checked, show it the evidence, and make sending work back as cheap as accepting it.
Push Back to the Agent
working_dir: /srv/repos/app
steps:
- id: base
run: git rev-parse HEAD
output: BASE
- id: implement
depends: [base]
action: harness.run
with:
provider: claude
model: sonnet
permission_mode: acceptEdits
allowed_tools: "Bash(make test)"
add_dir: ${context.paths.artifacts_dir}
prompt: |
Implement the change described in TASK.md.
If ${context.paths.artifacts_dir}/test.log exists, it holds the
output of the last failed `make test`. Read it first.
repeat_policy:
repeat: until
condition: make test > "${context.paths.artifacts_dir}/test.log" 2>&1
limit: 5
- id: evidence
depends: [implement]
run: |
make test
git diff ${BASE} > "${DAG_RUN_ARTIFACTS_DIR}/change.diff"
- id: review
depends: [evidence]
action: human.task
with:
prompt: |
Review the change for TASK.md. The tests pass.
Complete the task to open a pull request, or request changes.
artifacts:
- change.diff
- test.log
push_back:
rewind_to: implement
form:
type: object
required: [feedback]
properties:
feedback:
type: string
title: What should change?
- id: open_pr
depends: [review]
run: git push -u origin HEAD && gh pr create --fillimplement is the inner loop: it repeats until make test passes. evidence fails the run if the inner loop ran out of attempts, and otherwise saves the diff. The run then waits at review without holding a process, for minutes or for days.
The reviewer opens the run from Cockpit, reads change.diff and test.log on the Actions tab, and chooses:
- Complete task:
open_prruns. - Request changes: the feedback form opens. Submitting it sends the work back.
What a Push-back Does
implement,evidence, andreviewreset toNot Started.The run resumes from the queue.
implementruns again as a fresh inner loop:${context.step.repeat_index}starts at0, and up to five attempts run untilmake testpasses.Every attempt of that inner loop gets the feedback appended to its prompt:
textImplement the change described in TASK.md. If /…/test.log exists, it holds the output of the last failed `make test`. Read it first. ## Push-back Context Push-back iteration: 1 Previous stdout log: /…/implement.20261106.004626.876.034sOFZe.out Reviewer feedback: - feedback: Handle the empty input caseevidencesaves a new diff, andreviewopens again with the iteration count and the earlier feedback shown on the task.
No wiring carries the feedback: harness.run steps receive it automatically. The previous stdout is passed as a path, not inlined, because agent output can be large. Other steps receive it as environment variables; see Push-back Context.
Review the Plan First
A wrong plan produces a lot of wrong code, and reading a plan takes minutes where reading a diff takes an hour. Put the first checkpoint on the plan:
working_dir: /srv/repos/app
steps:
- id: plan
action: harness.run
with:
provider: claude
model: opus
disallowed_tools: "Edit Write"
prompt: |
Read TASK.md and the code it touches. Write an implementation plan:
the files to change, the approach, the risks, and how to test it.
Do not change any files. Reply with the plan in Markdown only.
stdout:
artifact: plan.md
- id: approve_plan
depends: [plan]
action: human.task
with:
prompt: Approve the plan, or send it back with what to change.
artifacts:
- plan.md
push_back:
rewind_to: plan
form:
type: object
required: [feedback]
properties:
feedback:
type: string
- id: implement
depends: [approve_plan]
action: harness.run
with:
provider: codex
sandbox: workspace-write
prompt: |
Implement the approved plan in ${context.paths.artifacts_dir}/plan.md.
If ${context.paths.artifacts_dir}/test.log exists, it holds the
output of the last failed `make test`. Read it first.
repeat_policy:
repeat: until
condition: make test > "${context.paths.artifacts_dir}/test.log" 2>&1
limit: 5
- id: tests_pass
depends: [implement]
run: make test
- id: review
depends: [tests_pass]
action: human.task
with:
prompt: Review the implementation of the approved plan.
artifacts:
- plan.md
- test.log
push_back:
rewind_to: implement
form:
type: object
required: [feedback]
properties:
feedback:
type: string
- id: open_pr
depends: [review]
run: git push -u origin HEAD && gh pr create --fillThe run has two outer loops, each with its own rewind target:
- At
approve_plan, a push-back reruns only the planner. A rerun replacesplan.mdwith the new plan. - At
review, a push-back rerunsimplementandtests_pass, and leaves the approved plan alone.
The planner and the implementer are different agents. Claude Code plans without edit tools, and Codex implements inside its workspace-write sandbox.
Hand Over When the Agent Gives Up
Not every task fits in five attempts. Instead of failing the run, route the work to a person when the inner loop runs out, or when the agent itself says it is stuck:
type: graph
working_dir: /srv/repos/app
steps:
- id: prepare
run: rm -f "${DAG_RUN_ARTIFACTS_DIR}/blocked.md"
- id: implement
depends: [prepare]
action: harness.run
with:
provider: claude
model: sonnet
permission_mode: acceptEdits
allowed_tools: "Bash(make test)"
add_dir: ${context.paths.artifacts_dir}
prompt: |
Implement the change described in TASK.md.
If ${context.paths.artifacts_dir}/test.log exists, read it first.
If you cannot finish without a decision from a person, write your
question to ${context.paths.artifacts_dir}/blocked.md and stop.
repeat_policy:
repeat: until
condition: test -f "${context.paths.artifacts_dir}/blocked.md" || make test > "${context.paths.artifacts_dir}/test.log" 2>&1
limit: 5
- id: check
depends: [implement]
run: |
if [ -f "${DAG_RUN_ARTIFACTS_DIR}/blocked.md" ]; then result=blocked
elif make test > /dev/null 2>&1; then result=pass
else result=fail
fi
echo "result=$result" >> "$DAGU_OUTPUT_FILE"
outputs:
- name: result
- id: route
depends: [check]
action: router.route
with:
value: ${steps.check.outputs.result}
routes:
pass: [open_pr]
fail: [rescue]
blocked: [rescue]
- id: rescue
action: human.task
with:
prompt: |
The agent stopped (${steps.check.outputs.result}).
Read its question or the last test output, then give it guidance.
Stop the run to abandon the task.
artifacts:
- blocked.md
- test.log
push_back:
rewind_to: prepare
form:
type: object
required: [guidance]
properties:
guidance:
type: string
title: Guidance for the agent
- id: open_pr
run: git push -u origin HEAD && gh pr create --fill- The condition ends the inner loop early when the agent writes
blocked.md, so a stuck agent does not spend its remaining attempts. checkturns the outcome into a value, androutesendspassstraight on andfailorblockedtorescue.- A push-back from
rescuererunsprepare, which clears the old question, and then the whole inner loop, with the guidance in the agent's prompt. - When the work passes,
rescueis skipped and nobody is asked anything.
Giving the agent a sanctioned way to stop is not only a convenience. When agents have an explicit "needs a person" exit, they cheat on impossible tests far less often (Zhong et al.).
Reach the Reviewer
A run that waits for a person is only as fast as the person finds out:
| Surface | How |
|---|---|
| Cockpit | The Review column lists every run waiting for a person. |
| Notification rules | Include the Waiting event. |
| Wait handler | handler_on.wait runs a step, with the waiting step names in DAG_WAITING_STEPS. Post them to chat. |
| MCP | Ask an MCP client what is waiting, then complete or push back from the chat with dagu_execute. |
| CLI | dagu human-task complete and dagu human-task push-back. |
From a script, pass --expected-iteration with the iteration that was reviewed. A late or repeated push-back then fails instead of sending back work that has already been redone.
Things to Know
- Human tasks run only in root DAGs. An inner loop in a child workflow is fine; the task that reviews it belongs to the root.
- A waiting run holds no process and has no timeout. It stays open until someone acts or the run is stopped.
- Completions and push-backs resume the run through the queue, so a scheduler must be running:
dagu scheduler, ordagu start-all. - The feedback of one push-back is limited to 16 KiB.
- A push-back discards the results of every step it resets, including other human tasks downstream.
See Also
- Human Tasks: forms, artifacts, completion, and push-back in full.
- Inner Loops: the automated loop that runs before the person sees the work.
- Background Loops: outer loops that a schedule or an event starts.

