> For the index of every page in Orbital's docs, read https://docs.beta.runorbital.dev/llms.txt.

# Add a loop

> Send work back to an earlier step, and stop the loop after a set number of rounds

Send work back to an earlier step when a review rejects it. A loop is any edge that points back at an earlier step, and nothing else marks it.

```dot title="bounded-retries.dot"
review -> repair [condition="verdict == 'reject'", weight="3", repair_budget="answer", repair_round="retry"]
repair -> review
```

Writing a loop is easy. The work is in stopping it.

## How a loop stops

Orbital gives you two ways to stop a loop, and two ways to make a long loop cheaper.

### Repair rounds

A repair budget limits how many times one piece of work goes back for correction. It is the bound to reach for when an agent may need several corrections but must not correct forever.

You name the budget on three kinds of edge:

| Round | Edge | What it does |
| --- | --- | --- |
| `retry` | The edge that sends work back for repair. | Starts a repair round. |
| `exhausted` | A partner with the same condition and budget, and a different weight. | Runs once after five unsuccessful rounds, instead of the retry. |
| `reset` | The edge taken when the work has passed. | Clears the budget, so the next piece of work gets five rounds again. |

Put the retry and exhausted edges after the review that decides. Never put them on a fan-out or inside a parallel branch. The [`repair_round`](/reference/attributes/#repair_round) entry gives the full rules.

In the editor, each edge that loops back shows its condition and weight.

![The workflow editor shows a pull-request workflow in which conditioned edges such as review_verdict == 'reject' lead from later steps back to earlier ones.](/screenshots/workflow-editor-loop.webp?v=4dacb0ba6a)

*Edges that loop back carry a condition and a weight like any other edge.*

### Visit caps

`max_visits` on a node caps every visit to that node for the life of the run, whatever the reason. `max_visits` on the workflow caps the run's agent turns, 200 by default. Commands and waits do not spend it.

| Limit reached | Result |
| --- | --- |
| The workflow's limit | The run ends Failed. |
| A step's own limit | That visit fails, and the run is Paused with "Waiting on you", whatever failure edges the step has. |

Visit caps stay independent of repair budgets. Treat them as the outer guard, not as the way the loop is meant to stop. See [`max_visits`](/reference/attributes/#max_visits).

### Waits

A poll loop watches something outside the run, such as a pull request, a build or a queue. Put an `insulator` wait in it. Waiting spends no agent visits, and you can choose **Skip** to check again now. [Add gates and waits](/workflows/gates-and-waits/) covers the wait step itself.

![The run page's Waterfall tab shows a poll loop: probe, route and wait steps repeat as probe@2, route@2, wait@2 and so on, each with its duration.](/screenshots/run-steps.webp?v=fee718c962)

*Each pass of a poll loop adds a new visit to the probe, route and wait steps.*

### Loop restart

`loop_restart="true"` on the edge back clears the values the loop wrote to context, the history of finished steps that later prompts show, and the agents' sessions. It keeps the original inputs, the run's identity, the visit counts and the repair budgets.

Use it when the next pass is a new piece of work, not another attempt at the same one. Without it, each prompt in the loop carries the history of every earlier pass. See [`loop_restart`](/reference/attributes/#loop_restart).

## Failure is not a loop

A failed step takes a matching conditioned edge and never the ordinary fallback. When the workflow knows how to recover, route `outcome == 'failed'` to a repair prompt that reads `{{ context.failure_reason }}`. Then return to the observation or review that proves the recovery worked.

:::caution
Read the failure reason before you decide to retry. An authentication failure needs credentials. Asking the same unauthenticated harness again cannot repair it.
:::

Make exhaustion visible with a terminal or a step that reassesses the work. A failed terminal ends the run as Failed. A failed step with no edge for the failure leaves the run Paused, with "Waiting on you". [Troubleshooting](/troubleshooting/) explains how to tell them apart in the app, and [Error handling](/reference/error-handling/) gives the exact rules.

## Before you start

You need a harness you have signed in to, and a project in Orbital. Any project works. It does not need to be a Git repository, unless you run the example with Codex, which needs one. See [Projects and folders](/projects/).

## 1. Download the example

This workflow reviews an answer and sends rejections to a repair step. It gives up after five unsuccessful rounds instead of looping forever. [Download the example](/bounded-retries.zip) and unpack it into `~/.orbital/workflows/`. Keep every relative path.

![review passes to done or rejects into repair; repair returns to review. Five unsuccessful repair rounds lead to exhausted.](/bounded-retries.svg?v=32a54bfbd3)

```dot title="bounded-retries.dot"
digraph retries {
  graph [version="1", entry="review", max_visits="30"]
  review [tool_access="Read only", prompt="Review the goal and any previous repair. Choose pass or reject.", outputs="verdict:choice"]
  repair [tool_access="Read only", prompt="Revise the proposed answer to the goal."]
  done [shape="Msquare"]
  exhausted [shape="Msquare", outcome="failed"]
  review -> repair [condition="verdict == 'reject'", weight="3", repair_budget="answer", repair_round="retry"]
  review -> exhausted [condition="verdict == 'reject'", weight="2", repair_budget="answer", repair_round="exhausted"]
  review -> exhausted
  review -> done [condition="verdict == 'pass'", weight="1", repair_budget="answer", repair_round="reset"]
  repair -> review
}
```

Four edges leave `review`, and three of them name `repair_budget="answer"`:

| Edge | Condition | What it does |
| --- | --- | --- |
| `retry` | `verdict == 'reject'`, weight 3 | Starts a repair round. |
| `exhausted` | The same condition, weight 2 | Runs once after five unsuccessful rounds. |
| `reset` | `verdict == 'pass'`, weight 1 | Clears the budget, so a later review that rejects gets five rounds again. |

The fourth edge, `review -> exhausted`, has no condition, so it is the fallback. Validation does not count the retry and exhausted edges as answering `reject`, so `review` needs a fallback for the case where neither round is open. It points at `exhausted`, so a rejection that no round can take still ends the run Failed.

Both steps have the Read only tool access, so the repair revises its answer without changing files or running commands.

[Download bounded-retries.dot](/examples/bounded-retries/bounded-retries.dot)

## 2. Validate it

```sh
orbital validate ~/.orbital/workflows/bounded-retries.dot
```

## 3. Run it

Choose **New task**, pick any project, and choose the `bounded-retries` workflow and your harness. One project serves every example. Write the goal the review step should judge, and start the run.

The first passing verdict ends the run successfully. A rejection starts up to five repairs. A sixth rejecting review ends at `exhausted` with a failed outcome.

## Choosing how to stop a loop

| You want to | Use |
| --- | --- |
| Allow a few corrections of one piece of work | A repair budget with `retry`, `exhausted` and `reset` edges. |
| Guard against a loop that never ends | `max_visits` on the node or the workflow. |
| Check on something outside the run | A wait in the loop. |
| Start each pass as new work | `loop_restart="true"` on the edge back. |
| Recover from a failed step | An `outcome == 'failed'` edge to a repair prompt. |

## Related

- [Add branches](/workflows/add-branches/): how edges choose the next step.
- [Add gates and waits](/workflows/gates-and-waits/): pause a loop for time or for a person.
- [Pass context between steps](/workflows/pass-context/): what a loop restart clears from context.
- [Error handling](/reference/error-handling/): how failed steps and repair budgets behave.
