Skip to content

Closed Loops ​

In a closed loop, something outside a coding agent checks each result, and what it found goes back to the agent for another attempt. The agent no longer decides on its own that the work is done. It keeps working from evidence until the check passes or the attempts run out.

The term comes from control engineering. A heater on a timer runs open loop: it never learns the temperature of the room. A thermostat measures the room and corrects, so it runs closed. Closed does not mean unattended, as the word sometimes does in operations. The check can be a person, and for work that ships, one of the checks usually should be.

A coding agent in a harness.run step produces a result. A signal from outside the agent measures it: a command in repeat_policy, a reviewer agent with output_schema, or a person in a human.task step. A failure takes the route back to the agent with what the check found. A pass lets the next step run. When the limit is reached, the workflow fails or hands the work over.

The Three Parts ​

A closed loop needs all three parts. Take one away and what remains is the agent grading itself, a plain retry, or a loop with no end.

  1. A signal from outside the agent. A command's exit code, a reviewer's verdict, or a person's decision says whether the work is right. The agent's own report does not count: models that review their own work without outside feedback do not reliably improve it, and often make it worse (Huang et al.).
  2. A route back to the agent. What the signal found reaches the next attempt. An exit code decides whether there is another attempt. The output tells that attempt what to fix.
  3. A limit. The attempts are bounded, and running out is an outcome the workflow handles: the run fails, or a person takes over.

Close an Open Loop ​

This step is an open loop. Codex edits the repository, and the step succeeds when the CLI exits 0, whether or not the tests pass:

yaml
working_dir: /srv/repos/app

steps:
  - id: fix
    action: harness.run
    with:
      provider: codex
      sandbox: workspace-write
      prompt: |
        Make `make test` pass. Do not edit files under tests/.
    timeout_sec: 1200

The highlighted lines close it:

yaml
working_dir: /srv/repos/app

steps:
  - id: fix
    action: harness.run
    with:
      provider: codex
      sandbox: workspace-write
      prompt: |
        Make `make test` pass. Do not edit files under tests/.
        If ${context.paths.artifacts_dir}/test.log exists, it holds the
        output of the last failed `make test`. Read it first.
    timeout_sec: 1200
    repeat_policy:
      repeat: until
      condition: make test > "${context.paths.artifacts_dir}/test.log" 2>&1
      limit: 5

  - id: tests_pass
    depends: [fix]
    run: make test
  • Signal. Dagu runs condition after every attempt, and exit code 0 ends the loop. The agent cannot skip the check or report a result in place of it.
  • Route back. The condition writes the output of make test to test.log, and two lines of the prompt tell the next attempt to read it. Without them the loop still repeats, but each attempt has to work out for itself what failed.
  • Limit. limit: 5 stops the loop, but it does not judge the work: a step that runs out of attempts still ends Succeeded. tests_pass runs the check once more, which fails the run if the tests still fail.

Inner Loops covers this form in full.

Close a Gate ​

A person can close the loop too. This is the release notes gate from Open Loops, where the person could only complete the task or stop the run. with.push_back lets them send the draft back:

yaml
working_dir: /srv/repos/app

steps:
  - id: draft
    action: harness.run
    with:
      provider: claude
      model: sonnet
      prompt: |
        Draft release notes for the changes since the last tag.
        Group them as Features, Fixes, and Breaking changes.
    stdout:
      artifact: release-notes.md

  - id: confirm
    depends: [draft]
    action: human.task
    with:
      prompt: Publish these release notes?
      artifacts:
        - release-notes.md
      push_back:
        rewind_to: draft
        form:
          type: object
          required: [feedback]
          properties:
            feedback:
              type: string

  - id: publish
    depends: [confirm]
    run: ./publish-notes.sh "${DAG_RUN_ARTIFACTS_DIR}/release-notes.md"
  • Signal. The person's decision, made with the draft open beside the prompt.
  • Route back. push_back reruns draft and every step after it, with the feedback appended to the agent's prompt. No wiring carries it.
  • Limit. The person. The task opens again after each rerun, and the loop continues only when someone sends the work back. Completing the task or stopping the run ends it.

Outer Loops covers this form in full.

Choose What Closes It ​

Closed bySignalRoute backLimitGuide
A commandThe exit code of repeat_policy.conditionThe condition writes its output to a file, and the prompt names the filerepeat_policy.limitInner Loops
A reviewer agentA verdict that output_schema validatesA review.json that the next round readsrepeat_policy.limit on a dag.run stepWhen the Check Needs Judgment
A personCompleting or pushing back a human.taskwith.push_back appends the feedback to the promptThe person completes the task or stops the runOuter Loops

Use the cheapest one that can decide, and when a run needs more than one, stack them in this order. A command costs little and cannot be talked round. A reviewer agent spends tokens and can be wrong. A person's attention is the scarcest of the three. Run in this order, each one sees only work that the one before it passed.

