Skip to content
Orbital

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.

review.toml
version = 3
entry = "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.

The version of the workflow format.

Key Type Required Default Notes
version number Yes None Always 3.
version = 3

The 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.

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.

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.

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 = 1
max = 10
default = 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'.

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.

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"

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.

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.

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.

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.

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.

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.

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.

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 = true
required_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.

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.

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 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.

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.

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.

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.

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.

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.

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 = true
label = "Every ticket in the project is shipped"

Related: Repeat a workflow.

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"
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.

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.

A style file holds version = 3 and [styles.*] tables, and nothing else. A workflow pulls it in with use.

shared/team.toml
version = 3
[styles.thorough]
effort = "high"
[styles.codex]
harness = "codex"
model = "gpt-6"

Orbital gathers every style a workflow can see into one set, in this order:

  1. the style files in use, in list order;
  2. the styles of the workflows the file imports;
  3. the file’s own [styles];
  4. 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.

Each setting resolves on its own, from the first source that sets it:

  1. the step’s own keys;
  2. the step’s style, a later name in a list before an earlier one;
  3. the styles of the groups that hold the step, the innermost first, including the import step that brought it in;
  4. the model picked when the run started, if one was;
  5. the workflow’s style;
  6. the run’s preset: the one picked when the run started, or else the preset in use in Settings > Presets;
  7. 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.

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.

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.

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.

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.

  1. Download the files. The archive holds syntax.toml and the gate below, gate.toml.
  2. Unpack it into your workflow library, ~/.orbital/workflows/. Keep every relative path.
  3. 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.

syntax.toml
version = 3
entry = "steps.answer.plan"
description = "Key reference wrapper"
max_visits = 10
style = "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 = 2
next = "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"

Download syntax.toml

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.

gate.toml
version = 3
entry = "steps.approval"
[steps.approval]
kind = "gate"
label = "Approve the release"
next = "steps.done"
[steps.done]
kind = "terminal"

Download gate.toml