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

# Repeat a workflow

> Start a new run of the same workflow each time one succeeds, until a final ending

Some work comes in pieces, such as every sub ticket of an epic, shipped one per run. Add a `[repeat]` table, and each time a run of the workflow ends on a successful terminal, Orbital starts a new run of the same workflow with the same inputs.

```toml title="repeat.toml"
[repeat]
wait = "5m"
max_runs = 50
```

## How the chain ends

A repeating workflow says it has finished with a terminal that sets `final = true`. A run that reaches one ends the chain. A run that ends `failed` or `aborted` ends it too, so a broken run is never repeated. Validation requires at least one `final` terminal, so every repeating workflow has a way to say the work is done, and `final` is allowed only in a repeating workflow.

| The run ends | What happens next |
| --- | --- |
| On a `success` terminal | Orbital starts the next run after `wait`. |
| On a terminal with `final = true` | The chain ends. |
| `failed` or `aborted` | The chain ends. |
| As run number `max_runs` | The chain ends. |

`wait` is the pause between one run and the next, such as `"5m"`; without it the next run starts straight away. `max_runs` caps the chain, 100 by default.

## Each run starts with the same inputs

Every run in the chain starts with the inputs the first run was given. It does not start from what the previous run learned, so the workflow finds out for itself what is left to do. A `ticket` or `epic` input is looked up again when each run starts, so each run reads the ticket's title and description as they stand then. A run whose ticket can no longer be read does not start, and the chain ends there.

Each run is a separate run in the Runs list, with its own history, and each run's page shows the chain it belongs to.

## Before you start

You need a project whose primary folder is a Git repository with a GitHub remote you can push to, Git credentials, a harness you have signed in to, and an epic, a GitHub or Linear issue whose sub tickets describe work in that repository. See [Projects and folders](/projects/).

## 1. Download the example

This workflow ships an epic one sub ticket per run. [Download the example](/repeat.zip) and unpack it into `~/.orbital/workflows/`. Keep every relative path.

![pick chooses the next sub ticket, deliver ships it, and the run ends at shipped; when no sub ticket is left or the rest wait on people, the run ends at a final terminal.](/repeat.svg?v=1008caaf64)

```toml title="repeat.toml"
version = 3
entry = "steps.pick"
description = "Ship an epic one sub ticket per run until none is left"

[repeat]
wait = "5m"
max_runs = 50

[inputs.epic]
kind = "epic"
label = "Epic"
hint = "A GitHub or Linear epic whose sub tickets this workflow ships"

[steps.pick]
style = "planner"
prompt = """
Ship the sub tickets of {{ inputs.epic.key }}, {{ inputs.epic.title }}, one per run.
The epic is at {{ inputs.epic.url }} and reads:

{{ inputs.epic.body }}

Read its sub tickets as they stand now and pick the next one to ship.
Prefer tickets whose dependencies have merged.
Answer none when every sub ticket is shipped, or blocked when the rest wait on people.
"""
routes = [
  { if = "next == 'ticket'",  to = "steps.deliver" },
  { if = "next == 'none'",    to = "steps.all_shipped" },
  { if = "next == 'blocked'", to = "steps.waiting_on_people" },
]

[steps.pick.outputs.next]
kind = "choice"
options.ticket = "A sub ticket is ready to ship"
options.none = "Every sub ticket is shipped"
options.blocked = "The remaining sub tickets wait on people"

[steps.pick.outputs.ticket]
kind = "ticket"
description = "The sub ticket to ship next"
required_if = "next == 'ticket'"

[steps.deliver]
import = "imports/ship-ticket.toml"
inputs.ticket = "{{ context.ticket }}"
exits.shipped = "steps.shipped"
exits.rejected = "steps.stuck"

[steps.shipped]
kind = "terminal"
label = "Shipped one ticket"

[steps.all_shipped]
kind = "terminal"
final = true
label = "Every sub ticket of the epic is shipped"

[steps.waiting_on_people]
kind = "terminal"
final = true
label = "The remaining sub tickets wait on people"

[steps.stuck]
kind = "terminal"
outcome = "failed"
label = "A ticket could not be shipped"
```

