Outer loop over an epic
An epic is a list of sub tickets, and one run can work through all of them. The outer loop selects the next unfinished sub ticket, hands it to an imported delivery graph, and comes back to select again. It stops when the selector reports that nothing remains.
Prerequisites
Section titled “Prerequisites”An authenticated harness with read access to your tracker, and a project folder for the repository the sub tickets change. This workflow can run for a long time; watch the first two iterations before leaving it alone.
select routes a remaining ticket into the imported sub graph and returns; no remaining ticket ends the run, and a sub ticket the reviewer will not pass ends it failed.
How it works
Section titled “How it works”select declares two outputs. remaining:choice decides the route, and Orbital infers ticket and none from the two outgoing conditions. current_work:work is the interesting one: the work kind accepts a validated work scope rather than free text, so a selector that returns a vague description or a ticket URL that is its own parent fails validation instead of sending the implementor somewhere unhelpful. Any prompt downstream renders that scope with {{ current_work }}.
ticket [import="subgraphs/ticket.dot"] is an import placeholder. The imported graph is flattened into the parent before validation, and its two terminals become the exits the parent’s edges bind: delivered returns to the selector, and rejected ends the run. Every exit has to be bound exactly once.
The return edge carries loop_restart="true". That clears the previous sub ticket’s context, stage history and thread sessions while keeping the original inputs, the run identity, visit counts and repair budget state. Without it, the tenth sub ticket would carry nine sub tickets of history into every prompt.
Bounding the inner review loop needs a repair budget rather than a visit cap. max_visits counts every visit to a node for the life of the run, and a restarted loop keeps those counts, so a cap of four on implement would bound the whole epic at four sub tickets rather than four attempts at one of them. The sub graph puts repair_budget="sub_ticket" on all three edges out of review instead: retry sends the work back, exhausted gives up after five rounds, and reset on the passing edge clears the budget so the next sub ticket starts fresh. The graph’s max_visits remains the outer guard on total agent turns.
Replace the sub graph with your own delivery workflow. The ticket workflow is the realistic one: it publishes a pull request, watches its state and merges.
Save, validate and run
Section titled “Save, validate and run”Download all files. Choose the repository the sub tickets change. Make an .orbital/ directory inside it and extract the archive there, keeping the subgraphs/ directory intact. From that project folder run:
orbital validate .orbital/workflow.dotAdd the repository folder to a project in Orbital. On New run select the project and workflow graph, choose your authenticated harness, set work to the epic’s URL and supply the goal.
Expected behaviour
Section titled “Expected behaviour”Each iteration selects one sub ticket, implements and reviews it, then returns to the selector with a cleared context. A selector that reports none ends the run successfully. A reviewer that rejects the same sub ticket five times exhausts its repair budget and ends the run at the failed terminal, naming the sub ticket that stalled.
Complete source
Section titled “Complete source”Every DOT file below is a complete runnable graph. The archive contains the same files. Prompt files are companions, not separate workflows.
workflow.dot
Section titled “workflow.dot”digraph epic { graph [version="1", entry="select", inputs="work", description="Work through an epic one sub ticket at a time", max_visits="200"]
select [prompt="Read the epic at {{ inputs.work }} 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,remaining:choice"] ticket [import="subgraphs/ticket.dot"] finished [shape="Msquare", outcome="success"] stalled [shape="Msquare", outcome="failed", label="A sub ticket could not be delivered"]
select -> ticket [condition="remaining == 'ticket'", weight="2"] select -> finished [condition="remaining == 'none'", weight="1"] ticket -> select [exit="delivered", loop_restart="true"] ticket -> stalled [exit="rejected"]}subgraphs/ticket.dot
Section titled “subgraphs/ticket.dot”digraph ticket { graph [version="1", entry="implement", description="Implement and locally review one sub ticket"]
implement [prompt="Implement the current work described below. Satisfy only its requirements. {{ current_work }}", outputs="implementation_summary:text"] 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:choice"] delivered [shape="Msquare", outcome="success"] rejected [shape="Msquare", outcome="failed", label="Five reviews rejected the same sub ticket"]
implement -> review review -> delivered [condition="verdict == 'pass'", weight="3", repair_budget="sub_ticket", repair_round="reset"] review -> implement [condition="verdict == 'reject'", weight="2", repair_budget="sub_ticket", repair_round="retry"] review -> rejected [condition="verdict == 'reject'", weight="1", repair_budget="sub_ticket", repair_round="exhausted"]}See sub graphs for the exit seam rules and loops for the other ways to bound a loop.