Any Command Can Close It ​

Tests are the common case, not the only one. Anything that exits 0 for good work and non-zero for bad work can be the condition:

ResultCheck
Codemake test, go vet ./..., tsc --noEmit
A pull requestgh pr checks --watch --fail-fast
A JSON replycheck-jsonschema --schemafile schema.json reply.json
A Dagu workflowdagu validate workflow.yaml
A performance targetA script that compares a benchmark with a threshold

The more exactly a check says what is wrong, the more it gives the next attempt. test -f report.md can end a loop, but it tells the agent nothing.

This loop closes around a structured reply. The agent never edits a file, and a validator takes the place of the tests:

yaml
working_dir: /srv/repos/app

steps:
  - id: triage
    action: harness.run
    with:
      provider: codex
      sandbox: read-only
      prompt: |
        Triage the bug report in REPORT.md. Return only one JSON object:
        {"severity": "low|high", "area": "...", "summary": "..."}
        If ${context.paths.artifacts_dir}/errors.txt exists, your last reply
        failed validation with those errors. Fix them.
    stdout:
      artifact: triage.json
    repeat_policy:
      repeat: until
      condition: >-
        check-jsonschema --schemafile triage.schema.json
        "${context.paths.artifacts_dir}/triage.json"
        > "${context.paths.artifacts_dir}/errors.txt" 2>&1
      limit: 3

  - id: valid
    depends: [triage]
    run: check-jsonschema --schemafile triage.schema.json "${DAG_RUN_ARTIFACTS_DIR}/triage.json"

Each attempt replaces triage.json, so the condition always checks the latest reply. After a bad one, the next attempt reads the validator's message:

text
Schema validation errors were encountered.
  /…/triage.json::$: 'summary' is a required property
  /…/triage.json::$.severity: 'critical' is not one of ['low', 'high']

output_schema checks the same thing as a gate: it fails the step, and a retry sends the same prompt again. The difference between the two is the route back.

Loops That Only Look Closed ​

These are easy to build by accident, and most of them finish green.

Looks closedWhat is missingTo close it
The prompt says "run the tests until they pass"Signal. The agent decides when to stop, and reports its own result.Make the tests the repeat_policy condition.
The agent reviews its own workSignal. A model is judging its own output.Use a command, or a reviewer from another provider.
The agent can edit what the check runsSignal. The agent can change the answer.Keep the check honest.
condition: make test | tee test.logSignal. A pipeline exits with the status of its last command, so the check always passes and the loop ends after one attempt. The same goes for | tail.Redirect with >, or set shell: bash -o pipefail on the DAG.
condition: make testRoute back. The check can end the loop, but the next attempt is not told what failed.Write the output to a file, and name the file in the prompt.
The prompt says only "implement TASK.md"Route back. The failure reaches the agent, but nothing tells it to act. When the check tests more than the task, the agent can read the failure, call it out of scope, and spend every attempt saying so.Say that the check decides.
retry_policy or with.fallbackRoute back. The next attempt gets the same prompt.Keep them for crashes and outages, and add repeat_policy for the work itself.
A test step after the agentRoute back. It is a gate: it can fail the run, but it cannot send anything back.Make the test the condition.
output_schemaRoute back. It is a gate: the step fails, and the agent never learns why.Run a validator in the condition.
human.task without with.push_backRoute back. It is a gate: the person can complete the task or stop the run.Add with.push_back.
A nightly scheduleRoute back. Nothing about last night's result reaches tonight's run.Keep results where the next run reads them, and close a loop inside each run.
repeat_policy without limitLimit. The step repeats until the condition is met or the run stops.Set limit.
limit with nothing after the loopLimit. Running out ends Succeeded, and the run carries on with failing work.Check again in a later step, or hand the work to a person.

Three questions tell you whether a loop is closed:

  1. Could the agent change the check's answer?
  2. What does the next attempt know that this one did not?
  3. What happens when the attempts run out?

Good answers are no, what the check found, and something the workflow handles.

Closed Loops Nest ​

One run usually holds several closed loops at different speeds. A command closes the fastest, around each attempt. A person closes a slower one around that, and each push-back starts the fast one again.

  • Inner Loops: a command or a reviewer agent closes the loop, in minutes, with nobody watching.
  • Outer Loops: a person closes the loop, in hours or days, around work that already passed the inner loop.
  • Background Loops: a schedule or an event starts the run, and the loops inside it close as usual. What ships changes what the next run finds.

See Also ​

  • Open Loops: when a result does not need to go back to the agent.
  • Repetition: every repeat_policy field.
  • Human Tasks: forms, artifacts, and push-back in full.

Dagu is open source under the GNU General Public License v3.0.