> For the index of every page in Orbital's docs, read https://docs.beta.runorbital.dev/llms.txt.

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

```toml title="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](https://toml.io) 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

### `version`

The version of the workflow format.

| Key | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `version` | number | Yes | None | Always `3`. |

```toml
version = 3
```

### `entry`

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

```toml
entry = "steps.plan"
```

Related: [Step ids and references](#step-ids-and-references).

### `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`

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`](#keys-every-step-takes) too.

Related: [Add a loop](/workflows/add-a-loop/), [Error handling](/reference/error-handling/).

### `style`

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

```toml
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](#styles).

### `use`

Style files the workflow pulls in.

| Key | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `use` | list of paths | No | None | Relative to the workflow file. |

```toml
use = ["shared/team.toml"]
```

Related: [Style files](#style-files).

## Inputs

The values a run takes besides its goal. Each input is a table under `inputs`.

```toml
[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](#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](#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

| 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](#repeat), 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.

```toml
prompt = "Implement {{ inputs.ticket.title }} ({{ inputs.ticket.url }}). {{ inputs.ticket.body }}"
```

Related: [Pass context between steps](/workflows/pass-context/), [Start a run](/runs/start-a-run/).

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

```toml
[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

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

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](#imports) acts as a group for the steps it brings in, and a [fan-out](/reference/step-kinds/#fan-out) holds its branches the same way.

Related: [Step kinds](/reference/step-kinds/#group).

## 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](/reference/step-kinds/). |
| `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](#next). |
| `routes` | list of routes | No | None | The [conditioned routes](#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](#budgets). A terminal takes neither `next` nor `routes`. A fan-out takes `join` instead of `next`.

## Agent keys

An agent step runs one turn of a harness.

```toml
[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](#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](#needs). |
| `thread` | name | No | None, so each turn starts fresh | [Shares a session](#thread). |
| `timeout` | duration | No | `1h` | [Waits for leftover work](#timeout). |
| `auto_merge` | `squash`, `merge` or `rebase` | No | None | [Merges the pull request](#auto_merge). |
| `style` | style name, or a list | No | None | See [Styles](#styles). |
| `harness`, `model`, `effort`, `connection`, `provider`, `persona`, `permissions` | see [Style keys](#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](#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.

```toml
prompt = """
Read the code the goal touches and write a short plan.
Name each file you would change and why.
"""
```

Related: [Write prompts](/workflows/write-prompts/), [Template variables](/reference/template-variables/).

### `needs`

Values the step reads from earlier steps, each with what it reads instead when none arrives.

```toml
[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](/workflows/pass-context/).

### `thread`

A session shared by several agent steps.

```toml
[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](/reference/history-and-threads/).

### `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`

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`](/reference/step-kinds/#action) action takes the same key.

Related: [Worktrees and delivery](/projects/worktrees-and-delivery/).

## Outputs

The values an agent hands to later steps. A short output names only its kind; a longer one is a table.

```toml
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](#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`](#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

| 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](/reference/context/#agent-outputs) describes the work scope.

### `required_if`

Makes an output required only when another output of the same step has a given value.

```toml
[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](#conditions) 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

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.

```toml
[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"
```

### `next`

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](#budgets). 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`

The conditioned routes, tried in order. The first whose condition holds is taken.

| Key | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `if` | [condition](#conditions) | 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](#budgets). |
| `resets` | budget reference | No | None | Refills the budget. |
| `loop_restart` | `true` or `false` | No | `false` | See [`loop_restart`](#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.

```toml
[steps.implement]
prompt = "Make the change the goal describes."
routes = [{ if = "outcome == 'failed'", to = "steps.recover" }]
next = "steps.done"
```

Related: [Add branches](/workflows/add-branches/), [Error handling](/reference/error-handling/).

### 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`

Starts the next pass of a loop clean. It goes on a route, on `next` written as a table, or on an [exit](#imports).

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.

```toml
next = { to = "steps.select", loop_restart = true }
```

Related: [Add a loop](/workflows/add-a-loop/).

## Budgets

A budget is a named number of repair rounds, shared by the routes that name it.

```toml
[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](/workflows/add-a-loop/), [Error handling](/reference/error-handling/).

## Imports

An import step puts another workflow file in its place.

```toml
[steps.delivery]
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](#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](/workflows/reuse-a-workflow/), [Step kinds](/reference/step-kinds/#import).

## Repeat

A repeating workflow starts itself again when a run finishes well.

```toml
[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.

```toml
[steps.all_shipped]
kind = "terminal"
final = true
label = "Every ticket in the project is shipped"
```

Related: [Repeat a workflow](/workflows/repeat-a-workflow/).

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

```toml
[styles.careful_review]
extends = "reviewer"
effort = "max"
codex.sandbox_mode = "read-only"
```

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

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

```toml
[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

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

```toml title="shared/team.toml"
version = 3

[styles.thorough]
effort = "high"

[styles.codex]
harness = "codex"
model = "gpt-6"
```

### How styles merge

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.

### How a step's settings resolve

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.

### 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](/supervisor/) speaks with. |

A run reads **Settings > Presets** when it loads its workflow, on start and on every resume.

Related: [Styles and personas](/styles/), [Give steps a style](/styles/configure-steps/), [Use different models](/harnesses/use-different-models/).

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

`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](/reference/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

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](/workflows/write-a-workflow/) show imports, outputs, parallel work and budgets in complete workflows.

1. [Download the files](/syntax.zip). 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`.

```toml title="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](/examples/syntax/syntax.toml)

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

```toml title="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](/examples/syntax/gate.toml)

## Related

- [Step kinds](/reference/step-kinds/): what each kind of step does and the keys it takes.
- [Context](/reference/context/): the values inputs, outputs and facts put into context.
- [Template variables](/reference/template-variables/): the names a prompt can read.
- [Error handling](/reference/error-handling/): how failure routes and budgets behave.
- [The orbital command](/reference/command/#orbital-migrate): convert a workflow written before version 3.