`inputs.epic` has `kind = "epic"`, so the new-run form asks for the epic's address and Orbital looks the epic up when each run starts. `pick` reads its key, title, address and description from the input, then reads the sub tickets itself, because Orbital does not list them.

`pick` answers `next` with `ticket`, `none` or `blocked`, and hands over the ticket to ship as its `ticket` output. `required_if = "next == 'ticket'"` makes that output required only when there is a ticket to ship, so `pick` can answer `none` or `blocked` without one. Because the output's kind is `ticket`, the handoff refuses a value that is not written as a ticket reference.

`deliver` imports `imports/ship-ticket.toml` and passes the ticket in as its `ticket` input. That input renders the reference `pick` gave, and the agent reads the ticket itself: Orbital looks up only the inputs a run starts with. A shipped ticket ends the run at `shipped`, a success terminal without `final`, so Orbital starts the next run five minutes later. `all_shipped` and `waiting_on_people` are final, so the chain ends once nothing is left or the rest wait on people. A ticket that cannot be shipped ends at `stuck`, a failed terminal, which ends the chain too, so you can look before anything else ships.

The imported workflow takes a checkout, implements and commits the ticket, reviews it, then pushes the branch, opens a pull request and asks GitHub to squash-merge it:

```toml title="imports/ship-ticket.toml"
version = 3
entry = "steps.checkout"
description = "Implement one ticket, review it and open its pull request"

[inputs.ticket]
kind = "ticket"
label = "Ticket"

[steps.checkout]
kind = "action"
do = "worktree.create"
next = "steps.implement"

[steps.implement]
style = "implementer"
prompt = """
Implement the ticket {{ inputs.ticket }}. Read it first, with its description and comments.

Commit the change using the repository's conventions. Do not push.
"""
next = "steps.review"

[steps.review]
style = "reviewer"
prompt = "Review the committed change against the ticket {{ inputs.ticket }}. Choose pass when it is ready to publish, or reject. Do not change files."
outputs.verdict = { kind = "choice", options = ["pass", "reject"] }
routes = [
  { if = "verdict == 'pass'",   to = "steps.push" },
  { if = "verdict == 'reject'", to = "steps.rejected" },
]

[steps.push]
kind = "action"
do = "git.push"
next = "steps.open"

[steps.open]
kind = "action"
do = "github.pr.open"
auto_merge = "squash"
title = { write = "A short title naming the change" }
body = { write = "Why the change exists and what it changes. End with Closes followed by the ticket reference." }
next = "steps.shipped"

[steps.shipped]
kind = "terminal"

[steps.rejected]
kind = "terminal"
outcome = "failed"
```

[Push and open pull requests without an agent](/workflows/use-actions/) explains its actions, and [Reuse a workflow](/workflows/reuse-a-workflow/) explains the import.

## 2. Validate it

```sh
orbital validate ~/.orbital/workflows/repeat.toml
```

## 3. Run it

Choose **New task** and pick the project on the repository. Choose the `repeat` workflow, set **Epic** to the epic's address, and write the goal.

Each shipped ticket is its own run in the Runs list, five minutes apart. The chain stops at the first final ending, at the first run that fails, or after fifty runs.

## Related

- [`[repeat]`](/reference/workflow-file/#repeat): the keys and the rules for a chain of runs.
- [Terminal](/reference/step-kinds/#terminal): `outcome` and `final`.
- [Pass context between steps](/workflows/pass-context/): outputs, `required_if` and ticket outputs.
- [Push and open pull requests without an agent](/workflows/use-actions/): the actions the imported workflow uses.
- [Reuse a workflow](/workflows/reuse-a-workflow/): import steps, inputs and exits.
