Workflow file
Every key a workflow file takes, with its values and defaults
Look up any key you can write in a workflow file, with its values and default.
version = 3entry = "steps.review"
[steps.review]prompt = "Review the goal. Do not change files."outputs.verdict = { kind = "choice", options = ["pass", "reject"] }routes = [ { if = "verdict == 'pass'", to = "steps.done" }, { if = "verdict == 'reject'", to = "steps.rejected" },]
[steps.done]kind = "terminal"
[steps.rejected]kind = "terminal"outcome = "failed"A workflow is one TOML file named after the workflow, such as review.toml. Keys at the top of the file describe the workflow. Each table under steps is a step or a group of steps. Unknown keys are errors everywhere, and the message names the key and where it is.
Names are lowercase with underscores: step ids, group names, styles, budgets, inputs and outputs. Labels, descriptions and hints are ordinary text.
Top-level keys
Section titled “Top-level keys”version
Section titled “version”The version of the workflow format.
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
version |
number | Yes | None | Always 3. |
version = 3The first step of a run.
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
entry |
step reference | Yes | None | Names a step or an import step, never a group or a terminal. |
entry = "steps.plan"Related: Step ids and references.
description
Section titled “description”A description shown with the workflow.
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
description |
text | No | None | Shown in the workflow list and the new-run form. |
max_visits
Section titled “max_visits”The number of agent turns a run may take.
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
max_visits |
positive whole number | No | 200 |
Counts agent turns only. Probes, actions, commands, gates and waits do not spend it. |
When a run spends it, the run ends Failed. Treat it as a guard, not as the way a loop is meant to stop. A step can carry its own max_visits too.
Related: Add a loop, Error handling.
The style every step starts from.
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
style |
style name, or a list of names | No | None | In a list, a later style wins on a key both set. |
style = "balanced"The workflow’s style is where a workflow sets its harness, model, effort or permissions for every step. A step’s own styles and its groups’ styles win over it, and so does a model picked when the run starts.
Related: Styles.
Style files the workflow pulls in.
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
use |
list of paths | No | None | Relative to the workflow file. |
use = ["shared/team.toml"]Related: Style files.
Inputs
Section titled “Inputs”The values a run takes besides its goal. Each input is a table under inputs.
[inputs.ticket]kind = "ticket"label = "Ticket"hint = "The issue to deliver, for example https://github.com/acme/app/issues/628"
[inputs.max_rounds]kind = "number"label = "Repair rounds"min = 1max = 10default = 5
[inputs.notes]label = "Notes for the agent"required = false| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
kind |
one of the input kinds | No | text |
Decides the new-run form’s field and how Orbital checks the value. |
label |
text | No | The input’s name | The new-run form’s label. |
hint |
text | No | None | The help text under the field. |
required |
true or false |
No | true |
An optional input that is not given renders empty. |
default |
a value of the input’s kind | No | None | Used when the run starts without the input. |
options |
list, or table of option and description | With choice |
None | The values a choice input offers. |
min, max |
numbers | No | None | Bounds for a number input. |
from |
list of github, linear |
No | Both | The trackers a ticket, epic or project input accepts. |
goal is built in. [inputs.goal] may set its label, hint and default, and nothing else. Starting a run needs a goal and every required input, and refuses inputs the workflow does not declare. From the command line, pass each input with --input <name>=<value>.
Orbital copies the inputs into context when the run starts. {{ context.work }} can then change while {{ inputs.work }} keeps the original, and a loop restart restores the original inputs.
Any value in the file may read an input, written as a template: rounds = "{{ inputs.max_rounds }}". Validation checks that the input’s kind fits the key. A condition reads an input’s copy in context by its name, such as mode == 'quick'.
Input kinds
Section titled “Input kinds”| Kind | The form shows | A prompt reads |
|---|---|---|
text |
A box for several lines | The text |
short_text |
One line | The text |
number |
A number field, bounded by min and max |
The number |
yes_no |
A switch | true or false |
url |
One line that must be a web address | The address |
choice |
A list of options |
The option chosen |
ticket |
A ticket address | The address, and url, key, number, tracker, title, body, state |
epic |
An epic’s address | As ticket |
project |
A project’s address | The address |
pull_request |
A pull request’s address | The address |
repository |
A repository’s address | The address |
A ticket or epic input is a GitHub issue or pull request, written as its URL or owner/repository#N, or a Linear issue, written as its URL or key such as ENG-12. Orbital looks it up once, when the run is created: GitHub through the GitHub token, Linear through the saved Linear key. A ticket Orbital cannot read, because it does not exist, the token cannot see it or the tracker does not answer, refuses the run before any step, with a message that names the input and gives the tracker’s reason. A restarted run, and each run of a repeating workflow, looks its tickets up again.
The fields hold what the tracker said at that moment. url is the ticket’s address and key its name, such as acme/app#12 or ENG-12. number is the number in that name, and tracker is github or linear. body is the description, empty when there is none, and state is the tracker’s state, such as open on GitHub or In Progress on Linear. A value written on its own, such as {{ inputs.ticket }}, renders the reference the run was given.
A project, pull_request or repository input is checked to be written as an address of a tracker its from accepts, and is not looked up. An imported workflow’s inputs are not looked up either. When an import passes a looked-up input on unchanged, as inputs.ticket = "{{ inputs.ticket }}", the imported steps read its fields too.
prompt = "Implement {{ inputs.ticket.title }} ({{ inputs.ticket.url }}). {{ inputs.ticket.body }}"Related: Pass context between steps, Start a run.
Steps and groups
Section titled “Steps and groups”Every table under steps is a step or a group. A table that has kind, prompt, prompt_file or import is a step. Any other table is a group, and holds at least one step.
[steps.coder]style = "implementer"
[steps.coder.plan]prompt = "Plan the change. Do not change files."outputs.plan = "text"next = "steps.coder.implement"
[steps.coder.implement]prompt = "Carry out this plan: {{ context.plan }}"next = "steps.done"Step ids and references
Section titled “Step ids and references”A step’s id is its path under steps: [steps.coder.implement] has the id coder.implement. The run’s history, its visit counts, manual jumps and context.response.<id> all use the id. Give steps stable ids, because renaming one changes all of these.
Everywhere a key names a step, it writes the full path as quoted text, "steps.coder.implement". TOML has no unquoted references. A budget is named the same way, "budgets.answer". A style is named on its own, "implementer", because it may come from Orbital’s settings rather than the file.
Groups
Section titled “Groups”A group gathers steps that belong together. It takes no turn and is never a route’s target.
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
style |
style name, or a list | No | None | Applies to every step inside the group, at any depth. |
label |
text | No | The group’s name | Shown around the group in the editor. |
Groups nest. A step inside two groups takes the inner group’s style before the outer one’s. An import step acts as a group for the steps it brings in, and a fan-out holds its branches the same way.
Related: Step kinds.
Keys every step takes
Section titled “Keys every step takes”| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
kind |
probe, action, command, decide, gate, wait, fan_out, fan_in or terminal |
No | An agent | Selects the step kind. |
label |
text | No | The step’s id | Display text only. Changes no identity and no routing. On a gate it is what the run waits on; on a terminal, why the run ended. |
max_visits |
positive whole number | No | No limit | Counts every visit to the step, for any reason. |
next |
step reference, or a table | No | None | The fallback route. |
routes |
list of routes | No | None | The conditioned routes. |
Reaching a step’s max_visits fails that visit, and the run is Paused with “Waiting on you”, whatever failure routes the step has. The cap is independent of budgets. A terminal takes neither next nor routes. A fan-out takes join instead of next.
Agent keys
Section titled “Agent keys”An agent step runs one turn of a harness.
[steps.review]prompt_file = "prompts/review.md"style = "reviewer"outputs.verdict = { kind = "choice", options = ["pass", "reject"] }thread = "review"timeout = "30m"routes = [ { if = "verdict == 'pass'", to = "steps.publish" }, { if = "verdict == 'reject'", to = "steps.repair" },]| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
prompt |
Nunjucks template | One of prompt or prompt_file |
None | Use """ for a prompt over several lines. |
prompt_file |
path | One of prompt or prompt_file |
None | Relative to the workflow file that names it. |
outputs |
table of outputs | No | None | Values the agent hands to later steps. |
needs |
table of path and default | No | What the prompt reads | Values the step needs. |
thread |
name | No | None, so each turn starts fresh | Shares a session. |
timeout |
duration | No | 1h |
Waits for leftover work. |
auto_merge |
squash, merge or rebase |
No | None | Merges the pull request. |
style |
style name, or a list | No | None | See Styles. |
harness, model, effort, connection, provider, persona, permissions |
see Style keys | No | From styles | Set on the step, each beats every style. |
claude, codex, opencode, pi, or a plugin harness’s name |
tables | No | None | Harness-specific keys. |
Orbital reads a prompt file again before each later visit, so an edit takes effect on the next turn. If an edit makes it invalid, Orbital reports the problem and uses the prompt it last checked. A prompt written in the file is never read again.
prompt = """Read the code the goal touches and write a short plan.Name each file you would change and why."""Related: Write prompts, Template variables.
Values the step reads from earlier steps, each with what it reads instead when none arrives.
[steps.repair]prompt = "Fix {{ context.findings }}. Summary: {{ context.summary }}"needs.findings = ""needs.summary = "No summary given."Each key is a context path, and its value is the default, or "" for none. A dotted path nests: needs.diff.changed_lines = "". A need without a default must arrive on every path into the step. Validation reports one that arrives on some paths only, or that no earlier step provides. A default stands in when the value arrives empty or not at all, so a step can read an optional output. A step without needs needs the values its prompt reads.
Related: Pass context between steps.
thread
Section titled “thread”A session shared by several agent steps.
[steps.plan]prompt = "Plan an answer to {{ inputs.goal }}."thread = "answer"next = "steps.answer"
[steps.answer]prompt = "Give the planned answer."thread = "answer"Agent steps with the same thread continue one conversation. The steps of a thread must run on the same harness, and steps inside a fan-out cannot join a thread.
Related: History and threads.
timeout
Section titled “timeout”How long a finished turn waits for work its agent left running.
Once the agent stops talking, the step waits this long for background commands and sub-agents it left running. They are then stopped, and the run’s history names them. Only Claude Code steps wait. A duration is a whole number followed by ms, s, m or h.
auto_merge
Section titled “auto_merge”Asks GitHub to merge the run’s pull request once the step’s turn ends.
Once the step’s outputs are accepted, Orbital turns on auto-merge for the run’s pull request, and GitHub merges it when its checks and reviews allow. Orbital asks GitHub itself, with the GitHub token from Settings > GitHub, so the agent needs no gh command and no extra permission. It asks about the pull request the github.pr probe would read in each repository of the run. A pull request that can merge already merges at once, including one whose checks that are not required fail or are still running.
The run’s history records what came of it for each repository: requested, already requested, merged, not allowed by the repository, blocked by a rule, or denied to the token. None of these fails the step. A step that fails asks nothing. auto_merge is refused on a step that can run before the worktree is made or after it is removed. The github.pr.open action takes the same key.
Related: Worktrees and delivery.
Outputs
Section titled “Outputs”The values an agent hands to later steps. A short output names only its kind; a longer one is a table.
outputs.summary = "text"outputs.verdict = { kind = "choice", options = ["pass", "reject"] }
[steps.review.outputs.findings]kind = "list"description = "Each problem that still stands"allow_empty = truerequired_if = "verdict == 'reject'"item.file = { kind = "short_text", description = "Path of the file" }item.severity = { kind = "choice", options = ["blocker", "nit"] }| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
kind |
one of the output kinds | Yes | None | |
description |
text | No | None | What goes in the output. The agent reads it with the output’s kind. |
required |
true or false |
No | true |
The agent may leave an optional output out, and a later step finds it empty. |
required_if |
condition | No | None | Required only when the condition holds. See required_if. |
options |
list, or table of option and description | No | The values its routes test | A choice’s options, in file order. |
item.<name> |
kind, or table with kind, description, options |
With list |
None | A field every item of a list fills. |
allow_empty |
true or false |
No | false |
Lets a list hand over no items. |
The agent ends its turn by calling the handoff tool with the declared outputs. Orbital tells it each output’s kind, description and options, and refuses missing required outputs, unknown names, empty text, values of the wrong kind and choices outside the options. A refused handoff gets up to two correction turns in the same session; after that the step fails. Context changes only once the handoff is accepted. An agent without outputs gets no handoff tool and leaves only its final message, at context.response.<id>.
A route may test only a required output. A choice without options offers the values its routes test, in route order, so it needs at least one route that compares it with a value. An item field is always required and is never a list; an item’s choice declares its options, since no route tests it.
Output kinds
Section titled “Output kinds”| Kind | The agent hands over | Context holds |
|---|---|---|
choice |
One of the options | The option |
text |
Text that is not empty, shown as a paragraph | The text |
short_text |
Text on one line | The text |
number |
A number | The number, so size == 3 compares it |
yes_no |
true or false |
The value, so risky == true compares it |
list |
A list of items, each filling every item field | The list as JSON text |
json |
Any JSON value, unchecked | The value as JSON text |
work |
A work scope | The scope as JSON text, also {{ current_work }} |
ticket |
A ticket reference: a URL, owner/repository#N, #N or a key such as ENG-123. Orbital does not look it up |
The reference |
Context describes the work scope.
required_if
Section titled “required_if”Makes an output required only when another output of the same step has a given value.
[steps.pick.outputs.next]kind = "choice"options = ["ticket", "none"]
[steps.pick.outputs.ticket]kind = "ticket"required_if = "next == 'ticket'"The condition uses the condition language and may read only outputs of the same step, which may not have a required_if of their own. When it holds, the handoff refuses a call without the output. When it does not, the agent may still give the output.
Routing
Section titled “Routing”Routing is written on the step the run leaves. routes lists conditioned routes in the order Orbital tries them, and next is where the run goes when none holds.
[steps.check]kind = "probe"observe = "github.pr"routes = [ { if = "github.pr.state == 'MERGED'", to = "steps.merged" }, { if = "github.pr.state == 'CLOSED'", to = "steps.closed" },]next = "steps.pause"The fallback route.
| Form | Example |
|---|---|
| A step reference | next = "steps.done" |
| A table | next = { to = "steps.done", loop_restart = true } |
| A table that refills a budget | next = { to = "steps.select", resets = "budgets.answer" } |
A succeeded step takes next when no route holds. A failed step never takes it. Written as a table, next takes to, loop_restart and resets, which refills a budget. It takes no spends, because only a route with a condition spends a round. A step that branches needs next unless its routes answer every option of the choice they test.
routes
Section titled “routes”The conditioned routes, tried in order. The first whose condition holds is taken.
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
if |
condition | Yes | None | A route without a condition is next. |
to |
step reference | Yes | None | |
spends |
budget reference | No | None | Takes one round of the budget. |
resets |
budget reference | No | None | Refills the budget. |
loop_restart |
true or false |
No | false |
See loop_restart. |
cause |
output name | No | None | On a route into a failed terminal, names an output of the step the run leaves. Its value becomes the cause the run’s history and the runs list show for the failure. |
A failed step takes the first route whose condition holds, such as outcome == 'failed', and never next. If none holds, the run stops and waits for you. A succeeded step never takes a route written for a failure, because its condition does not hold.
[steps.implement]prompt = "Make the change the goal describes."routes = [{ if = "outcome == 'failed'", to = "steps.recover" }]next = "steps.done"Related: Add branches, Error handling.
Conditions
Section titled “Conditions”A condition compares one context path with one literal.
| Form | Example | True when |
|---|---|---|
| Equals | verdict == 'pass' |
The value equals the text. |
| Not equals | outcome != 'failed' |
The value differs from the text. |
| Number | attempt == 2 |
The value equals the number. |
| True or false | tests.pass == true |
The value equals true or false. |
| Bare path | git.worktree.clean |
The value is truthy. |
| Negated path | !git.worktree.clean |
The value is falsy. |
Missing values, false, zero and empty text are falsy. A path may start with context., so context.github.pr.state and github.pr.state read the same value. You cannot combine conditions or compare two paths: a decision that needs and is a judgement, and belongs in a prompt that hands over a choice. A path reads an input or a value an earlier step produces: an output, a fact, a decide step’s answer, outcome or failure_reason. Reading anything else fails validation with the list of what is available there.
loop_restart
Section titled “loop_restart”Starts the next pass of a loop clean. It goes on a route, on next written as a table, or on an exit.
Taking it 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 budgets.
next = { to = "steps.select", loop_restart = true }Related: Add a loop.
Budgets
Section titled “Budgets”A budget is a named number of repair rounds, shared by the routes that name it.
[budgets.answer]rounds = 5
[steps.review]prompt = "Review the answer. Choose pass or reject."outputs.verdict = { kind = "choice", options = ["pass", "reject"] }routes = [ { if = "verdict == 'pass'", to = "steps.done", resets = "budgets.answer" }, { if = "verdict == 'reject'", to = "steps.repair", spends = "budgets.answer" }, { if = "verdict == 'reject'", to = "steps.exhausted" },]| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
rounds |
positive whole number, or an input template | No | 5 |
How many times its spending routes may be taken before a reset. |
A route with spends takes one round when the run follows it. Once the budget is empty, Orbital skips that route and tries the next one, so a later route with the same condition catches the run. A route with resets fills the budget again, and so does next written as a table with resets. Put it where the run shows progress, such as a passing review or the start of new work.
A spending route does not count as answering a choice option, because it can be skipped. Follow it with an ordinary route for the same option, or give the step next.
A budget differs from a step’s max_visits. It counts the repair rounds of one piece of work rather than every visit, it fills again when the work moves on, and when it runs out the run takes a route you wrote instead of stopping. Budgets are shared by name across the whole workflow, imports included, and survive a restart, a resume and a loop restart. Waiting, probing and queueing spend no rounds. Budget routes cannot leave a fan-out or a step inside one; put them after the join.
Related: Add a loop, Error handling.
Imports
Section titled “Imports”An import step puts another workflow file in its place.
[steps.delivery]import = "imports/pr-delivery.toml"style = "unattended"inputs.work = "{{ inputs.work }}"exits.merged = "steps.complete_ticket"exits.closed = { to = "steps.select", loop_restart = true }| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
import |
path | Yes | None | A complete workflow, relative to the file that names it. |
inputs.<name> |
template | For each required input of the imported file | None | Rendered each time an imported step reads the input. |
exits.<name> |
step reference, or { to, loop_restart } |
No | Ends the run | Where the run goes when the imported workflow ends at that terminal. |
style |
style name, or a list | No | None | Applies to every imported step. |
label |
text | No | The step’s id |
The import step becomes a group. The imported steps take its id as a prefix, so pr.check in the imported file becomes delivery.pr.check, and context.response.delivery.publish reads an imported agent’s reply. Outputs and facts keep their own names, so a condition outside reads a value the imported workflow produced.
Every terminal of the imported file is an exit, named by its id. The imported file decides why it leaves, with the routes into its terminals; the importing file decides where the run goes, with exits. An exit no exits key maps ends the run with that terminal’s outcome. An imported terminal cannot be aborted.
inputs.<name> passes the imported workflow its inputs, and an imported step reads each as {{ inputs.<name> }}. A value may read the importing workflow’s inputs and the run’s context, such as "{{ context.ticket }}". An input the imported file does not declare is refused, and an optional one left out reads as its default, or empty. An import step that passes no inputs at all leaves its steps reading the importing workflow’s inputs. An imported file’s own styles join the importing workflow’s styles, as How styles merge describes.
An import that cannot be found fails loading and names the step. A cycle of imports is refused, naming the files that close it.
Related: Reuse a workflow, Step kinds.
Repeat
Section titled “Repeat”A repeating workflow starts itself again when a run finishes well.
[repeat]wait = "5m"max_runs = 50| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
wait |
duration | No | None | The pause before the next run starts. |
max_runs |
positive whole number | No | 100 |
The most runs one chain may hold. |
When a run ends at a success terminal, Orbital starts a new run of the same workflow with the same inputs. A terminal with final = true ends the chain instead, and so does a run that ends failed or aborted. A repeating workflow must hold at least one final terminal, so the chain can end by itself. Each run appears in the Runs list on its own and shows the chain it belongs to.
[steps.all_shipped]kind = "terminal"final = truelabel = "Every ticket in the project is shipped"Related: Repeat a workflow.
Styles
Section titled “Styles”A style is a named set of turn settings. Steps take styles the way an HTML page takes stylesheets: from the workflow, from the groups they sit in and from their own style key, with the closest one winning on each key.
[styles.careful_review]extends = "reviewer"effort = "max"codex.sandbox_mode = "read-only"Style keys
Section titled “Style keys”| Key | Type | Notes |
|---|---|---|
extends |
style name | Starts from another style. The keys written here win. |
harness |
claude, codex, opencode or pi |
|
model |
model name | The harness interprets it. A model of another harness is refused before the turn. |
effort |
minimal, low, medium, high, xhigh, max or ultra |
Each harness accepts its own subset. |
connection |
connection name | OpenCode only. |
provider |
provider name | OpenCode only. |
persona |
persona name, or "" |
A named prompt from Settings > Personas. "" means none. |
permissions |
auto-accept or full |
auto-accept lets the harness decide routine approvals and asks you about the rest. full approves everything. |
A step can set any of these keys itself, and its own value beats every style. When nothing in the workflow sets a key, the step takes it from the run’s preset, and then from the defaults in Settings > Harnesses. A missing persona does not stop the step: it runs without one, and its history names the missing persona.
Harness-specific keys
Section titled “Harness-specific keys”A table named after a harness holds keys that apply only when that harness runs the step. Each beats the plain key. Besides the four built-in harnesses, a plugin harness reads a table of its own name, such as fixture.window_size = "42" for a harness named fixture, in a style or on a step.
| Key | Values |
|---|---|
claude.model |
A Claude Code model |
claude.effort |
An effort Claude Code supports |
claude.setting_sources |
A list of user, project and local. [] turns off settings from files. |
codex.model |
A Codex model |
codex.effort |
An effort Codex supports |
codex.sandbox_mode |
read-only, workspace-write or danger-full-access |
codex.approval_policy |
never, on-request or untrusted |
opencode.model |
An OpenCode model |
opencode.connection |
A configured connection |
opencode.provider |
A configured provider |
pi.model |
A Pi model |
pi.effort |
minimal, low, medium, high or max |
[styles.portable_review]effort = "high"claude.model = "claude-opus-5"codex.model = "gpt-6"Loading checks only that each value is text or a list of text. The harness checks its own keys before the turn that uses them: a key it does not define, such as a misspelt one, or a value that does not suit it, such as an effort it does not support, refuses the turn and names the key.
Style files
Section titled “Style files”A style file holds version = 3 and [styles.*] tables, and nothing else. A workflow pulls it in with use.
version = 3
[styles.thorough]effort = "high"
[styles.codex]harness = "codex"model = "gpt-6"How styles merge
Section titled “How styles merge”Orbital gathers every style a workflow can see into one set, in this order:
- the style files in
use, in list order; - the styles of the workflows the file imports;
- the file’s own
[styles]; - the presets in Settings > Presets.
When a name appears more than once, the definitions merge key by key and the later one wins on each key it sets. A preset in Settings > Presets therefore wins over a file’s style of the same name on every key the setting sets. To change a shipped preset for one workflow, extend it under a new name.
How a step’s settings resolve
Section titled “How a step’s settings resolve”Each setting resolves on its own, from the first source that sets it:
- the step’s own keys;
- the step’s
style, a later name in a list before an earlier one; - the styles of the groups that hold the step, the innermost first, including the import step that brought it in;
- the model picked when the run started, if one was;
- the workflow’s
style; - the run’s preset: the one picked when the run started, or else the preset in use in Settings > Presets;
- the defaults in Settings > Harnesses.
Within a style, a harness-specific key comes before the plain key. Naming a harness or a model when a run starts, or switching a run’s harness later, removes harness, model, effort, connection and provider from every layer; harness-specific keys, persona and permissions stay. Steps of one thread must resolve to the same harness. A fan-in without its own style takes the workflow’s.
The editor’s Inspector and the read_workflow MCP tool show, for every step that takes a turn, each resolved setting and where it came from.
Shipped presets
Section titled “Shipped presets”Orbital ships these presets and personas in Settings > Presets and Settings > Personas. Edit them there, reset an edited one to Orbital’s version, or delete one; a deleted shipped entry can be restored. Only claude and codex name a harness, so the persona presets run on whichever harness the rest of the cascade chooses.
| Style | Settings |
|---|---|
balanced |
effort = "medium" |
deep_thinking |
effort = "high" |
planner |
persona = "planner", effort = "high" |
implementer |
persona = "implementer", effort = "medium" |
reviewer |
persona = "strict_reviewer", effort = "high" |
researcher |
persona = "researcher", effort = "medium" |
claude |
harness = "claude". In use on a fresh install. |
codex |
harness = "codex" |
| Persona | Prompt |
|---|---|
planner |
You plan work before anyone changes code. Read what exists, name what must change and why, and say how each change will be checked. Prefer the smallest plan that meets the goal, and call out what you could not find out. |
implementer |
You make the change the plan describes. Follow the repository’s conventions, keep the change as small as the goal allows, and verify it with the checks the repository uses before you say it is done. |
strict_reviewer |
You review work you did not write. Look for defects, missed requirements and risks, and report each with the evidence that shows it. Do not change the work yourself, and do not approve what you have not checked. |
researcher |
You find things out. Search the code and the web, compare sources, and report what you found with where you found it. Separate what the evidence shows from what you infer. |
supervisor |
The prompt the Supervisor speaks with. |
A run reads Settings > Presets when it loads its workflow, on start and on every resume.
Related: Styles and personas, Give steps a style, Use different models.
Reserved names
Section titled “Reserved names”Orbital writes some names into context itself, so an input, an output, a fact or a step id cannot take them: current_node, failure_reason, last_response, last_stage, outcome, internal, response, parallel, work_scope and repair_history. Nor can a name take the namespace of a probe, github, git or tracker, unless it sits under it as a fact a script declares. Nor can a name be a root probe facts had before format version 3, such as pr or diff, and a fact or output cannot take a probe fact’s old path, such as pr.state for github.pr.state. goal is built in, and <id>.failed belongs to each command. A command’s fact may not repeat a fact a probe in the same workflow writes, or an output available at that step, and one step may not declare both a and a.b.
What validation reports
Section titled “What validation reports”orbital validate, the editor’s Problems bar and the validate_workflow MCP tool check the same things. Errors stop the workflow loading; warnings leave it runnable.
| Errors | Warnings |
|---|---|
| An unknown key, or a value of the wrong type | A step the entry cannot reach |
| A step that is not a terminal with no way out | A declared input no prompt reads |
A branching step with no next whose routes do not answer every option |
A style the workflow defines and nothing uses |
| A route to a step that does not exist, or to a group | A worktree.remove without a route testing git.worktree.clean |
| A condition that does not parse, or tests an option the choice does not declare | A failure route back into a loop that no max_visits or budget bounds |
| A prompt, condition or need reading a value that does not arrive on every path into the step | A route that tests outcome after a wait or a terminal, which cannot fail |
| A route testing an optional output | A step that branches on a name without declaring outputs |
| Fan-out, gate and thread rules broken, as Step kinds gives them | A step after an agent on another harness that reads none of its replies |
| An import that cannot be read, or a cycle of imports |
Each message names the step and the key, and an unknown name lists the ones that would fit.
Complete example
Section titled “Complete example”This workflow puts many of the keys above in context: inputs with defaults, styles with extends, a group with a style, a shared thread, a command with facts, failure routing, a wait and a loop restart. The workflow guides show imports, outputs, parallel work and budgets in complete workflows.
- Download the files. The archive holds
syntax.tomland the gate below,gate.toml. - Unpack it into your workflow library,
~/.orbital/workflows/. Keep every relative path. - Check it with
orbital validate ~/.orbital/workflows/syntax.toml.
Choose New task, pick any project, choose the syntax workflow and write a goal. Claude Code must be installed and signed in, because the review style names the claude harness. The script prints a true fact, so the run reaches done and ends Succeeded. A failed script routes to failed, and the run ends Failed. A false fact takes the one-second wait, then a loop restart into done.
version = 3entry = "steps.answer.plan"description = "Key reference wrapper"max_visits = 10style = "review"
[inputs.goal]default = "Explain this example"
[inputs.notes]kind = "text"label = "Notes"hint = "Anything the plan should take into account"default = "Keep it short"
[styles.review]harness = "claude"effort = "high"permissions = "auto-accept"
[styles.short]extends = "review"effort = "low"
[steps.answer]label = "Answer the goal"style = "short"
[steps.answer.plan]label = "Plan the answer"prompt = "Plan an answer to {{ inputs.goal }}. Notes: {{ inputs.notes }}"thread = "answer"max_visits = 2next = "steps.answer.give"
[steps.answer.give]prompt = "Give the planned answer."thread = "answer"next = "steps.measure"
[steps.measure]kind = "command"repeat_safety = "idempotent"timeout = "30s"facts.check.ready = "bool"script = '''printf '%s\n' '{"check.ready":true}' '''routes = [ { if = "outcome == 'failed'", to = "steps.failed" }, { if = "check.ready == true", to = "steps.done" },]next = "steps.wait"
[steps.wait]kind = "wait"duration = "1s"next = { to = "steps.done", loop_restart = true }
[steps.done]kind = "terminal"
[steps.failed]kind = "terminal"outcome = "failed"A gate
Section titled “A gate”This workflow stops at a gate. Check it with orbital validate ~/.orbital/workflows/gate.toml. A run of it shows “Waiting:” followed by the label, and takes no run slot. Resume it, and it follows the gate’s next to done.
version = 3entry = "steps.approval"
[steps.approval]kind = "gate"label = "Approve the release"next = "steps.done"
[steps.done]kind = "terminal"Related
Section titled “Related”- Step kinds: what each kind of step does and the keys it takes.
- Context: the values inputs, outputs and facts put into context.
- Template variables: the names a prompt can read.
- Error handling: how failure routes and budgets behave.
- The orbital command: convert a workflow written before version 3.