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.
[repeat]wait = "5m"max_runs = 50How the chain ends
Section titled “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
Section titled “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
Section titled “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.
1. Download the example
Section titled “1. Download the example”This workflow ships an epic one sub ticket per run. Download the example and unpack it into ~/.orbital/workflows/. Keep every relative path.
version = 3entry = "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 = truelabel = "Every sub ticket of the epic is shipped"
[steps.waiting_on_people]kind = "terminal"final = truelabel = "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:
version = 3entry = "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 explains its actions, and Reuse a workflow explains the import.
2. Validate it
Section titled “2. Validate it”orbital validate ~/.orbital/workflows/repeat.toml3. Run it
Section titled “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
Section titled “Related”[repeat]: the keys and the rules for a chain of runs.- Terminal:
outcomeandfinal. - Pass context between steps: outputs,
required_ifand ticket outputs. - Push and open pull requests without an agent: the actions the imported workflow uses.
- Reuse a workflow: import steps, inputs and exits.