Step kinds
The kinds of step a workflow can hold, what each does and the keys it takes
Pick the kind of step you need, and set its kind. A step is one table under steps in a workflow, and its kind decides what it does. A step with no kind is an agent.
[steps.review]prompt = "Review the change."outputs.verdict = { kind = "choice", options = ["pass", "reject"] }
[steps.pause]kind = "wait"duration = "10m"
[steps.done]kind = "terminal"| Kind | kind |
What it does |
|---|---|---|
| Agent | none | Sends a prompt to a harness and waits for the turn to finish. |
| Probe | probe |
Reads built-in observations, such as the pull request’s state. |
| Action | action |
Does a built-in change, such as pushing a branch or opening a pull request. |
| Command | command |
Runs a script of your own and records the facts it prints. |
| Decide | decide |
Asks a decision model a question and follows its answer. |
| Wait | wait |
Pauses for a set duration. |
| Gate | gate |
Stops the run until a person lets it continue. |
| Fan-out | fan_out |
Runs its child steps as parallel branches. |
| Fan-in | fan_in |
Joins the parallel branches. |
| Terminal | terminal |
Ends the run with an outcome. |
Two more tables under steps are not kinds of their own: an import step puts another workflow in its place, and a group gathers steps under one name. Every step takes kind, label, max_visits, next and routes, as Keys every step takes describes. Unknown keys are errors.
The editor labels each step on the canvas with its kind.

