Skip to content
Orbital

Outer loop over an epic

An outer loop that delivers an epic's tickets through a sub workflow

In this example you work through all of an epic’s sub tickets in one run. The outer loop selects the next unfinished sub ticket, hands it to an imported delivery workflow, and comes back to select again. It stops when the select step reports that nothing remains.

About five minutes to set up. The run can take a long time, one delivery per sub ticket.

epic.toml
version = 3
entry = "steps.select"
description = "Work through an epic one sub ticket at a time"
max_visits = 200
[inputs.work]
kind = "epic"
label = "Epic"
hint = "The epic whose sub tickets this run delivers"
[steps.select]
prompt = """
Read the epic at {{ inputs.work.url }} and every sub ticket under it.
Choose the next unfinished sub ticket and report it as the current work.
Choose ticket while one remains, or none once every sub ticket is finished.
"""
outputs.current_work = "work"
outputs.remaining = { kind = "choice", options = ["ticket", "none"] }
routes = [
{ if = "remaining == 'ticket'", to = "steps.ticket" },
{ if = "remaining == 'none'", to = "steps.finished" },
]
[steps.ticket]
import = "imports/ticket.toml"
exits.delivered = { to = "steps.select", loop_restart = true }
exits.rejected = "steps.stalled"
[steps.finished]
kind = "terminal"
[steps.stalled]
kind = "terminal"
outcome = "failed"
label = "A sub ticket could not be delivered"

select routes a remaining ticket into the imported sub workflow and returns; no remaining ticket ends the run, and a sub ticket the reviewer will not pass ends it failed.

You need:

  • a signed-in harness that can read your tracker;
  • a project whose primary folder holds the code the sub tickets change. The example makes no worktree, so the agent changes files in that folder itself. A plain folder works, except with Codex. See plain folders and Git;
  • the workflow: download the archive. It holds epic.toml and imports/ticket.toml.
  1. Unpack the archive into your workflow library, ~/.orbital/workflows/. Keep every relative path, so imports/ticket.toml sits beside epic.toml.

  2. Check it:

    Terminal window
    orbital validate ~/.orbital/workflows/epic.toml
    The workflow editor showing a workflow with an imported sub workflow drawn as one step.
    The imported sub workflow shows as one step in its parent.

The work input has the epic kind, so Orbital looks the epic up when the run starts and refuses an address it cannot read. The select prompt reads that address as {{ inputs.work.url }}.

select declares two outputs. remaining is a choice of ticket or none, and its two routes answer both, so the step needs no next. current_work matters more. The work kind accepts a validated work scope rather than free text. A select step that returns a vague description, or a ticket URL that is its own parent, fails validation instead of sending the implementing agent somewhere unhelpful. Any prompt downstream renders that scope with {{ current_work }}.

ticket is an import step. import = "imports/ticket.toml" brings the other file’s steps into this workflow as a group, so they run as ticket.implement and ticket.review. Each terminal of the imported file is an exit named by its id, and the import step’s exits say where each one goes: delivered returns to the select step, and rejected goes to stalled, which ends the run Failed. An exit without a mapping would end the run with that terminal’s outcome.

The delivered exit sets loop_restart = true. That clears the previous sub ticket’s context, the history of its steps and its agent sessions. It keeps the original inputs, the run identity, visit counts and budgets. Without it, the tenth sub ticket would carry nine sub tickets of history into every prompt.

Bounding the inner review loop needs a budget rather than a visit cap. max_visits counts every visit to a step for the life of the run, and a restarted loop keeps those counts. So a cap of four on ticket.implement would bound the whole epic at four sub tickets rather than four attempts at one of them. The imported file declares [budgets.sub_ticket] instead, and three routes leave review. A reject spends a round and goes back to implement. Once the five rounds are spent, that route is skipped and the next reject route ends at rejected. A pass resets the budget, so the next sub ticket starts fresh. The workflow’s max_visits remains the outer guard on total agent turns.

imports/ticket.toml
version = 3
entry = "steps.implement"
description = "Implement and locally review one sub ticket"
[budgets.sub_ticket]
rounds = 5
[steps.implement]
prompt = "Implement the current work described below. Satisfy only its requirements. {{ current_work }}"
outputs.implementation_summary = "text"
next = "steps.review"
[steps.review]
prompt = "Review the implementation against the current work described below. Choose pass when it satisfies the work, or reject when it does not. {{ current_work }}"
outputs.verdict = { kind = "choice", options = ["pass", "reject"] }
routes = [
{ if = "verdict == 'pass'", to = "steps.delivered", resets = "budgets.sub_ticket" },
{ if = "verdict == 'reject'", to = "steps.implement", spends = "budgets.sub_ticket" },
{ if = "verdict == 'reject'", to = "steps.rejected" },
]
[steps.delivered]
kind = "terminal"
[steps.rejected]
kind = "terminal"
outcome = "failed"
label = "Five reviews rejected the same sub ticket"

Download epic.toml or ticket.toml on its own.

Replace the sub workflow with your own delivery workflow. The ticket workflow is the realistic one: it publishes a pull request, watches its state and merges.

Choose New task and pick the project that holds the code the sub tickets change. Choose the epic workflow, set work to the epic’s URL and write the goal.

Each iteration selects one sub ticket, implements and reviews it, then returns to the select step with a cleared context. A select step that reports none ends the run Succeeded. When the reviewer keeps rejecting the same sub ticket after five repairs, the budget is spent and the run ends Failed at stalled, labelled “A sub ticket could not be delivered”.