Steps that do work
Section titled “Steps that do work”Sends a prompt to a harness and waits for the turn to finish.
[steps.<id>]prompt = "<template>"outputs.<name> = "<kind>"next = "steps.<id>"| Key | Type | Required | Default | Description |
|---|---|---|---|---|
prompt |
template | One of prompt or prompt_file |
None | The prompt, in the file. |
prompt_file |
path | One of prompt or prompt_file |
None | A prompt file, relative to the workflow file. |
outputs |
table | No | None | Values the agent hands to later steps. |
needs |
table | No | What the prompt reads | Values the step needs, with defaults. |
thread |
name | No | None | A session shared with other agent steps. |
timeout |
duration | No | 1h |
How long the turn waits for work its agent left running. |
auto_merge |
squash, merge or rebase |
No | None | Merges the run’s pull request after the turn. |
style |
name or list | No | Inherited | The step’s styles. |
harness, model, effort, connection, provider, persona, permissions and the harness-specific tables |
see the reference | No | From styles | Settings for this step alone. |
The agent renders its prompt, calls the harness and waits for a turn to finish. It is the step that changes files or talks to a service on your behalf through the agent’s own tools. Declare outputs when a later route or prompt needs a value from it, because a bare turn leaves nothing behind but prose. Context gives each output kind’s contract.
Orbital reads a prompt file again before each later visit. If an edit makes it invalid, Orbital reports the problem and falls back to the prompt it last checked.
version = 3entry = "steps.plan"
[steps.plan]label = "Plan the change"prompt = "Read the code the goal touches and write a short plan. Do not change files."outputs.plan = "text"next = "steps.implement"
[steps.implement]label = "Make the change"prompt = "Carry out this plan: {{ context.plan }}"next = "steps.done"
[steps.done]kind = "terminal"plan reads the code and writes its plan as the plan output; implement gets that plan in its prompt.
Related: Write your first workflow, Write prompts.
Reads built-in observations and records them as facts.
[steps.<id>]kind = "probe"observe = ["<probe>", "<probe>"]| Key | Type | Required | Default | Description |
|---|---|---|---|---|
observe |
probe name, or a list | Yes | None | The observations to read. |
A probe only reads, so running it again is always safe and it takes no repeat_safety. It spends no agent turn. The facts it records start with its name, so github.pr records github.pr.state and git.diff records <folder>.git.diff.changed_lines. Each probe answers once for every repository folder of the run, except git.change and tracker.tickets, which answer once for the whole run.
| Probe | What it observes |
|---|---|
github.pr |
The current branch’s pull request: its checks, review state and merge state, rolled up into github.pr.state. |
github.threads |
The review threads on that pull request. <folder>.github.threads.open holds the unresolved ones as readable text. |
git.diff |
Changes in the working branch. |
git.change |
The branch’s diff against its base branch at git.change.diff, and what the checkout holds beyond the copy already pushed at git.change.unpublished, each with uncommitted and untracked files and capped at 4,000 lines. A run with several repositories heads each with its folder name. |
git.worktree |
Whether each checkout could be removed without losing work. |
tracker.tickets |
The title, state and text of every ticket the goal and work scope name, at tracker.tickets.text, with tracker.tickets.total and tracker.tickets.unreachable. It reads GitHub links, owner/repository#N and bare #N through GitHub, and Linear links and identifiers through Linear. A ticket it cannot read is listed with the reason. |
github.pr.state is one of MERGED, CLOSED, CONFLICTS, CI_FAILED, CHANGES_REQUIRED and WAITING. Plain folders supply no repository observations. A failed probe attempt retries inside the same step, up to six attempts with a wait of 5 seconds growing to a minute between them, and records its progress; it spends no visit and takes no failure route per attempt. Context lists every fact each probe writes.
version = 3entry = "steps.check"
[steps.check]kind = "probe"observe = ["github.pr", "github.threads"]routes = [ { if = "github.pr.state == 'MERGED'", to = "steps.merged" }, { if = "github.pr.state == 'CHANGES_REQUIRED'", to = "steps.address" },]next = "steps.done"
[steps.address]prompt = """Answer the open review threads, one repository at a time:{% for name, folder in run.folders %}### {{ name }}{{ context[name].github.threads.open }}{% endfor %}"""next = "steps.done"
[steps.merged]kind = "terminal"
[steps.done]kind = "terminal"check reads the pull request and its threads. A merged pull request ends the run; one with changes requested goes to address, which quotes the open threads of each repository.
Related: Add branches, Context.
Action
Section titled “Action”Does a built-in change, without an agent turn.
[steps.<id>]kind = "action"do = "<action>"| Key | Type | Required | Default | Description |
|---|---|---|---|---|
do |
action name | Yes | None | The change to make. |
| The action’s own keys | see below | Depends on the action |
| Action | What it does | Keys | Records |
|---|---|---|---|
worktree.create |
Makes one isolated checkout per repository folder on one generated branch, orbital/<id>, and moves the run into it. |
None | |
worktree.remove |
Puts the checkouts away when nothing would be lost. | None | git.worktree.clean, git.worktree.status |
git.commit |
Commits every change in each repository’s checkout. | message |
|
git.push |
Pushes each repository’s branch. | None | |
github.pr.open |
Opens a pull request for each repository, or reuses the open one for the branch. | title, body, draft, base, auto_merge |
github.pr.url, github.pr.number, github.pr.state |
An action knows whether repeating it is safe, so it takes no repeat_safety. Committing with nothing to commit and pushing an unchanged branch do nothing. git.commit and git.push skip the repository’s Git hooks, and a push is never forced. github.pr.open writes the same paths the github.pr probe does, so a later condition reads one path whichever step wrote it. git.commit needs a message. On github.pr.open, title is required and body defaults to empty; draft is true or false and defaults to false; base defaults to the branch the run’s checkout was made from, which is the repository’s default branch unless the run chose another; a repository with no work of the run’s is skipped; auto_merge works as it does on an agent.
worktree.create leaves plain folders where they are, and the run’s folder paths follow the checkouts. If creation fails partway, the checkouts already made are kept for the retry. worktree.remove measures every checkout first and removes none if any would lose uncommitted files or unpushed work. Commits GitHub has already seen on a merged or closed pull request are not unpushed, even when the remote branch was deleted after the merge, and nor is a local commit the pull request head or the base holds rebased. git.worktree.status names any commits that remain. Removal deletes no branch. A run that publishes usually starts with worktree.create and ends with worktree.remove; a workflow without them works in your own folders.
A text key, message, title or body, takes text or asks a model to write it:
title = { write = "A short title naming the change", style = "quick" }A write is a short model session with no tools. It reads the run’s context, follows the instruction and returns only the text. Its settings come from its own style, then the styles of the groups holding the action, then the workflow’s style. It spends no agent turn, and the run’s history shows what it wrote. Every built-in harness can write a text. A plugin harness that cannot fails the step with a message naming it.
version = 3entry = "steps.checkout"
[styles.quick]effort = "low"
[steps.checkout]kind = "action"do = "worktree.create"next = "steps.implement"
[steps.implement]prompt = "Make the change the goal describes. Do not commit."outputs.change_summary = "text"next = "steps.publish.commit"
[steps.publish.commit]kind = "action"do = "git.commit"message = { write = "A conventional commit message for the change", style = "quick" }next = "steps.publish.push"
[steps.publish.push]kind = "action"do = "git.push"next = "steps.publish.open"
[steps.publish.open]kind = "action"do = "github.pr.open"draft = truetitle = { write = "A short title naming the change", style = "quick" }body = { write = "Why the change exists, then what it changes", style = "quick" }next = "steps.done"
[steps.done]kind = "terminal"checkout makes the isolated checkout. After implement changes the files, the three publish steps commit, push and open a draft pull request, with a model writing the commit message, title and body.
Related: Push and open pull requests, Worktrees and delivery.
Command
Section titled “Command”Runs a script of your own and records the facts it prints.
[steps.<id>]kind = "command"script = "<shell command>"facts.<name> = "<kind>"| Key | Type | Required | Default | Description |
|---|---|---|---|---|
script |
shell command | Yes | None | Runs through bash -c. Not a prompt template. |
facts.<name> |
bool, number or text |
Yes, at least one | None | The facts the script prints. A fact’s name has at least two parts, such as facts.tests.pass, which declares tests.pass. |
timeout |
duration | No | 10m |
A script that runs out of time fails the step. |
repeat_safety |
idempotent, reconcilable or uncertain |
No | uncertain |
What running the script again would do. |
The script runs once, in the primary working folder, and inherits the server’s environment. Earlier lines of output are free-form, but the last line must be one JSON object holding exactly the declared facts with matching types. A timeout, a non-zero exit or a malformed last line fails the step and sets <id>.failed to true; a successful run sets it to false and saves the facts. A script cannot overwrite a probe’s facts, an output or a reserved failure path. A command spends no agent turn.
Use idempotent when running the script again adds no further effect, reconcilable when it checks what an earlier attempt did before acting again, and uncertain when neither holds. After a restart, Orbital runs an idempotent or reconcilable script again, but stops before an uncertain one that had started and waits for you, so a script that changes the world never runs twice by surprise.
Write a longer script between ''', which keeps quotes and backslashes as they are.
version = 3entry = "steps.test"
[steps.test]kind = "command"repeat_safety = "idempotent"timeout = "10m"facts.tests.pass = "bool"script = '''if npm test >/dev/null 2>&1; then echo '{"tests.pass":true}'else echo '{"tests.pass":false}'fi'''routes = [ { if = "tests.pass", to = "steps.passed" }, { if = "outcome == 'failed'", to = "steps.failing" },]next = "steps.failing"
[steps.passed]kind = "terminal"
[steps.failing]kind = "terminal"outcome = "failed"The script prints tests.pass as JSON, and the routes read it. A script that fails or times out takes the outcome == 'failed' route.
Related: Add branches, Durability after restarts.
Decide
Section titled “Decide”Asks a decision model structured questions about the run and follows the route its answers select.
[steps.<id>]kind = "decide"state = "<template>"routes = [{ if = "<question> == '<option>'", to = "steps.<id>" }]default = "steps.<id>"
[steps.<id>.questions.<question>]kind = "choice"options.<option> = "<when it applies>"| Key | Type | Required | Default | Description |
|---|---|---|---|---|
state or state_file |
template or path | One of them | None | What the model is asked about. It reads {{ context.x }} like an agent prompt, and {{ diff }} for the run’s change against its base, cut to the model’s context with a [… diff truncated …] marker. A file is read again before each visit. |
questions.<name> |
table | At least one | None | A question, with kind and an optional description. |
default |
step reference | Yes | None | Where the run goes when no answer selects a route. |
decider |
name | No | The default decision model | The name of a decision model in Settings. A name no model answers to takes the default path. |
threshold |
number from 0 to 1 | No | The model’s threshold, which starts at 0.5 |
The confidence every answer needs before the run follows it. |
routes |
routes | No | None | The routes the answers select. |
A question’s kind is choice, with options as for a choice output; score, with levels written the same way, each valued with when it applies; or noul, which answers yes or no. The answers land in context as <name> and <name>_confidence, so routes test them like outputs: risk == 'high', or a bare ship for a noul. A noul question is tested only as true or false.
A decide step takes default instead of next. It tries its routes in order and follows the first that holds. The run follows default when no route holds, when any answer’s confidence is below threshold, when no decision model is configured, and when the model cannot be reached or answers with something unusable; no agent answers in its place. Each decision, with why it took its route, is recorded as a DecisionMade event and shown as a card in the run’s Session tab. A restart replays the recorded decision and does not ask again. Validation refuses a decide step without a question or without default, and a route testing a question or option it does not declare.
version = 3entry = "steps.implement"
[steps.implement]prompt = "Make the change the goal describes and summarise it."outputs.change_summary = "text"next = "steps.ready"
[steps.ready]kind = "decide"state = "The change: {{ context.change_summary }}"threshold = 0.7routes = [{ if = "ship", to = "steps.done" }]default = "steps.revise"
[steps.ready.questions.ship]kind = "noul"description = "Is the change ready to ship as it stands?"
[steps.revise]prompt = "Revise the change. Summary so far: {{ context.change_summary }}"next = "steps.done"
[steps.done]kind = "terminal"ready asks whether the change can ship. A confident yes ends the run; anything else, including no decision model, takes default to revise.
Related: Decide with a model, Add branches.
Steps that wait
Section titled “Steps that wait”Pauses the run for a set time.
[steps.<id>]kind = "wait"duration = "<duration>"| Key | Type | Required | Default | Description |
|---|---|---|---|---|
duration |
duration, such as 15m, 30s or 1h |
Yes | None | A whole number followed by ms, s, m or h. |
A loop that polls an outside system needs a wait; without one, a loop that watches a pull request spends agent turns. Waiting spends no agent turn, survives a server restart and keeps the interface responsive. You can skip the remaining time, and the run’s history records that you did.
version = 3entry = "steps.check"
[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"
[steps.pause]kind = "wait"duration = "10m"next = "steps.check"
[steps.merged]kind = "terminal"
[steps.closed]kind = "terminal"outcome = "failed"The probe reads the pull request. Until it has merged or closed, the run waits ten minutes and looks again.
Related: Add gates and waits, Add a loop.
Stops the run until a person lets it continue.
[steps.<id>]kind = "gate"label = "<what the run waits on>"next = "steps.<id>"| Key | Type | Required | Default | Description |
|---|---|---|---|---|
label |
text | No | The step’s id | Shown as what the run waits on. |
resume |
pull_request or operator |
No | pull_request |
Whether the run may open the gate by itself when the world changes. |
next |
step reference | Yes | None | Where the run goes when the gate opens. |
The gate stops the run without recording a failure. The run shows “Waiting:” followed by the gate’s label, and it gives its run slot to other runs. Resume or Skip wait lets it through.
With resume = "pull_request", the run also watches the world once a minute and carries on by itself when it changes. When the run has read its tickets with the tracker.tickets probe and could read every one, it waits for the tickets’ text to change. Otherwise, when the context holds a github.pr.state, it waits for the pull request to leave WAITING. A gate with neither in play waits for you, and so does every gate with resume = "operator". Either way, the run joins the back of the queue and follows next. Waiting spends no agent turn and survives a server restart. A gate without next, or one inside a fan-out, fails validation.
version = 3entry = "steps.plan"
[steps.plan]prompt = "Write a plan for the goal. Do not change files."outputs.plan = "text"next = "steps.approve"
[steps.approve]kind = "gate"label = "Read the plan, then continue"next = "steps.implement"
[steps.implement]prompt = "Carry out this plan: {{ context.plan }}"next = "steps.done"
[steps.done]kind = "terminal"The run stops after the plan. When you let it continue, it goes on to implement.
Related: Add gates and waits.
Parallel steps
Section titled “Parallel steps”Fan-out
Section titled “Fan-out”Runs its child steps as parallel branches.
[steps.<id>]kind = "fan_out"join = "steps.<fan-in>"
[steps.<id>.<branch>]prompt = "<template>"next = "steps.<fan-in>"| Key | Type | Required | Default | Description |
|---|---|---|---|---|
join |
step reference | Yes | None | The fan-in where the branches meet. |
branches |
list of step references | No | None | Steps elsewhere in the file that each start a branch. |
max_parallel |
positive whole number | No | 4 |
How many branches run at once. |
style |
name or list | No | None | Applies to every branch step inside it. |
routes |
routes | No | None | Where a partial failure goes. |
| Child steps | steps | No | None | Each one starts a branch, in file order. |
state or state_file |
template or path | To decide | None | What a decision model is asked about, as on a decide step. Declaring it, or a question, makes the fan-out decide which branches start. |
questions.<name> |
table | To decide | None | One choice question, or several noul questions, as on a decide step. |
choices.<answer> |
list of step references | To decide, for every branch | None | The branches an answer starts: an option of the choice question, or the name of a noul question answered yes. |
default |
list of step references | To decide | None | The branches that start when no decision is made. |
decider |
name | No | The default decision model | The decision model to ask. |
threshold |
number from 0 to 1 | No | The model’s threshold | The confidence every answer needs. |
Each child step starts a branch, and so does each step branches lists, up to max_parallel at a time. Write the branches nested, listed or both; a fan-out needs at least two. A branch runs from its child step until it reaches join. Branches cannot loop, hold a gate, join a thread, share a step with another branch or leave for a step outside the branch. A branch may hold a fan-out of its own. Neither a branch step nor the fan-in can be the workflow’s entry. They share the same files, so keep them reading unless they write to separate paths.
When every branch succeeds, the run enters the fan-in. When some fail, the fan-out takes the first of its routes that holds, such as outcome == 'failed'. With none, the run is Paused and names the failed branches. Each branch works on its own copy of the context; History and threads gives the rules.
version = 3entry = "steps.reviews"
[steps.reviews]kind = "fan_out"max_parallel = 2join = "steps.collect"
[steps.reviews.clarity]prompt = "Review the goal for clarity."outputs.assessment = "text"next = "steps.collect"
[steps.reviews.risks]prompt = "Review the goal for risks."outputs.assessment = "text"next = "steps.collect"
[steps.collect]kind = "fan_in"prompt = "Summarise these reviews: {{ context.parallel.results['0'].context.assessment }} and {{ context.parallel.results['1'].context.assessment }}"next = "steps.done"
[steps.done]kind = "terminal"reviews starts reviews.clarity and reviews.risks side by side. collect waits for both, then one turn summarises their answers.

A fan-out can decide which branches start. It declares state, and either one choice question or several noul questions, and optionally decider and threshold, as a decide step does. choices names the branches each answer starts, and default the branches that start when no decision is made.
[steps.<id>]kind = "fan_out"join = "steps.<fan-in>"state = "<template>"default = ["steps.<id>.<branch>"]choices.<option> = ["steps.<id>.<branch>"]
[steps.<id>.questions.<name>]kind = "choice"options.<option> = "<when it applies>"The model’s option, or each noul question it answers yes, starts the branches listed under it. The fan-out starts its default branches instead when no decision model is configured, the model cannot answer, an answer is below the threshold, or the picks start no branch. The fan-in joins only the branches that started. The pick is recorded, so a restart starts the same branches and does not ask again. Validation refuses a deciding fan-out without default, a branch no answer starts, and an answer its questions do not offer.
Related: Run steps in parallel, Decide with a model.
Fan-in
Section titled “Fan-in”Joins the branches of a fan-out.
[steps.<id>]kind = "fan_in"prompt = "<template>"next = "steps.<id>"| Key | Type | Required | Default | Description |
|---|---|---|---|---|
prompt or prompt_file |
template or path | No | None | Adds one turn that combines the branch results. |
style |
name or list | No | The workflow’s | Settings for that turn. |
persona |
persona name, or "" |
No | From styles | The persona for that turn. |
With a prompt, the fan-in runs one turn that combines the branch results, which it reads through parallel.results. Without one, it is only the point where the branches meet. Each fan-in belongs to one fan-out.
Related: Run steps in parallel, Write prompts.
Ending a run
Section titled “Ending a run”Terminal
Section titled “Terminal”Ends the run with an outcome.
[steps.<id>]kind = "terminal"outcome = "<outcome>"| Key | Type | Required | Default | Description |
|---|---|---|---|---|
outcome |
success, failed or aborted |
No | success |
How the run ends. |
label |
text | No | The step’s id | The reason the run ended. |
final |
true |
No | Not set | Ends the chain of a repeating workflow. |
cause |
output name | No | None | Works as a route’s cause for every route into the terminal. Each step that routes here must be an agent that declares the output. |
Give a workflow a terminal for each way it can end. A terminal takes no next or routes. In an imported workflow, every terminal is an exit the importing workflow can continue from.
version = 3entry = "steps.review"
[steps.review]prompt = "Review the change. Choose pass when it is ready, or reject when it is not."outputs.verdict = { kind = "choice", options = ["pass", "reject"] }routes = [ { if = "verdict == 'pass'", to = "steps.approved" }, { if = "verdict == 'reject'", to = "steps.rejected" },]
[steps.approved]kind = "terminal"outcome = "success"
[steps.rejected]kind = "terminal"outcome = "failed"label = "The review rejected the change"A pass ends the run as Succeeded; a reject ends it as Failed, with the label as the reason.
Related: Error handling.
Organising steps
Section titled “Organising steps”Import
Section titled “Import”Puts another workflow file in this step’s place.
[steps.<id>]import = "<path>"exits.<terminal> = "steps.<id>"| Key | Type | Required | Default | Description |
|---|---|---|---|---|
import |
path | Yes | None | A complete workflow, relative to the file that names it. |
inputs.<name> |
template | For the imported workflow’s required inputs | None | The imported workflow’s inputs. |
exits.<terminal> |
step reference, or { to, loop_restart } |
No | Ends the run | Where the run goes when the imported workflow ends there. |
style |
name or list | No | None | Applies to every imported step. |
label |
text | No | The step’s id |
Before validation, Orbital puts the imported workflow in the import step’s place, so the editor, the validator and the run all see one workflow:
- The import step becomes a group, so imported ids take its id as a prefix:
review.reviewis the importedreviewstep. - Routes into the import step reach the imported workflow’s entry.
- The imported workflow’s terminals become exits, and
exitssays where each one leads. - Outputs and facts keep their own names, so a condition outside can read a value the imported workflow produced.
Relative imports and prompt files resolve from the file that names them. Import cycles are errors. An import that cannot be found fails loading and names the step, rather than producing a partial workflow that runs. The import step’s style applies below each imported step’s own styles, and the imported file’s styles join the importing workflow’s.
version = 3entry = "steps.implement"
[steps.implement]prompt = "Make the change the goal describes."next = "steps.review"
[steps.review]import = "imports/review.toml"exits.approved = "steps.done"exits.rejected = "steps.failed"
[steps.done]kind = "terminal"
[steps.failed]kind = "terminal"outcome = "failed"version = 3entry = "steps.review"
[steps.review]prompt = "Review the change in the working folder. Choose pass when it is ready, or reject when it is not. Do not change files."outputs.verdict = { kind = "choice", options = ["pass", "reject"] }routes = [ { if = "verdict == 'pass'", to = "steps.approved" }, { if = "verdict == 'reject'", to = "steps.rejected" },]
[steps.approved]kind = "terminal"
[steps.rejected]kind = "terminal"outcome = "failed"review is replaced by imports/review.toml. Its approved and rejected terminals are the exits the two exits keys map.
Related: Reuse a workflow, Give steps a style.
Gathers steps that belong together under one name.
[steps.<group>]style = "<style>"
[steps.<group>.<id>]prompt = "<template>"| Key | Type | Required | Default | Description |
|---|---|---|---|---|
style |
name or list | No | None | Applies to every step inside, at any depth. |
label |
text | No | The group’s name | Shown around the group in the editor. |
A table under steps with no kind, prompt, prompt_file or import is a group. It holds at least one step, takes no turn and is never a route’s target; route to one of its steps instead. Groups nest, and a step takes its innermost group’s style first.
version = 3entry = "steps.coder.plan"
[styles.careful]effort = "high"
[steps.coder]label = "The coder"style = "careful"
[steps.coder.plan]prompt = "Plan the change the goal describes. Do not change files."outputs.plan = "text"next = "steps.coder.implement"
[steps.coder.implement]prompt = "Carry out this plan: {{ context.plan }}"next = "steps.done"
[steps.done]kind = "terminal"Both coder.plan and coder.implement run with the careful style, because they sit in the coder group.
Related: Workflow file, Styles and personas.
Branching
Section titled “Branching”Routes live on the step the run leaves, so a branch is an ordinary agent, probe or command whose routes carry conditions. A decide step is the one kind whose only job is to choose a route, and it asks a decision model rather than following a rule.
version = 3entry = "steps.classify"
[steps.classify]prompt = "If the goal asks for a greeting, choose greet. Otherwise choose explain."outputs.route = { kind = "choice", options = ["greet", "explain"] }routes = [ { if = "route == 'greet'", to = "steps.greet" }, { if = "route == 'explain'", to = "steps.explain" },]
[steps.greet]prompt = "Write a friendly greeting."next = "steps.done"
[steps.explain]prompt = "Explain the goal in one sentence."next = "steps.done"
[steps.done]kind = "terminal"classify is an ordinary agent that chooses a route. Its two routes carry the branch, and because they answer both options, it needs no next.
Related
Section titled “Related”- Workflow file: every key a workflow and its steps take.
- Add branches: how routes choose the next step.
- Add gates and waits: how a run stops for a person or for time.
- Reuse a workflow: build and import a workflow of your own.