# Overview > Agents that finish the job. Turn goals into finished work. You pick a workflow and a project, and describe what you want. Orbital runs the workflow one step at a time on your own machine, with agents such as Claude Code, Codex and OpenCode, and you watch, steer and collect the result. Use this prompt to get started faster: ```text Help me set up Orbital, an app that runs agent workflows on my own machine. Its docs index is https://docs.runorbital.dev/llms.txt. Work through the steps below in order. Ask me one question at a time, and wait for my answer before you ask the next one. Don't change my files, except to register Orbital with you in step 3. 1. Install Orbital. Ask me which system I use. On a Mac, I download the app for Apple Silicon (https://releases.runorbital.dev/macos/Orbital-latest-arm64.dmg) or Intel (https://releases.runorbital.dev/macos/Orbital-latest-x64.dmg), drag it to Applications and open it. On Linux or Windows, follow https://docs.runorbital.dev/get-started/install.md to install the `orbital` command, then run `orbital serve`. 2. Check for an agent. Orbital runs Claude Code, Codex or OpenCode, and ships none of them. Check whether `claude`, `codex` or `opencode` is installed and signed in. If none is, ask me which one I want, and help me install it and sign in. 3. Connect Orbital to you over MCP. Ask me to open Settings > MCP in Orbital, and to paste the endpoint address and the token. Register a server named `orbital` with that address and the header `Authorization: Bearer `, the way https://docs.runorbital.dev/mcp/connect.md shows for your client. If your client needs a new session to see the server, tell me. 4. Pick a project. Call `list_projects`. If there is none, ask me to add a Git repository as a project in the app, then call it again. Ask me which project to use. 5. Start the first run. Call `start_run` with that project, the workflow `explain-repo` and the goal "Explain this repository: what it does, who it is for, how it is built and how to run it." The workflow only reads the repository. Follow the run with `wait_for_run` until it ends, then tell me what it found and where the app shows the run. ``` The prompt has your coding agent install Orbital, connect to it over MCP and start the read-only `explain-repo` workflow on a repository of yours. To do the same by hand, follow [Install Orbital](/get-started/install/) and [Start your first run](/get-started/first-run/). ![A run page with an agent step selected, showing the step's prompt, the agent's answer and the values it recorded, with run facts, pull requests and the list of steps on the right.](/screenshots/run-hero.webp?v=4fa21a573c) *A run shows every step: what the agent was asked, what it answered and what comes next.* ## What you can run | Kind of work | What Orbital does | Example | | --- | --- | --- | | Research and reporting | Researches a question on the web and writes a short report with its sources. | [Research report](/examples/research-report/) | | Triage | Proposes a label, a priority and a next step for each open issue. | [Triage issues](/examples/triage-issues/) | | Code | Implements a feature, delivers an epic ticket by ticket, or reviews pull requests. | [Implement a feature](/examples/feature-pull-request/) | Use Orbital for work you repeat. In one long agent session, you must remember when to research, draft, review, wait and stop. A workflow writes that process down once, so every run follows it. For a one-off question, a plain chat is enough, and Orbital has [chats](/chats/) too. ![You start a run from a project and a workflow. Each agent step runs on the harness and model chosen for it, and the run ends with a result such as a report, a summary, a triaged board or a pull request.](/overview.svg?v=86907d9fe0) ## The parts A **run** carries out one workflow for one project, towards one goal. Each step runs on the harness and model that its styles or the run chose, so one run can use several. Orbital keeps every run's history, and a run carries on where it was when Orbital restarts. Read [Runs](/runs/). A **workflow** is a file of steps. Most steps are prompts for an agent. Others are checks a machine does better, such as "did anything change?" or "did the pull request merge?". Each step's routes say what happens next, so a workflow can branch, loop and wait. Orbital ships eight workflows, from `explain-repo`, which only reads, to `implement-epic`, which delivers a whole epic. Read [Workflows](/workflows/) and [Shipped workflows](/reference/shipped-workflows/). A **project** is a name and the folders a run works in. A folder can be a Git repository, or a plain folder of notes and documents. Plain folders work with every harness except Codex, which needs a Git repository. Read [Projects and folders](/projects/). A **harness** is the agent program that does an agent step: Claude Code, Codex or OpenCode. You sign in to it yourself, and Orbital uses your plan. [Styles](/styles/) choose the harness and model step by step. Read [Harnesses and models](/harnesses/). The **delivery lead** is an optional chat that leads delivery for you. A watcher wakes it when a run stops, fails or runs out of quota, and it steps in inside the limits you set. Read [Delivery lead](/delivery-lead/). [How Orbital works](/concepts/how-orbital-works/) puts every idea on one page. ## How work reaches you Most results arrive as text. The agent's last reply is the answer, and each step's output stays in the run's **Context** tab. The research, follow-up and triage workflows work this way, and so does `explain-repo`. A workflow that changes code can add a worktree step. A **worktree** is an isolated copy of a repository on a branch of its own, so your own checkout stays untouched. Workflows that use one can then open a pull request, repair failing CI, conflicts and review comments, and wait for it to merge. Read [Worktrees and delivery](/projects/worktrees-and-delivery/). ## Where it runs Orbital runs on your machine. The Mac app starts its own server. On other systems, `orbital serve` starts the server and you use Orbital in a browser. Coding agents can drive Orbital too, over [MCP](/mcp/connect/). ## Find your way - [Get started](/get-started/install/): Install the app, add a project and start your first run. - [Runs](/runs/): Start, watch, message and stop a run, and read its result. - [Workflows](/workflows/): Write the steps an agent follows, with branches, loops and gates. - [Automate](/triggers/): Start runs from GitHub, Linear or a schedule, and let a delivery lead watch your runs. - [Examples](/examples/feature-pull-request/): Complete workflows for code, research and triage, with downloads. - [Reference](/reference/workflow-file/): Look up the workflow file, the command line, the HTTP API and MCP tools. ## Related - [Install Orbital](/get-started/install/): get the Mac app or the `orbital` command and sign in to a harness. - [Start your first run](/get-started/first-run/): run `explain-repo` on a repository you already have. - [Build with AI](/get-started/build-with-ai/): connect coding agents, and give them these docs as Markdown. - [How Orbital works](/concepts/how-orbital-works/): every idea in Orbital, with a link to its page. --- # Install Orbital > Install the Mac app or the orbital command, then sign in to a harness You install Orbital, then sign in to at least one harness, the coding agent that does the work: Claude Code, Codex or OpenCode. At the end, **Settings > Harnesses** shows a harness ready to take a run. About 5 minutes. ## Before you start On a Mac, you need nothing else. Orbital.app starts its own server, updates itself and needs neither npm nor Node. On Linux or Windows, you install the `orbital` command with npm and run `orbital serve`. You need Node 24.19 or newer and `git`, and an invitation to the npm package during the beta. ## 1. Install Orbital **Mac app** 1. **Download the disk image for your Mac.** These addresses always serve the newest release: [Apple Silicon](https://releases.runorbital.dev/macos/Orbital-latest-arm64.dmg) or [Intel](https://releases.runorbital.dev/macos/Orbital-latest-x64.dmg). 2. **Drag Orbital to Applications.** Open the disk image and drag Orbital to the **Applications** folder. 3. **Open Orbital.** Open it from **Applications** or the Dock. The first launch asks you to set up a project. ![The first-launch wizard with the heading Give your agents a place to work, the steps Welcome, Project and First run, and a Set up a project button.](/screenshots/first-launch.webp?v=5ece35762f) *The first launch walks you through adding a project.* The app is signed and notarised. It reads your login shell's `PATH`, so it finds `git`, `claude`, `codex` and `opencode` where your terminal finds them. It serves on port 42121, or on a free port when another program, or an Orbital server on another home, holds that one. A second Orbital server on the same home refuses to start and names the one already running. **Command line** The Mac app carries the `orbital` command. To use it from a terminal: 1. **Open the command line settings.** Open **Settings > Command line and skills**. 2. **Install the command.** Under **The orbital command**, choose **Install**. You can also choose **Orbital > Install Command Line Tool** from the menu bar. 3. **Give your password if asked.** Enter your administrator password if your Mac asks for it. 4. **Check the command.** Open a new terminal and run `orbital --help`. This links `/usr/local/bin/orbital` to the command inside the app. After the app updates, the command runs the new release without another install. `orbital run start` then starts runs in the open app. [The orbital command](/command-line/) explains every command. **npm** npm access is by invitation during the beta. The Mac app is the main way to get Orbital, and npm releases are on hold. To ask for access, contact the Orbital team through [runorbital.dev](https://runorbital.dev) and give your GitHub account name. Use npm on Linux or Windows, or on a Mac without the app. You need Node 24.19 or newer and `git`. Check them: ```sh node --version git --version ``` The package is `@flashingpumpkin/orbital` on GitHub Packages, which needs a token even to read it. Once your invitation gives your GitHub account read access: 1. **Create a token.** Create a [classic personal access token](https://github.com/settings/tokens) with only the `read:packages` scope. 2. **Add the registry to npm.** Add these two lines to `~/.npmrc`, with your token in place of ``: ```ini title="~/.npmrc" @flashingpumpkin:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken= ``` 3. **Install and check.** ```sh npm install --global @flashingpumpkin/orbital orbital --version ``` 4. **Start the server.** ```sh orbital serve ``` The server prints its address, `http://127.0.0.1:42121/` unless you change the port, and opens it in your browser. It listens only on your own machine. `orbital app` opens the same app in a window of its own instead, on this server when one is running. [The orbital command](/reference/command/#orbital-serve) lists the flags. With pnpm, allow Orbital's storage dependency to build: `pnpm add --global --allow-build=better-sqlite3 @flashingpumpkin/orbital`. This registry token only downloads the package. Orbital never reads it. Workflows that read GitHub use a separate GitHub token; see [Settings](/reference/settings/#github). :::caution The server has no login of its own. Keep it on your own machine unless you protect the network yourself. ::: ## 2. Install and sign in to a harness Orbital runs the harnesses you install yourself, and ships none of them. Install at least one. Sign in before you start Orbital, because a running server does not see credentials you export later. **Claude Code** Install it with `npm install -g @anthropic-ai/claude-code@latest`. Orbital works with version 2.1.280 or newer. To sign in, run `claude` and follow the login. Leave `ANTHROPIC_API_KEY` unset to use your subscription. The [Claude Code setup guide](https://code.claude.com/docs/en/setup) covers installing it on your system. **Codex** Install it with `npm install -g @openai/codex@latest`. Orbital works with version 0.156.0 or newer. To sign in, run `codex login`. The [Codex guide](https://learn.chatgpt.com/docs/codex/cli) covers installing it on your system. **OpenCode** Install it with `npm install -g opencode-ai@latest`. Orbital works with version 1.18.0 or newer. To sign in, run `opencode auth login` for your provider. The [OpenCode guide](https://opencode.ai/docs/cli/#auth) covers installing it on your system. ## 3. Check that Orbital finds the harness Check the harness in a terminal first: `claude --version`, `codex --version` or `opencode --version`. Then open **Settings > Harnesses** in Orbital. The status table shows whether each harness is installed, whether it is new enough and whether it is signed in. ![Settings, Harnesses page with a status table showing whether Claude, Codex and OpenCode are installed and signed in, followed by Claude's settings for turned on, program, default model, default effort and environment variables.](/screenshots/settings-harnesses.webp?v=6446e71ff0) *The status table shows which harnesses are ready.* Orbital looks for each harness in this order: 1. The program set under **Program** in **Settings > Harnesses**. 2. `claude`, `codex` or `opencode` on the `PATH` of your login shell. The Mac app reads that `PATH` from your shell when it opens, so Orbital finds any harness you can start in a terminal. A harness Orbital cannot find reads "Not installed", with the command to install it. Install it, then choose **Check again**. [Harnesses and models](/harnesses/) explains more. ## Upgrade Orbital **Mac app** The app checks for a new release when it starts and every hour. It downloads an update in the background and says "Orbital \ is downloading". When the update is ready, choose **Restart to update**, or it installs the next time you quit. **Settings > About** shows the version you are running, the last check and its result, and a **Check for updates** button. Orbital updates itself only from the **Applications** folder. Opened from the disk image, it says "Move Orbital to Applications to get updates". ![Settings, About page showing the running version, its updates and the path of the settings file.](/screenshots/settings-about.webp?v=ae05abacce) *About shows the version you run and whether an update is waiting.* **Command line** The `orbital` command installed from the Mac app is a link to the command inside the app. It updates with the app, and runs the new release without another install. **npm** The npm package checks for a newer release when the server starts and once an hour. In a terminal, `orbital serve` asks before it starts: ```text Orbital is available. You are running . Update before starting? [Y/n] ``` A server that is already running shows "Orbital \ is available." with an **Update and restart** button. Runs that are working carry on by themselves after the restart. You can also run `npm update --global @flashingpumpkin/orbital` and restart the server. Your data in `~/.orbital` survives upgrades and uninstalling. :::caution An older release refuses run history that a newer one has updated. Keep a copy of `~/.orbital/run-history.db` before you go back to an older release. ::: ## Next steps - [Start your first run](/get-started/first-run/): add a project and run `explain-repo`. - [Harnesses and models](/harnesses/): how Orbital picks and starts each harness. - [Command line](/command-line/): what each subcommand does. - [Troubleshooting](/troubleshooting/): fixes when the app or a harness will not start. --- # Start your first run > Add a repository as a project, run explain-repo and read its answer You add a repository you already have as a [project](/projects/) and run the `explain-repo` workflow on it. One agent step reads the repository and answers with its purpose, who it is for, how it is built and how to run it. About 10 minutes. The run itself usually takes one to three minutes. The run only reads. It makes no worktree, no branch and no pull request. ## Before you start You need a Mac for the app. On Linux or Windows, [install the `orbital` command](/get-started/install/) with npm and run `orbital serve` instead. The steps in the browser are the same. You also need a repository on your machine to explain. ## 1. Install Orbital and sign in to a harness 1. **Download Orbital.** Download it for [Apple Silicon](https://releases.runorbital.dev/macos/Orbital-latest-arm64.dmg) or [Intel](https://releases.runorbital.dev/macos/Orbital-latest-x64.dmg). 2. **Drag Orbital to Applications.** Open the disk image and drag Orbital to **Applications**. 3. **Install a harness and sign in.** Orbital ships no [harness](/harnesses/), so install one yourself. Any of the three runs this first run. **Claude Code** Run `npm install -g @anthropic-ai/claude-code@latest`. Then run `claude` in a terminal and follow the login. The [Claude Code setup guide](https://code.claude.com/docs/en/setup) has other ways to install it. **Codex** Run `npm install -g @openai/codex@latest`, then `codex login`. The [Codex guide](https://learn.chatgpt.com/docs/codex/cli) has other ways to install it. Codex starts only in a Git repository, so add a repository, not a plain folder, as your project. **OpenCode** Run `npm install -g opencode-ai@latest`, then `opencode auth login`. The [OpenCode guide](https://opencode.ai/docs/) has other ways to install it. 4. **Open Orbital.** Open it from **Applications**. Orbital finds `git`, `claude`, `codex` and `opencode` on your login shell's `PATH`. **Settings > Harnesses** shows each harness. A harness it cannot find reads "Not installed", with the command to install it. [Install Orbital](/get-started/install/) has the details for each harness. ## 2. Add the repository as a project The first time Orbital opens, it asks you to "Give your agents a place to work." ![The first-launch wizard with the heading Give your agents a place to work, the steps Welcome, Project and First run, and a Set up a project button.](/screenshots/first-launch.webp?v=5ece35762f) *The wizard opens on the first launch.* 1. **Start the project setup.** Choose **Set up a project**. 2. **Pick a folder.** Choose **Choose folder** and pick a repository on your machine. A plain folder works too, except on Codex; see [plain folders and Git](/projects/#plain-folders-and-git). Orbital names the project after the folder. ![The first-launch wizard while a project is being added, with the same Welcome, Project and First run steps.](/screenshots/add-project.webp?v=2d32cbc234) *Orbital names the project after the folder you pick.* 3. **Add the project.** Choose **Add project**. The project is ready when the app says "Your project is ready." 4. **Go to the first run.** Choose **Prepare first run**. Already past the first launch? Open **Projects** and choose **New project**. Name it, then choose **Add folder** on the project's page. The first folder you add is the project's primary folder. [Projects and folders](/projects/) explains more. ## 3. Run explain-repo 1. **Open a new session.** Choose **New task** in the sidebar. The project you added is selected at the top. 2. **Choose Workflow mode.** Make sure the switch under the text box says **Workflow**, not **Chat**. 3. **Pick the workflow.** Open the workflow menu and choose `explain-repo`. 4. **Write the goal.** In the text box, write what you want to know, for example `Explain this repository to a new team member`. This is the run's goal. ![The Start page asking what to do in a project, with the goal box, the preset picker, the workflow picker and the Chat and Workflow switch.](/screenshots/start-page.webp?v=7454bfb8bf) *Set the switch to Workflow, pick explain-repo, then write your goal.* 5. **Pick a preset.** Leave the preset picker on the preset in use, or choose one whose harness you signed in to. A chosen preset applies to every step of this run. 6. **Start the run.** Choose **Start run**. The step only reads the repository and explains it. Its prompt tells it not to change, create or delete any file, and to run only commands that read. It works the same on Claude Code, Codex and OpenCode. ![explain-repo has one step, explain, and then done.](/shipped/explain-repo.svg?v=900c23c839) The run page opens. The **Session** tab shows the `explain` step working: the files the agent reads and its reply as it writes it. [Watch a run](/runs/watch-a-run/) explains the rest of the page. ## 4. Read the answer The answer is the agent's last message in the **Session** tab, just above "Run ended: success". It has four parts: 1. Purpose: what the repository does. 2. Who it is for. 3. How it is built. 4. How to run it. The agent says where it found each fact. It also says so when the repository does not tell. The **Context** tab keeps the same answer as `response.explain`. ## If the run stops A run that cannot start its harness stops. It is [Paused](/runs/#standings), and its badge reads "Waiting on you". The step shows why, for example that the harness is not installed or not signed in. Fix that, then choose **Resume**. [Troubleshooting](/troubleshooting/) lists the common causes. ![A run page on the Session tab showing its steps one by one and ending in Stopped with an error, with run facts such as project, workflow, branch, harness, model, run ID and path on the right.](/screenshots/run-thread.webp?v=f4f203d47e) *The Session tab says where and why a run stopped.* ## Next steps - [Implement a feature](/examples/feature-pull-request/): deliver a change as a pull request. - [Research report](/examples/research-report/): turn a question into a written report. - [Runs](/runs/): what a run is and what you can do with one. - [Permissions](/styles/#permissions): what a run asks you before its agent acts, on each harness. - [How Orbital works](/concepts/how-orbital-works/): the ideas behind the run you just watched. - [Write your first workflow](/workflows/write-a-workflow/): write a workflow of your own. --- # Build with AI > Give agents and chat assistants Orbital's docs, its MCP endpoint and its skills You can hand Orbital to an AI tool in three ways: give an assistant the docs as Markdown, connect a coding agent to Orbital's MCP endpoint, or install the skills Orbital ships. The quickest start is to give your assistant the docs index: ```text https://docs.runorbital.dev/llms.txt ``` ## Give an assistant the docs Every page of these docs is also published as plain Markdown, for assistants to read. | File | What it holds | | --- | --- | | [`llms.txt`](https://docs.runorbital.dev/llms.txt) | An index of every page, with absolute links, grouped by tab: Docs, Examples and Reference. | | [`llms-full.txt`](https://docs.runorbital.dev/llms-full.txt) | Every page in one file. | | A page's Markdown copy | One page. Add `.md` to the page's URL without its trailing slash, such as `/runs/start-a-run.md`. The home page is `/index.md`. | Each Markdown copy starts with a line that points to `llms.txt`, so an assistant that reads one page can find the rest. Every page also advertises its copy with ``, for tools that look for it. ### The Copy page button Every page has a **Copy page** button at the top. It copies the page's Markdown, ready to paste into a chat. Its menu has more: | Item | What it does | | --- | --- | | View as Markdown | Opens the page's Markdown copy. | | Open in ChatGPT, Open in Claude, Open in T3 Chat, Open in Cursor | Opens the assistant with the prompt "Read *the page's .md URL*, I want to ask questions about it." | ## Connect a coding agent over MCP MCP, the Model Context Protocol, is how coding agents connect to tools. Orbital serves MCP over Streamable HTTP, on the same address as the app, at `/mcp`. A coding agent such as Claude Code, Codex or OpenCode that connects to it can do what you do in the app. It can list, start, follow, message and control runs, and read and validate workflows. It can also read where a run's pull request stands and what its worktree holds, and secure a run's work, without `gh` or `git` on the machine. The endpoint is on by default. Every call must carry a token, even for `orbital serve`. **Settings > MCP** shows the address and the token, and gives each client a ready-made setup to paste into a terminal. [Connect a coding agent](/mcp/connect/) shows the steps. ![Settings, MCP page showing a switch for each tool group: reading runs and workflows, starting runs, controlling runs, messaging and answering runs, archiving, and changing Orbital's settings.](/screenshots/settings-mcp-tools.webp?v=73920e6114) *Each tool group has its own switch.* Each tool group has a switch. Reading, starting, controlling, messaging and answering are on by default. Archiving and changing Orbital's settings are off until you turn them on. [MCP tools](/reference/mcp-tools/) describes every tool. This endpoint is for agents outside Orbital and for the built-in [delivery lead](/delivery-lead/). The agents inside a run use a separate MCP server of their own. ## Install the shipped skills A skill is a folder of instructions an agent loads when a task matches it. Orbital ships two: | Skill | What it does | | --- | --- | | `orbital-delivery-lead` | Delivers tickets and epics through Orbital runs, as a delivery lead. It refines tickets, starts runs, checks in on them, unsticks them and closes what they deliver. It needs the MCP endpoint connected. | | `orbital-create-workflow` | Designs a new Orbital workflow with you, one decision at a time. Then it writes the files and validates them. | Install them from **Settings > Command line and skills**, or with `orbital skill install`. They work for every project on your machine. [Use the shipped skills](/mcp/skills/) explains how. ![Settings, Command line and skills page showing the orbital command installed and the agent skills installed.](/screenshots/settings-command-line.webp?v=2453b7527a) *Install the skills and the orbital command from one page.* ## Choosing a route | You want | Use | | --- | --- | | An assistant that answers questions about Orbital | `llms.txt`, `llms-full.txt` or a page's Markdown copy | | A coding agent that starts, follows and steers your runs | The MCP endpoint | | A coding agent that delivers tickets or designs workflows the way Orbital expects | The shipped skills, with the MCP endpoint for delivery | ## Related - [Connect a coding agent](/mcp/connect/): register Claude Code, Codex, OpenCode or another client. - [Use the shipped skills](/mcp/skills/): install the skills and ask agents to use them. - [MCP tools](/reference/mcp-tools/): every tool, its arguments and its results. - [Lead delivery](/delivery-lead/lead-delivery/): the loop the delivery lead skill follows. --- # How Orbital works > How a run carries out a workflow, step by step, with the coding agents you already use You give Orbital a goal, a [workflow](/workflows/) and a [project](/projects/), and it starts a [run](/runs/). The run carries out the workflow one step at a time, hands each agent step to a coding agent you already use, and records every result, so you can follow it, steer it and pick it up after a restart. ![A run page with an agent step selected, showing the step's prompt, the agent's answer and the values it recorded, with run facts, pull requests and the list of steps on the right.](/screenshots/run-hero.webp?v=4fa21a573c) *Most ideas meet on the run page: the run, its workflow's steps, its harness and its project.* ## A run works one step at a time A workflow is a TOML file of steps, each with the routes that lead out of it. Its `entry` names the first step. From there, the run takes one step at a time: - An agent step fills in its prompt and sends it to a harness. - A probe step reads facts, such as the state of a pull request, and a command step runs your shell command and records its facts. - An action step makes a built-in change, such as opening a pull request. - A step's routes and its `next` choose what happens next. - A terminal step ends the run. The harness is the coding agent that does an agent step: Claude Code, Codex, OpenCode or Pi. A step takes each turn setting, such as its harness, model and effort, from the first place that sets it: the step itself, its [style](/styles/), the styles of the groups holding it, a model picked when the run started, the workflow's style, the run's preset, else **Settings > Harnesses**. [Which harness a step uses](/harnesses/#which-harness-a-step-uses) gives the order. The transcript shows the harness and model each step used. ![The run page's Waterfall tab shows each step as a bar on a timeline with its duration, including repeated visits such as probe@2 and wait@2.](/screenshots/run-steps.webp?v=fee718c962) *Each visit to a step is its own bar; a repeat visit carries a number such as @2.* ## What a run remembers A run begins with its goal and declared inputs. Its context holds those values, accepted agent outputs, command facts and each step's outcome. Prompts read context. Routes only read values available on their path through the workflow. [Pass context between steps](/workflows/pass-context/) explains what a step can read. Agent outputs are checked, structured data. Orbital does not scrape them from the agent's prose. An `outputs` declaration tells Orbital which keys and types to accept. The agent's final response stays available under `context.response.`, where `` is the step's id. Built-in probes only observe. A `probe` step with `observe = "github.pr"` reads each repository folder's current pull request from GitHub. Command steps are different: they run your shell command once in the primary folder and declare their own typed facts. ![The run page's Context tab lists fourteen values held by the run, such as github.pr.state and summary, each with the step that set it on the right.](/screenshots/run-context.webp?v=3e79bd8607) *Each context value shows the step that set it.* ## Runs carry on after a restart Orbital keeps every run's progress, context and transcript in `~/.orbital/run-history.db`. `ORBITAL_HOME` moves it to `$ORBITAL_HOME/.orbital`. Closing the browser leaves the server running. When Orbital restarts, every run you did not pause carries on from its last finished activity, with the context it had: - A finished step does not run again. - A wait wakes at the time it first set. - An agent turn continues its session. The message an agent was writing when Orbital restarted may be lost. A tool call that was running may run again. A paused run stays paused until you resume it. **Resume** carries the step on in the same agent session. **Retry** starts the step again as a new visit. A run keeps the workflow as it was when the run started, and the harness you picked for it. Orbital rereads a prompt file before a later visit. If your edit is invalid, it keeps the checked prompt. [Durability after restarts](/runs/durability/) covers restarts in full. ## You can steer a live run Open the run and read the active step and its transcript. Send a message when the agent needs a correction. A message you queue reaches the agent whole in its next turn that can take it, even when the history sent with that turn is shortened. Harnesses differ in whether they can take a message during a live turn. [Message a run](/runs/message-a-run/) shows how. [Pause](/runs/control-a-run/) the run to stop its current work before you change direction. Answer questions and permission requests on the run page. During a wait, skip the remaining time only when the observation should run again now. :::caution A manual jump changes the next step and may lack context that normal routing would have produced. Read its warnings before you confirm. Use normal routing when it already expresses the correction you need. ::: ## When a step fails What happens depends on where the failure is: | Failure | Result | | --- | --- | | A step fails and a failure route matches | The run takes the route and may recover. | | A step fails and no failure route matches | The run is Paused and shows "Waiting on you" until you act. | | A terminal step fails | The run ends as Failed. | These are different [standings](/runs/#standings), so check both the standing and the transcript. [Error handling](/reference/error-handling/) gives the exact rules. [Troubleshooting](/troubleshooting/) explains how to recognise each state in the app. ![The run page's Waterfall tab highlights a failed command step with its error output and a red Stopped with an error box, followed by Run ended: failed.](/screenshots/run-current-step.webp?v=cf3386ca9f) *The failed step is highlighted, with the error that stopped it.* ## Quick reference | Idea | What it is | | --- | --- | | [Run](/runs/) | One workflow carried out for one project, towards one goal. Orbital keeps its full history, and you can follow, message, pause and resume it. | | [Workflow](/workflows/) | One TOML file of steps and the routes between them. Orbital loads, validates and runs it, and ships nine of its own. | | [Step](/reference/step-kinds/) | One part of a workflow. Its `kind` decides what it does, from agent steps to probes, actions, decide steps, gates and fan-outs. | | [Gates and waits](/workflows/gates-and-waits/) | A gate stops a run until a person lets it continue. A wait stops it for a set time. Both survive a restart. | | [Run queue](/runs/queue/) | Orbital works on at most a set number of runs at once, two unless you change it. Other runs wait and start as slots free up. | | [Durability](/runs/durability/) | When Orbital restarts, every run you did not pause carries on where it was. Queued runs keep their place. | | [Project](/projects/) | A name and the folders a run works in. A folder can be a Git repository or a plain folder. Every run belongs to one project. | | [Worktree](/projects/worktrees-and-delivery/) | An isolated copy of a repository on a branch of its own. Code workflows use it to open a pull request and repair it until it merges. | | [Harness](/harnesses/) | The coding agent that does an agent step: Claude Code, Codex or OpenCode. You install and sign in to each yourself, so each step runs on your own plan. | | [Style](/styles/) | A named set of turn settings, such as the harness, model, effort and persona, that a step, a group or the whole workflow can use. | | [Chat](/chats/) | A conversation with one coding agent in a project, with no workflow behind it. Use it for work you do not repeat. | | [Trigger](/triggers/) | A rule that acts on runs when something happens in GitHub, Linear or on a schedule. It can start a run, or stop, pause or message runs already working. | | [Delivery lead](/delivery-lead/) | An optional chat that leads delivery inside Orbital. When a run needs help, it steps in within the limits you set. | ## Related - [Start your first run](/get-started/first-run/): see runs, projects and harnesses work together. - [Pass context between steps](/workflows/pass-context/): what goes into context and what a step can read. - [Pause, stop or resolve a run](/runs/control-a-run/): take control of a live run. - [Error handling](/reference/error-handling/): the exact rules for failed steps. - [Settings](/settings/): where you change how each part behaves. --- # Runs > A run carries out one workflow for one project, towards one goal You start a run to carry out one [workflow](/workflows/) for one [project](/projects/), towards one goal. While it works, you can follow it, send its agent a message and pause it, and it picks up again after a restart. Orbital keeps each run's full history. Each step runs on the harness and model that its styles, its own keys or the run's preset chose, so one run can use several harnesses and models. [Harnesses and models](/harnesses/) gives the order. ![A run page with an agent step selected, showing the step's prompt, the agent's answer and its recorded values, with the run's facts, pull requests and steps in the dock on the right.](/screenshots/run-hero.webp?v=4fa21a573c) *A run page: the selected step in the middle, the run's facts and steps on the right.* ## Standings Each run has one standing at a time. The runs list, the run page and the MCP tools use the same names. | Standing | Meaning | | --- | --- | | Created | Recorded but not started. | | Queued | Waiting for a free run slot in the [queue](/runs/queue/). | | Running | A step is working. | | Question | The agent asked you something and waits for the answer. | | Waiting | The run waits for time or at a [human gate](/workflows/gates-and-waits/). A timed wait shows "Wakes in" and the time left. A run at a gate shows "Waiting:" and the gate's label, such as "Waiting: The pull request is waiting for review or merge". The Status filter and the MCP tools call it **At a gate** and `AtGate`. It carries on by itself. | | Paused | Nothing moves until you act. A run you paused shows "Paused". A run that stopped by itself shows "Waiting on you". It stops by itself when a step fails and the workflow has no route for the failure, when the harness is missing or out of quota, when a step reaches its visit limit, or when Orbital cannot read the workflow. A paused run holds no run slot. | | Succeeded | It reached a terminal step that ends in success. | | Failed | It reached a terminal step that ends in failure, or spent the workflow's `max_visits`. | | Aborted | Someone aborted it. | ## The phase field The HTTP run listing and the MCP `list_runs` and `get_run` tools return a `phase` field beside the standing. The phase says which part of a step the run is in. Use the standing to decide what to do. Use the phase when you need the finer detail. | Phase | Meaning | Standing | | --- | --- | --- | | Created | Recorded but not started. | Created | | Queued | Waiting for a free run slot. | Queued | | Entering | Starting a step. | Running, Question or Waiting | | Executing | A step is working. | Running, Question or Waiting | | Selecting | Choosing the next step from the routes. | Running, Question or Waiting | | AwaitingOperator | At a human gate, or stopped by itself. | Waiting at a gate, or Paused ("Waiting on you") | | Interrupted | You paused it. | Paused | | Ended | Reached a terminal step or was aborted. | Succeeded, Failed or Aborted, from the run's `outcome` | A run at a gate has the phase AwaitingOperator, as has a run that stopped by itself. For a run at a gate, the HTTP listing also returns `waitingOn`, the gate's label. ## Read the result A run ends at a terminal step. The **Session** tab then shows "Run ended: success". A run that failed or was aborted shows "Run ended: failed" or "Run ended: aborted" as an alert. What the run delivered depends on its workflow: | Workflow | Where the result is | | --- | --- | | Answers or writes, such as `explain-repo` or a research report | The agent's reply. It is the last message in the **Session** tab, above "Run ended". The **Context** tab keeps each step's reply as `response.`, and the latest one as `last_response`. | | Delivers code, through a worktree and a pull request | The dock's **Pull requests** section lists its pull requests. The **Changes** tab shows the branch's changes. | ![The dock's Pull requests list, showing open and merged pull requests with their branch names.](/screenshots/pull-requests-panel.webp?v=612ac483b1) *A code run's pull requests, listed in the dock.* ## Titles Orbital titles a run the moment you start it. It uses a ticket's identifier and title, a link's short form, or the first line of the goal. A small model then writes a better title from the ticket and from the first step's work. To rename a run, choose its title in the header. Orbital never replaces a title you gave. ## Quick reference | You want | Where to look | | --- | --- | | Whether a run needs you | Its standing, in the runs list or on the run page | | Finer detail for a script or an agent | The `phase` field from the HTTP run listing or the MCP tools | | A run's answer | The last message in the **Session** tab, or `response.` in the **Context** tab | | A run's pull requests | The dock's **Pull requests** section, and the **Changes** tab | | A new title | The run's title in the header | ## Related - [Start a run](/runs/start-a-run/): give a project a goal and a workflow. - [Watch a run](/runs/watch-a-run/): find runs and read the run page. - [Message a run](/runs/message-a-run/): steer the agent or answer its question. - [Pause, stop or resolve a run](/runs/control-a-run/): every control and what it does. - [The run queue](/runs/queue/): how many runs work at once, and in what order. --- # Workflows > A workflow is one TOML file of steps that Orbital runs You describe the work a run does as a workflow: one TOML file that Orbital loads, validates and runs. The smallest complete one has a version, an entry step, one agent step and one terminal: ```toml title="minimal.toml" version = 3 entry = "steps.hello" [steps.hello] prompt = "Reply with one sentence explaining the goal: {{ inputs.goal }}." next = "steps.done" [steps.done] kind = "terminal" ``` A workflow declares `version = 3`, the `entry` step the run starts at, and its [steps](/reference/step-kinds/) under `steps`. Each step says where the run goes next. [Write your first workflow](/workflows/write-a-workflow/) walks through this one. Keep the file in your project's `.orbital/` folder as `.toml`. The file name is the workflow's name. A step's ID is its path under `steps`, so `[steps.hello]` has the ID `hello`, and a reference to it is the quoted path `"steps.hello"`. Give every step a stable ID, because the run transcript, visit counts and manual jumps all address steps by ID. Write each prompt for someone arriving without prior conversation: state the expected result and the allowed actions. ## Why TOML? TOML gives every setting its own key. Settings that nest, such as a step's outputs or a style's Codex options, are real tables rather than small languages packed into a quoted string. One key holds one fact, so a change to the file reads as a change to that fact when you review it in Git, and Orbital refuses a key it does not know, naming the key and where it is. Run [`orbital validate`](#validate-before-you-run) to check a file as you write it. Workflows written in DOT, the format before version 3, no longer load, and a run of one is refused with a message saying how to convert it. Orbital converts the workflows it keeps itself, your saved custom workflows and your edits to built-in workflows, when it starts. A saved workflow it cannot convert stays as it was, and the Workflows page lists it with the problem, so you can rewrite it in the editor's Source view. DOT files stay where you put them. When a project's `.orbital/` folder or your `~/.orbital/workflows/` folder still holds some, the Workflows page says how many and offers **Convert to TOML**. It writes each `.toml` file beside its `.dot` file, leaves the `.dot` file in place, and lists each file it wrote with what it dropped, such as tool limits, and each step it could not convert. [`orbital migrate`](/reference/command/#orbital-migrate) does the same from the command line. ## Group steps A table under `steps` that has no `kind`, `prompt`, `prompt_file` or `import` is a group. It holds steps that belong together, takes no turn of its own, and is never a place a route can send the run. A group must hold at least one step, and groups nest to any depth. ```toml title="group.toml" version = 3 entry = "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" ``` The steps inside take the group's name into their IDs: `[steps.coder.plan]` has the ID `coder.plan`, and a route reaches it as `"steps.coder.plan"`. A group may set a `label`, and a `style` that applies to every step inside it. Here `plan` and `implement` both run with high effort from the `careful` style without naming it. A step's own keys and its own `style` still win, and an inner group's style wins over an outer one's. A `style` at the top of the file sets defaults for every step in the workflow in the same way. [Give steps a style](/styles/configure-steps/) gives the full order. ## Where Orbital looks for a workflow Orbital searches these places in order and takes the first match: | Order | Place | | --- | --- | | 1 | Custom workflows saved in the browser. | | 2 | The `.orbital/` folder of the project's primary folder. | | 3 | The `.orbital/` folder of each remaining folder. | | 4 | Your library, `~/.orbital/workflows/`. | | 5 | The workflows shipped with Orbital. | Because the first match wins, a custom workflow named `hello` shadows a file named `hello.toml`. Use a distinct name while experimenting. Pass an explicit path to `orbital validate` when you diagnose a collision. ## Shipped workflows Orbital ships eight workflows. They appear in the new-run menu of every project. A workflow of the same name in any other place takes a shipped one over. | Workflow | What it does | What it needs | | --- | --- | --- | | `explain-repo` | Reads the project's primary folder in one step that only reads, and explains the repository. No worktree, no pull request. | An optional goal | | `implement-ticket` | Plans, implements and reviews one ticket, then delivers it to merge. | A ticket URL, as `ticket_url` | | `implement-epic` | Delivers an epic's leaf tickets one at a time, then closes the epic. | An epic URL, as `ticket_url` | | `implement-epic-with-panel` | Delivers an epic the same way, with a panel of parallel reviewers. | An epic URL, as `ticket_url` | | `improve-codebase` | Finds and delivers one worthwhile improvement, then stops. | An optional goal | | `reduce-complexity` | Simplifies the function most worth simplifying. | An optional goal | | `pursue-goal` | Implements and validates a goal in a loop, then delivers it. | A goal in plain words | | `port-project` | Ports an application to a new stack, one proved slice per pull request. | A goal naming the source repository and the target stack | [Shipped workflows](/reference/shipped-workflows/) describes each one, with its diagram. ## The Workflows page Click **Workflows** in the sidebar to list every workflow the selected project offers. A badge says where each one comes from. ![The Workflows page lists four workflows, each with a source badge such as Project, Custom or Home, and Clone, Export or Revert to built-in buttons. Search, Import and New workflow sit at the top.](/screenshots/workflows-page.webp?v=991e5fde13) *Each workflow's badge says where it lives and whether you can edit it here.* | Badge | Where it lives | Can you edit it here? | | --- | --- | --- | | Custom | Saved in Orbital from this page. | Yes. | | Project | The `.orbital/` folder of one of the project's folders. | Yes. Saving writes the file in place and leaves it uncommitted. | | Home | Your library, `~/.orbital/workflows/`. | Yes. Saving writes the file in place. | | Shipped | Comes with Orbital and is kept in Orbital's database. | Yes. Saving edits it in place. | A shipped workflow you have saved stands as edited, and the editor offers **Reset to shipped** to bring the shipped version back. A later release updates every shipped workflow you have not edited and leaves your edits alone; for one you edited, the editor says when a newer shipped version exists. Deleting a shipped workflow hides it, and **Restore** on the Workflows page brings it back. A Custom workflow with a shipped name shows **Takes over built-in**; reverting it deletes it. On this page you can create a workflow, import one, clone one, export one as a file, and open one in the editor. Export writes one self-contained file with the prompts inlined. **Import** opens a box where you paste a workflow's TOML. Paste a DOT workflow and Orbital converts it to TOML, and lists what the conversion dropped as warnings. Nothing is stored until you save. Prompt files the workflow refers to become empty prompts for you to fill in. ![The Import a workflow page has one large text box with Import and Cancel buttons below it.](/screenshots/import-workflow.webp?v=b5c29c87bd) *Paste the workflow's TOML; nothing is stored until you save.* ### The editor The editor draws the workflow and validates it as you edit. The **Problems** bar at the bottom lists what validation found. The **Inspector** on the right holds the workflow's description, entry, inputs and turn budget. Move around the canvas with a two-finger swipe, or drag an empty area. Pinch or hold ⌘ while scrolling to zoom. Work in progress is kept as a Draft, even while it is not valid yet. A Draft cannot be run. It becomes a Custom workflow once you save it as a valid workflow. The browser workflow library also creates and edits custom workflows, and saving validates them. Inspect the selected workflow before you start a run. ![The workflow editor shows a project workflow as steps joined by arrows on a canvas, with the Inspector panel on the right and a Problems bar reading No problems at the bottom.](/screenshots/workflow-editor.webp?v=85a9c6275a) *A project workflow saves to its file in place; commit it to share it. Use Duplicate to make a copy.* ## Declared inputs Declare each value the workflow needs from the person who starts it as an `[inputs.]` table. Its `kind` chooses the field on the new-run form and how Orbital checks the value. `label` and `hint` explain it on the form; without a label, the form shows the input's name. ```toml [inputs.ticket] kind = "ticket" from = ["github", "linear"] label = "Ticket" hint = "The issue to deliver" [inputs.notes] label = "Notes" required = false ``` An input is required unless it sets `required = false`, and a `default` fills it when nobody gives a value. `goal` is built in; an `[inputs.goal]` table may only change its `label`, `hint` and `default`. Starting a run requires the goal and every required input, so a workflow states what it needs and you do not have to remember it. From the command line, pass each input with `--input =`. | Kind | What the input holds | | --- | --- | | `text` | Free text. The default when an input sets no `kind`. | | `short_text` | One line of text. | | `number` | A number, kept between `min` and `max` when you set them. | | `yes_no` | Yes or no. | | `url` | A link. | | `choice` | One of its `options`, given as a list or as a table of values and their descriptions. | | `ticket` | A GitHub issue or Linear issue. | | `epic` | A GitHub issue or Linear issue that other tickets sit under. | | `project` | The address of a GitHub or Linear project. | | `pull_request` | The address of a pull request. | | `repository` | The address of a repository. | `from` limits a ticket, epic or project to `github` or `linear`; it accepts both by default. Orbital looks a ticket or epic up when the run is created, and refuses the run before any agent turn when it cannot read the ticket, naming the input and the tracker's reason. Prompts then read its fields, such as `{{ inputs.ticket.title }}`, and `{{ inputs.ticket }}` on its own renders the reference you gave. A project, pull request or repository is checked to be written as an address, and a prompt reads that address. [Input kinds](/reference/workflow-file/#input-kinds) lists every field, and [Pass context between steps](/workflows/pass-context/) covers what happens to the values once the run starts. ## Validate before you run Validation reads the file, checks every key and its type, resolves imports and prompt files, and checks routing without calling an agent. ```sh orbital validate .orbital/hello.toml ``` Fix errors before you add more steps. Warnings identify unreachable steps, unused inputs and other suspicious but loadable declarations. Add one decision at a time. A choice output with two explicit routes is easier to inspect than a prompt that silently decides whether to publish. Validate again after you edit imports and prompt files, because those are separate files that the workflow only names. ## Choosing where to keep a workflow | You want | Keep it | | --- | --- | | The workflow beside the code it works on, reviewed in Git | The `.orbital/` folder of the project's folder | | The workflow in every project | Your library, `~/.orbital/workflows/` | | To build it in the app, without files | A Custom workflow, from the Workflows page | | A changed shipped workflow | The shipped workflow itself, edited in place. Reset to shipped to get the original back. | ## Related - [Write your first workflow](/workflows/write-a-workflow/): copy the smallest complete workflow and change it. - [Step kinds](/reference/step-kinds/): the kinds of step a workflow is made of. - [Workflow file](/reference/workflow-file/): every key a workflow file takes. - [How Orbital works](/concepts/how-orbital-works/): how a run carries out a workflow one step at a time. - [Shipped workflows](/reference/shipped-workflows/): each shipped workflow with its diagram. - [The orbital command](/command-line/): `orbital validate` and the other commands. --- # Durability after restarts > Every run carries on where it was when Orbital starts again You do not need to do anything when Orbital restarts. Every [run](/runs/) you did not pause carries on where it was, and nothing that finished runs again. Orbital restarts when you quit the app, when it updates, or when the computer restarts. ## What Orbital keeps An activity is one unit of a run's work: an agent turn, a command, a check, a wait or a worktree step. Orbital saves each run's progress as the run goes, in `~/.orbital/run-history.db`. It keeps: - the result of every finished activity; - the time each wait wakes; - every control you sent; - whether you paused the run. ## What happens at the next start 1. Orbital stops any agent programs that were still running from before the restart. 2. It carries on every unfinished run you did not pause, from its last finished activity. 3. Runs that were working take their slots back. Queued runs keep their place in the [queue](/runs/queue/). 4. A run that reached a new step while Orbital was shutting down starts that step now. Each kind of run comes back as follows: | Run | After the restart | | --- | --- | | At a timed wait | The wait wakes at the time it first set. | | Paused | It stays paused. | | At a gate | It stays at the gate. | | Aborting | An abort that was under way ends as aborted. | | With a half-applied control | The control is finished when you send it again. | ## A step that was under way A step that was under way when Orbital restarted had no result yet, so Orbital picks it up again. The run's thread shows both attempts. | Activity | What happens | | --- | --- | | An agent turn | It continues its session. If the harness cannot continue that session, the step starts again in the same worktree with the same prompt. The harness sets two limits: the message the agent was writing may be lost, and a tool call that was running may run again. | | A command marked `repeat_safety="idempotent"` or `"reconcilable"` | It runs again. | | A command left with the default, `uncertain` | It waits for you instead of running again, because running it twice could do harm. The run is Paused and shows "Waiting on you". | ![A run held back by a restart, with a Paused badge in its header.](/screenshots/run-interrupted.webp?v=700d8cf233) *A run held back after a restart waits for you to act.* For a command that did not run again, check what it did. Then choose **Retry step** or **Resume**. ## Pause and resume **Pause** marks the run as paused, stops its agent and frees its slot. The run stays paused across restarts. **Resume** carries the same step on: an agent turn continues the same session in the same worktree. **Retry step** starts the step again instead. See [Pause, stop or resolve a run](/runs/control-a-run/). ## Chats A [chat](/chats/) that was answering when Orbital stopped goes back to waiting, with a note that the answer failed. Send a message to carry on. ## Keep a copy Your data survives upgrades and uninstalling. :::caution Keep a copy of `~/.orbital/run-history.db` before you go back to an older release. An older release cannot open run history that a newer one has updated. ::: ## Related - [Pause, stop or resolve a run](/runs/control-a-run/): the controls Orbital records. - [The run queue](/runs/queue/): how slots come back after a restart. - [Error handling](/reference/error-handling/): what a run does when a step fails. - [Troubleshooting](/troubleshooting/): fixes for runs that stop or stall. --- # Projects and folders > A project is a name and the folders its runs work in You add a project to tell Orbital where your runs work. A project is a name and the folders a run works in. A folder can be a Git repository, or a plain folder that is not one, such as research reports. A project can hold several folders, such as a web app and its API. Every run belongs to one project. The **Projects** page lists your projects in a table, with each project's folders and runs. Its row menu has **Rename** and **Delete**. [Create a project](/projects/create-a-project/) shows how to make one. ![The Projects page shows a table of projects with their folders and runs, and a New project button.](/screenshots/projects-page.webp?v=8b88b77800) *Each row lists a project's folders and runs.* ## Folders The project's page lists its folders first, with their name, path, type and role. | Column | Values | | --- | --- | | **Type** | "Repository", with its base branch, or "Plain folder" for a folder that is not a Git repository. | | **Role** | "Primary" or "Secondary". | The first folder you add is the **primary folder**. Each agent step starts in it: it is the step's working directory. The other folders are open to the agent as additional folders it can read and change. A folder's menu has **Make primary**, **Rename**, **Change path** and **Remove**. When you remove the primary folder, another folder takes its place. Runs already under way keep the folders they started with. If you move or rename a folder on disk, use **Change path** to point the project at where it is now. Runs already under way follow it: each step reads the folder from the project when it starts, and their worktrees are repaired to find the repository in its new place. A step that needs a folder that no longer exists waits for you, so you can update the folder's path and resume the run. A workflow that makes a worktree makes one for each repository folder, all on the same branch name. Plain folders are used where they are. [Worktrees and delivery](/projects/worktrees-and-delivery/) explains this. ![A project page shows a table of folders with name, path, type and role, then the Runs limit and each folder's files to copy.](/screenshots/project-details.webp?v=a56bdefccc) *The folders come first, then the project's settings.* ## Project settings The project's settings sit below the folders, in groups. | Group | What it holds | | --- | --- | | **Runs** | The run limit and the project's own delivery lead. | | One group per folder, headed by its name | For a repository, the files to copy into new worktrees. | The settings save as you change them. The page header says "All changes saved" once they have. An info button beside a label explains the setting. ## Plain folders and Git A project does not need a Git repository. Plain folders work with Claude Code and OpenCode. Codex is the exception: it refuses to start in a folder that is not a Git repository. So a step on Codex needs its primary folder to be one. A workflow that changes code also needs a Git repository, because it works in a worktree. A worktree is a copy of a repository on a branch of its own. The shipped workflows that deliver a pull request need one for that reason, and so do the examples that make a worktree. A workflow that only reads or writes files runs in a plain folder. Examples are `explain-repo` and the research example. ## Runs at a time A project can limit how many of its runs work at once, with **Run limit** under **Runs** on its page. When a project reaches its limit, its next runs wait, and other projects' runs go first. [Create a project](/projects/create-a-project/#limit-runs-at-a-time) shows how to set it. ## Custom actions Custom actions are commands and links you add to a project for its runs. They appear in each run's **Run actions** menu. A command runs on the machine that runs Orbital, in the run's folder you choose. A link opens a URL template. [Create a project](/projects/create-a-project/#add-custom-actions) shows how to add one. ## Choose a project The project selector at the top of the sidebar chooses "All projects" or one project. The runs list, the Workflows page and the new-run composer follow it. Search with "Search projects…". ## Workflows a project offers A project offers these workflows: - the workflows in the `.orbital/` folder of each of its folders - your library in `~/.orbital/workflows/` - the Custom workflows you saved in Orbital - the shipped workflows [Workflows](/workflows/#where-orbital-looks-for-a-workflow) explains which one wins when two share a name. ## More on projects With the Triggers feature on, a project's page also holds its [trigger rules](/triggers/). With the delivery lead feature on, a project can have [its own delivery lead](/delivery-lead/#a-delivery-lead-for-one-project). ## Quick reference | You want to | Use | | --- | --- | | Choose where agent steps start | **Make primary** in a folder's menu | | Copy files into new worktrees of a repository | The folder's files to copy | | Limit how many of the project's runs work at once | **Run limit** under **Runs** | | Add a command or link to every run | [Custom actions](/projects/create-a-project/#add-custom-actions) | | Run on Codex, or deliver code | A Git repository as the primary folder | ## Related - [Create a project](/projects/create-a-project/): add a project, its folders, a run limit and custom actions. - [Worktrees and delivery](/projects/worktrees-and-delivery/): how code workflows use worktrees and open pull requests. - [The run queue](/runs/queue/): how project limits work with the limit in Settings. --- # Harnesses and models > A harness is the coding agent that does each agent step You choose which coding agent does each agent step of a run. That agent is the step's harness: Claude Code, Codex, OpenCode or [Pi](https://pi.dev/). Claude Code, Codex and OpenCode use your installed programs. Pi is bundled with Orbital. Configure credentials for the harnesses you use; each step runs on your own plan. Orbital starts the harness, gives it the step's prompt and settings, and records what it does. A run can move from one harness to another. ## Which harness a step uses A step's harness, model and effort come from its styles and its own keys, in the order [styles cascade](/styles/#how-styles-cascade): the step's own keys, then its styles, then the styles of its groups, then a model picked when the run started, then the workflow's top-level `style`. Each is decided on its own, so a step that sets only its effort keeps the harness and model it would have had. When nothing on the way names a harness, the step takes it from the run's preset: the preset picked on the Start page or passed to `start_run`, which pins the run to it, or else the preset in use in **Settings > Presets**. A run that follows the preset in use moves with it at its next turn when you switch it. When the run's preset names no harness either, the step uses **Default harness** in **Settings > General**. On a fresh install the shipped `claude` preset is in use. A harness named when the run starts, with `orbital run start --harness`, `start_run` or the API, works like [switching the run's harness](/harnesses/switch-harness/). It removes `harness`, `model`, `effort`, `connection` and `provider` from every layer, so every step runs on that harness. A model named with it applies to every step, except one whose own keys, styles or groups' styles set a model for that harness in its sub-table, such as `claude.model`. A model that belongs to another harness is refused before the turn starts. The step stops with a reason such as "Model `gpt-6-astra` runs on codex, not on claude, so the turn was not started." Change the model in the step's style or move the step to that harness, then retry. After a restart, Orbital works out each step's harness afresh. The run's header and the composer footer name the harness and model of the same step: the step running now, or the latest one. A run that used more than one pairing says so, for example "Codex · gpt-6-astra, +1 other". [Styles and personas](/styles/) and [Use different models in one workflow](/harnesses/use-different-models/) explain styles. [Workflow file](/reference/workflow-file/#how-a-steps-settings-resolve) gives the full order. ## See a harness's status Open **Settings > Harnesses**. The status table shows, for each harness, whether it is installed, whether it is signed in, and when Orbital last checked. Choose **Check again** after you install or sign in. Orbital also checks every five minutes. ![Settings, Harnesses page shows a status table for Claude, Codex and OpenCode, then Claude's settings: turned on, program, default model, default effort and environment variables.](/screenshots/settings-harnesses.webp?v=6446e71ff0) *The status table comes first, then each harness's settings.* | Status | Meaning | | --- | --- | | Installed, *version* | Orbital found the program and it answered. | | Not installed | Orbital found the program neither at the path set in Settings nor on the `PATH` of your login shell. The message gives the command to install it. | | Too old, *version* | The program is older than the oldest version Orbital works with. The message names that version and the command to update it. | | Could not start | The program is there, but it failed to start. | | Off | You turned the harness off. | | Signed in, Signed out, Sign-in not reported | What the harness says about its login. | Pickers add the reason after a harness's name, such as "(turned off)" or "(not installed)". ## Harness settings Each harness has its own group in **Settings > Harnesses**: **Turned on**, **Default model**, **Default effort** and **Environment variables**. Claude Code, Codex and OpenCode also have a **Program** path. See [Settings](/reference/settings/#harnesses). ## Use more than one Claude account Claude Code keeps each login in a config folder, `~/.claude` unless `CLAUDE_CONFIG_DIR` names another. Give each account its own folder and sign in to it once: ```sh CLAUDE_CONFIG_DIR="$HOME/.claude-work" claude ``` Run `/login` in that session, then `/status` to check which account it uses. Spell the folder the same way every time, without a slash at its end: Claude Code keys its stored login by the folder, so another spelling asks you to sign in again. Then add each account under **Settings > Harnesses > Claude > Accounts, in order of priority**. The first account is the primary one, and the rest are fallbacks, tried top to bottom. Use the arrows to change the order. Each account shows whether it is signed in and, when it has run out, until when it is out of quota. A new session starts on the first account with quota left, and every later turn in that session stays on the same account. When that account reaches a usage, rate or weekly limit, the step carries on in a new session on the next account down, with the run's context in its prompt as for any fresh step. Orbital never copies a session from one account's folder to another. The step's thread says which account ran out and which one took over, and the run page names the account beside the harness, as in "Claude (Work) · claude-opus-5-5". Only when every account is out of quota does the step stop as [out of quota](#when-a-harness-runs-out-of-quota), naming the soonest reset. ## Differences between harnesses **Claude Code** Claude Code works in plain folders as well as Git repositories. Claude has usage, rate and weekly limits. When it reaches one, the step moves to your [next Claude account](#use-more-than-one-claude-account), or stops as [out of quota](#when-a-harness-runs-out-of-quota) when there is none left. **Codex** Codex refuses to start in a folder that is not a Git repository. So a step on Codex needs its primary folder to be one. See [plain folders and Git](/projects/#plain-folders-and-git). To keep a Codex step from writing to the repository, set `codex.sandbox_mode = "read-only"` in its style. See [permissions](/styles/#permissions). **OpenCode** OpenCode works in plain folders as well as Git repositories. To use OpenRouter models through OpenCode, export `OPENROUTER_API_KEY` in the shell that starts Orbital, then restart it. **Pi** Orbital bundles the Pi SDK. Configure a provider in Pi, or set its API key in **Settings > Harnesses > Pi > Environment variables**. Orbital reports Pi's sign-in status as unknown because credentials belong to individual providers. Pi streams its responses, tool activity and usage into Orbital, and follows the step’s permission settings. Orbital connects Pi to its MCP tools for handoffs and run controls. Operator extensions are disabled for these turns. Pi still reads its settings, credentials, skills and project instructions. Use `provider/model-id` for an exact model selection. A style's `pi.model` and `pi.effort` apply only when Pi runs the step. Attachments reach Pi by their file paths, which its read tool can open. ## A missing or switched-off harness A step whose harness is turned off, not installed, or not set up does not fail. The run stops on that step with the reason on the step. It is [Paused](/runs/#standings), and its badge reads "Waiting on you". To carry on: 1. **Fix the cause.** Install the harness, sign in, or turn it back on under **Turned on** in **Settings > Harnesses**. 2. **Check again.** Choose **Check again** in **Settings > Harnesses**. 3. **Resume the run.** Open the run and choose **Resume**. Or [move the run to a harness that works](/harnesses/switch-harness/). A harness that is off takes no new turns anywhere. Runs that need it stop, and its chats take no new turns. ## When a harness runs out of quota Claude and other harnesses have usage, rate and weekly limits. When a harness reaches one, the step stops with an "out of quota" reason. The reason names the harness and, when it is known, when the quota resets. The run is Paused, with the badge "Waiting on you". You can then [switch the run's harness](/harnesses/switch-harness/) yourself. Or let the delivery lead do it with a [quota fallback](/harnesses/switch-harness/#quota-fallbacks). ## Switch a run's harness You can move a stopped run to another harness and retry its current step there. Finished steps, context and file changes stay. [Switch a run's harness](/harnesses/switch-harness/) gives the steps and what carries over. ## A step that goes silent A turn that writes nothing for the **Stalled turn limit** is stopped and started again in the same session. The limit is five minutes unless you change it in **Settings > General**. After two such restarts in one step, the run is Paused, with the badge "Waiting on you". ## Quick reference | | Claude Code | Codex | OpenCode | | --- | --- | --- | --- | | Install | `npm install -g @anthropic-ai/claude-code@latest` | `npm install -g @openai/codex@latest` | `npm install -g opencode-ai@latest` | | Sign in | `claude` | `codex login` | `opencode auth login` | | Oldest version Orbital works with | 2.1.280 | 0.156.0 | 1.18.0 | | Plain folders | Yes | No, the primary folder must be a Git repository | Yes | ## Related - [Switch a run's harness](/harnesses/switch-harness/): retry a step on another harness, by hand or by quota fallback. - [Use different models in one workflow](/harnesses/use-different-models/): give steps their own harness, model and effort. - [Install Orbital](/get-started/install/): install and sign in to a harness. - [Settings reference](/reference/settings/#harnesses): every harness setting. --- # Styles and personas > Named sets of turn settings that cascade from the workflow to each step A style is a named set of settings for an agent's turn: which harness runs it, which model it uses, how hard it thinks, which persona it speaks as and how the harness approves its actions. A persona is a named prompt that the agent receives with the step's own prompt. A style picks a persona with its `persona` key. Styles cascade like CSS. The workflow sets a base style for every step, a group adds its own for the steps inside it, a step names its own, and a key set on the step itself beats them all. Each setting is decided on its own, so a step that changes only its effort keeps the model it would have had. ```toml [styles.careful_review] extends = "reviewer" effort = "max" codex.sandbox_mode = "read-only" ``` The same name means the same thing on every step that uses it. Change the style called `reviewer`, and every step that uses it changes. ## What a style holds | Key | What it sets | | --- | --- | | `extends` | One style this one starts from. | | `harness` | The harness that runs the turn: `claude`, `codex`, `opencode` or `pi`. | | `model` | The model the harness uses. | | `effort` | How hard the model thinks: `minimal`, `low`, `medium`, `high`, `xhigh`, `max` or `ultra`. Each harness accepts only some of them. | | `connection` | The connection the harness routes the turn through. | | `provider` | The model provider. OpenCode only. | | `persona` | A persona name from **Settings > Personas**. `""` means no persona. | | `permissions` | How the harness approves the agent's actions: `auto-accept`, the default, or `full`. See [permissions](#permissions). | A style can also hold a sub-table for each harness: `claude.model`, `claude.effort` and `claude.setting_sources`; `codex.model`, `codex.effort`, `codex.sandbox_mode` and `codex.approval_policy`; `opencode.model`, `opencode.connection` and `opencode.provider`; `pi.model` and `pi.effort`. A plugin harness reads a sub-table of its own name. A sub-table applies only when that harness runs the step, and there it beats the plain key it sits beside. So a style can say `effort = "high"` for every harness and `codex.effort = "medium"` for Codex alone. An agent step takes the same keys, except `extends`, directly. [Style keys](/reference/workflow-file/#style-keys) and [harness-specific keys](/reference/workflow-file/#harness-specific-keys) give every value. ## How styles cascade Orbital decides each setting of a step from the first of these that sets it: 1. the step's own keys 2. the step's `style` 3. the styles of the groups that hold the step, the innermost group first 4. the model picked when the run started, if one was 5. the workflow's top-level `style` 6. the run's preset: the preset the run was started with, or else the preset in use 7. the defaults in **Settings > Harnesses** ```toml style = "balanced" [steps.coder] style = ["implementer", "codex"] [steps.coder.check] effort = "low" prompt = "Run the project's checks and fix what they find." ``` Here `coder.check` takes its effort from its own key, `low`. Its persona comes from `implementer` and its harness and model from `codex`, both through the `coder` group. The workflow's `balanced` style sets only effort, which the step already has, so it decides nothing here. An import step counts as a group for the steps it brings in, so its `style` reaches every imported step that does not choose for itself. [How a step's settings resolve](/reference/workflow-file/#how-a-steps-settings-resolve) gives the exact order. ## Lists of styles `style` takes one name or a list. In a list, later names win, key by key: ```toml [steps.review] style = ["careful_review", "thorough"] ``` `thorough` sets `effort = "high"`, so the step thinks at `high` even though `careful_review` says `max`. It still takes the persona and the Codex sandbox from `careful_review`, because `thorough` leaves them alone. ## Extend a style `extends` names one style to start from. The new style keeps every key of the one it extends and replaces the keys it sets itself. `careful_review` above is the shipped `reviewer` with more effort and a read-only sandbox on Codex. A style may extend a style from the app, from a style file or from the same workflow. ## Style files A style file is a TOML file with `version = 3` and only `[styles.*]` tables. A workflow pulls style files in with `use`, a list of paths relative to the workflow file: ```toml version = 3 entry = "steps.plan" use = ["shared/team.toml"] ``` Several workflows can `use` the same file, so a team keeps its styles in one place. Orbital builds the styles a workflow can name from four sources. A later source wins, key by key, when two define the same name: 1. the style files in `use`, in list order 2. the styles of workflows this file imports as steps 3. the workflow's own `[styles]` 4. the presets in **Settings > Presets** [Style files](/reference/workflow-file/#style-files) and [how styles merge](/reference/workflow-file/#how-styles-merge) give the details. ## Presets: styles saved in Orbital **Settings > Presets** holds presets: styles saved in Orbital, which every workflow can name without defining them. A step names a preset with `style`, just as it names a style its workflow defines. A preset wins over a file style of the same name on every key the app sets. So you can change the shipped `reviewer` in Settings and every workflow that names it follows. To change a shipped preset for one workflow only, extend it under a new name in that workflow, as `careful_review` does, rather than redefining `reviewer` in the file. Your Settings would win over the file's copy. ## The run's preset Every run also has a preset of its own, below everything the workflow sets. **Settings > Presets** marks one preset as in use. A run started without a preset follows the preset in use, and when you switch the preset in use, those runs move to it at their next turn. Pick a preset on the Start page, or pass `style` to `start_run`, to pin a run to that style instead: a later switch does not move it. The run's preset fills in what the workflow leaves open. On a fresh install the shipped `claude` preset is in use, so every step that names no harness runs on Claude Code. Switch the preset in use to `codex`, and those steps move to Codex at their next turn. A step whose own styles name a harness stays where it is. When the harness of the preset in use runs out of quota, a banner on **Settings > Presets** offers to switch the preset in use. Orbital never switches it by itself. ## Change, reset and restore styles Orbital adds the shipped presets on every start and never overwrites one you edited. **Reset to default** returns an edited shipped preset to Orbital's version. Deleting a shipped preset hides it until you choose **Restore**. [Settings](/reference/settings/#presets) describes the page. ## Personas A persona says who the agent is. It is a named prompt, such as "You review work you did not write", that the agent receives along with the step's own prompt. Personas live in **Settings > Personas**. A style or a step names one with `persona`, and `persona = ""` gives a step no persona even when a style below it names one. ## What ships Orbital ships these presets. Apart from `claude` and `codex`, none names a harness or a model, so each runs on whichever harness the run's preset gives it. | 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"` | It also ships these personas: | Persona | Who the agent is | | --- | --- | | `planner` | Plans the change before code is touched. | | `implementer` | Makes the planned change and checks it. | | `strict_reviewer` | Reviews others' work and reports defects. | | `researcher` | Finds things out and cites its sources. | | `delivery_lead` | Looks after your runs. [The delivery lead](/delivery-lead/) speaks as it, on the preset in use. | [Shipped presets](/reference/workflow-file/#shipped-presets) gives each persona's prompt. ## Permissions `permissions` decides whether the harness asks before the agent acts. It does not take tools away: a step can use every tool its harness offers. The default is `auto-accept`. When a harness asks you something, the run shows as Question until you answer. See [standings](/runs/#standings). To set permissions for a whole workflow, put them in a style and name it in the workflow's top-level `style`. :::caution[A prompt is not a limit] A prompt that says "do not change files" is a request, and the agent can ignore it. On Codex, `codex.sandbox_mode = "read-only"` in a style keeps the step from writing. ::: ### Claude Code | Setting | What happens | | --- | --- | | `auto-accept` | Claude Code's auto mode approves what it judges safe. Whatever auto mode does not settle, the run asks you as a Question. | | `full` | Claude Code bypasses its permission checks. Nothing is asked. | ### Codex | Setting | What happens | | --- | --- | | `auto-accept` | Codex defaults to `workspace-write` and `on-request`: it can change files in the workspace and run sandboxed commands, and decides when to ask for approval to act outside the sandbox. Each approval request appears as a Question for you to approve or decline. Set `codex.sandbox_mode` and `codex.approval_policy` to override these defaults. | | `full` | Codex runs with full access and never asks, whatever `codex.sandbox_mode` and `codex.approval_policy` say. | [Harness-specific keys](/reference/workflow-file/#harness-specific-keys) lists the sandbox modes and approval policies. ### OpenCode | Setting | What happens | | --- | --- | | `auto-accept` | Every permission OpenCode raises is asked as a Question. Your OpenCode permission settings decide what it raises. | | `full` | Every permission is approved. Nothing is asked. | ### Shipped workflows Of the shipped workflows, `pursue-goal` and `port-project` use `full`. The others use `auto-accept`. ## Quick reference | To decide | Where to set it | | --- | --- | | Settings for every step | `style` at the top level | | Settings for every step in a group | `style` on the group | | Settings for one step | `style` on the step, or the key itself on the step | | Who the agent is | `persona` | | Harness, model and effort | `harness`, `model`, `effort` | | A setting for one harness only | `claude.*`, `codex.*`, `opencode.*` or `pi.*` | | Whether the harness asks before it acts | `permissions` | | Styles shared between workflows | `use` with a style file | | Settings for every run that picks no preset | The preset in use, in **Settings > Presets** | ## Related - [Give steps a style](/styles/configure-steps/): a worked example. - [Use different models in one workflow](/harnesses/use-different-models/): styles on several harnesses. - [Workflow file](/reference/workflow-file/#styles): every style key and the exact resolution order. - [Settings](/reference/settings/#presets): the Presets and Personas pages. --- # Start a run > Give a project a goal, pick a workflow and start a run You start a [run](/runs/) from the Start page: you pick a project, write a goal and choose a workflow. The run then carries out that one workflow for that one project, towards that goal. ## Before you start You need a [project](/projects/create-a-project/) and a [harness](/harnesses/) that is set up. A run whose harness is missing stops and waits on you. ## Start a run from the app 1. **Open the Start page.** Choose **New task** in the sidebar. 2. **Check the project.** The project shows at the top. Choose it to pick another, or press ⌘K. 3. **Leave the switch on Workflow.** The switch sits under the text box. Set to **Chat**, it starts a [chat](/chats/) instead. 4. **Write the goal.** In the text box that reads "Describe what the run should achieve", type what the run should achieve. 5. **Choose a workflow and fill in its inputs.** The workflow dropdown lists every workflow the project offers, with its description, and starts on the project's default or the one it ran last. If the workflow has inputs, fill them in. For example, `implement-ticket` asks for a "Ticket URL". 6. **Pick a preset, if you want.** The preset picker starts on the preset in use, which follows your settings: if you switch the preset in use later, the run moves with it. Choose another style to pin the run to it, so every step uses that style's harness, model and effort until the run ends. A setting the workflow already gives a step, through the step's own keys, its style, its group or the workflow's `style`, wins over the run's preset. 7. **Choose Start run.** ![The Start page asking what to do in a project, with the goal box, the preset picker, the workflow picker and the Chat and Workflow switch.](/screenshots/start-page.webp?v=7454bfb8bf) *The goal box, with the style and workflow pickers beneath it.* ## If you choose no workflow The run uses the workflow the project ran last. If the project has never run one, it uses the **Default workflow** in [Settings](/reference/settings/#general). ## What happens next The new run joins the [queue](/runs/queue/) and starts when a run slot is free. Follow it on the [run page](/runs/watch-a-run/). ## Other ways to start a run You can also start a run without opening the app. | From | How | | --- | --- | | A terminal | [`orbital run start`](/reference/command/#orbital-run-start) | | A coding agent | Over [MCP](/mcp/connect/) | | An event or a schedule | A [trigger](/triggers/) | ## Related - [Runs](/runs/): what a run is and the standings it moves through. - [Watch a run](/runs/watch-a-run/): follow the run you started. - [The run queue](/runs/queue/): when a queued run starts. - [Permissions](/styles/#permissions): what a run asks you before its agent acts, on each harness. - [Start your first run](/get-started/first-run/): a guided first run for new users. --- # Watch a run > Find a run, see what it is doing and read its changes You follow your [runs](/runs/) in three places in the app. The **Runs** page lists them all, the **Attention** page lists the ones that wait for you, and a run's own page shows it in full. ## Find a run on the runs page Choose **Runs** in the sidebar to list every run. Search with "Search runs…", and filter by **Status**, **Time**, **Repo** and **Workflow**. Turn on **Show archived** to include archived runs. **Columns** chooses what the list shows: Status, Harness, Repo, Title, Workflow, Created, Duration and PR. Duration is the wall-clock time since the run's first recorded event: it counts on while the run is live, waits and gates included, stops at the run's last recording once it pauses, and stays fixed once it ends. A run that has not yet started its first step shows a dash, even once it is paused or aborted. The sidebar also lists **Recent runs** and **Recent chats**. **View all** opens the runs page. ![The Runs page, a table of runs with status, harness, repository, title, workflow, created, duration and pull request columns, with a search box and filter chips above it.](/screenshots/runs-page.webp?v=cfabb3ed64) *Search and the filter chips sit above the table of runs.* ![The Runs page with the Status filter open, listing Created, Queued, Running, Question, Waiting, At a gate, Paused, Succeeded, Failed and Aborted.](/screenshots/runs-status-filter.webp?v=859f79421d) *Each filter chip opens a list of the values you can pick.* ## See the runs that need you The **Attention** page lists the runs in the selected project that wait for you: the runs with the standing Question or Paused. When nothing waits, the page says "Nothing needs attention". ![The Attention page, listing the runs in a project that are waiting for an answer or an action from you.](/screenshots/attention-page.webp?v=541ff6c55b) *Only the runs that wait for you are listed.* ## Read the run page Choose a run to open its page. The page has a header, six tabs and a dock. The header shows the run's title, workflow, branch and harness. The **Run actions** menu (⋯) holds the [run controls](/runs/control-a-run/). It also holds **Copy run ID**, **Download run JSON** and your project's [custom actions](/projects/create-a-project/#add-custom-actions). **Open in** opens the run's folder in another app, such as your editor. ![A run page on the Session tab, showing route, wait and command steps one after another and ending in Stopped with an error, with the run's facts, pull requests and steps in the dock.](/screenshots/run-thread.webp?v=f4f203d47e) *The Session tab tells the run's story step by step.* | Tab | What it shows | | --- | --- | | Session | The conversation: the goal, each step, the agent's messages and tool calls, questions and notices. | | Waterfall | A timeline of the steps. Choose a step to see its status, input and output. | | Context | The values steps have written, such as outputs and facts. | | Logs | The run's recorded events, with search. | | Workflow | The workflow as a diagram and as source, frozen as the run started it. **Open in editor** opens it. | | Cost | Two tables. **Tokens by step** gives each step's model, tokens in and out, run time and cost, with a total. **Tokens by model** adds them up for each model. | ![The Waterfall tab, a timeline of the run's steps drawn as bars with their durations.](/screenshots/run-steps.webp?v=fee718c962) *Longer bars are steps that took longer.* ## Use the dock The dock sits beside the tabs and has three tabs of its own. | Dock tab | What it shows | | --- | --- | | Run | The run's project, workflow, branch, harness, model, run ID and path; its **Pull requests**; its **Steps**; and the **Agent** of the current turn, with its settings and tools. | | Changes | The files the run changed. Choose **Uncommitted**, **Staged** or **Branch**. Show the diff side by side or with word wrap. | | Files | The files in the run's folder, with a viewer. Use **Find a file** to search. | Choose **Hide dock** or **Expand dock** to change its size. ![The dock's Changes tab, with a tree of changed files on one side and the diff of the selected file.](/screenshots/run-changes.webp?v=a2e436d9fe) *Pick a file in the tree to see its diff.* ## Related - [Runs](/runs/): standings, the phase field and where the result appears. - [Message a run](/runs/message-a-run/): steer the agent from the Session tab. - [Pause, stop or resolve a run](/runs/control-a-run/): the controls in the Run actions menu. - [Pass context between steps](/workflows/pass-context/): where the values on the Context tab come from. --- # Message a run > Steer a working agent or answer its question from the composer You can write to a [run](/runs/)'s agent while the run works, from the composer at the bottom of the run page. Your message either steers the step that is working or waits for the next turn. ## Send a message 1. **Open the run.** Choose the run on the **Runs** page or under **Recent runs**. The run page opens on the **Session** tab. ![A run page for a running run, with a Running badge and Quiet for 4h 49m beside the tabs, and the harness, model, workflow and branch in a footer at the bottom.](/screenshots/run-running.webp?v=216080729b) *A running run shows how long its agent has been quiet.* 2. **Write in the composer.** The composer sits at the bottom of the **Session** tab. 3. **Send the message.** While a step is working, your message steers it. The composer then says "Message the agent". When no step is working, the message waits for the next turn. The composer then says "Message the agent for its next turn". ## Pause and chat Turn on **Pause and chat** in [Settings > Experimental features](/reference/settings/#experimental-features), then choose **Default Run interaction**. In **Run actions > Run interaction**, select **Steer**, **Pause and chat**, or **Inherit** to clear an override. A change applies to the next new message without restarting Orbital. In Pause and chat, sending a message durably holds the graph before stopping its work. A single interrupted agent keeps its Session; other work opens a conversation Session in the Run's recorded environment. Replies, Context edits and validated handoffs remain paused until approved Resume. Conversation never completes a step by itself. You can send another message while the agent replies. Orbital records it immediately and steers the running Turn when the Harness supports it. Otherwise it keeps the message for the next conversational Turn in the same Session. Closing the page does not stop an accepted reply. Once Resume is approved, Orbital finishes the active Turn and carries any messages still waiting into the resumed Workflow. It does not open another conversational Turn ahead of that Resume. Messages sent after approval are refused and retained in the composer for retry. Changing mode or disabling the feature does not discard an existing conversation or resume the graph. Its messages and Resume approval remain available under the rules it accepted. ## Answer a question When the agent asks a question, the run's standing becomes Question and the line under the composer says "Answer replies to the question. Return or Send will message the agent." Choose one of the options, or write your own answer and choose **Answer**. ![An agent's question with several options to choose from and a box for a free-text answer.](/screenshots/agent-question.webp?v=92fd44eff4) *Pick an option, or write your own answer.* The **Attention** page lists every run with a question waiting. See [See the runs that need you](/runs/watch-a-run/#see-the-runs-that-need-you). ## Related - [Watch a run](/runs/watch-a-run/): find the run and read its Session tab. - [Pause, stop or resolve a run](/runs/control-a-run/): the controls that sit beside the composer. - [Gates and waits](/workflows/gates-and-waits/): steps where a run waits for you. - [Chats](/chats/): talk to an agent without a workflow. --- # Pause, stop or resolve a run > Pause, resume, retry, abort, archive or resolve a run You pause, stop or resolve a [run](/runs/) from its run page. **Pause** and **Resume** sit on the composer, and every other control sits in the **Run actions** menu (⋯) in the header. The app offers a control only when it applies. ![A run page with the Run actions menu open, listing the controls that apply to the run.](/screenshots/run-actions-menu.webp?v=5686eaf17c) *The Run actions menu lists only the controls that apply now.* ## Pause and resume a run Choose **Pause** to pause the step the run is on and hand it back to you. The run frees its [run slot](/runs/queue/) and shows "Paused" until you act. Choose **Resume** to carry the same step on from where it stopped, in the same agent session. To start the step again from the beginning instead, choose **Retry step**. A run can also stop by itself and show "Waiting on you". [Standings](/runs/#standings) lists when that happens. The same controls carry it on. ![A run paused at a step, with a Waiting on you badge in the header.](/screenshots/run-paused.webp?v=66bbab501c) *A run that stopped by itself shows Waiting on you.* ## Resume a paused conversation In [Pause and chat](/runs/message-a-run/#pause-and-chat), your **Resume** is approval. An external or MCP Resume asks exactly "the agent was asked to resume. should it resume?". Rejecting leaves the graph paused. Approval during a conversational Turn waits for that Turn to settle, including its final edits and handoff. A server restart preserves pending requests and approved continuation. Resume consumes a held handoff once through normal step completion. Without a handoff, an interrupted agent continues its Session. Commands rerun, waits start their full duration, and every interrupted Fan-out branch starts a fresh visit. Earlier external effects remain; Resume does not roll them back. ## Abort a run Aborting a run stops its step and ends the run. :::caution You cannot resume an aborted run. To try the same work again, choose **Restart as new run**. ::: 1. **Open the Run actions menu.** Choose ⋯ in the run page's header. 2. **Choose Abort run.** Orbital asks first. 3. **Confirm.** Choose **Abort run** in the dialog, or **Cancel** to keep the run going. ![The Abort confirmation dialog, saying the run will stop the step it is on and end, and cannot be resumed afterwards.](/screenshots/abort-run-dialog.webp?v=80364622a4) *The dialog warns that the run cannot be resumed.* ## Resolve a failed or aborted run You can mark a run that ended Failed or Aborted as resolved. Choose **Mark as resolved** in the **Run actions** menu, and the run leaves the cards at the top of **Recent runs**. ## All run controls | Control | What it does | | --- | --- | | Pause | Pauses the step the run is on and hands it back to you. The run frees its slot. | | Resume | Carries the same step on from where it stopped, in the same agent session. | | Retry step | Starts the step again from the beginning, as a new visit. | | Retry with another harness… | Starts the step again on another harness. See [Switch a run's harness](/harnesses/switch-harness/). | | Jump to step… | Moves the run to another step. | | Skip | Ends a wait now, or opens a [gate](/workflows/gates-and-waits/). | | Abort run | Stops the step and ends the run. You cannot resume it afterwards. Orbital asks first. | | Restart as new run | Runs the same frozen workflow, goal and inputs again, as a new run. | | Archive, Unarchive | Hides a finished run from the list, or brings it back. Archiving removes the run's worktrees. | | Delete permanently | Removes the run and its history. | | Move to front of queue | On the runs list, starts a queued run next. | | Mark as resolved | Takes a Failed or Aborted run off the cards at the top of **Recent runs**. | ## Controls survive a restart A control you send while Orbital restarts still applies. [Durability after restarts](/runs/durability/) explains how. ## Related - [Runs](/runs/): the standings a control moves a run between. - [Message a run](/runs/message-a-run/): steer the agent instead of stopping it. - [Switch a run's harness](/harnesses/switch-harness/): retry a step on another harness. - [The run queue](/runs/queue/): what happens to a slot when you pause or resume. - [Durability after restarts](/runs/durability/): how controls and paused runs survive a restart. --- # The run queue > Orbital runs a set number of runs at once and queues the rest You set how many [runs](/runs/) Orbital works on at once, two unless you change it. Each working run holds one run slot, and the other runs wait in the queue until a slot frees up. A project can also have [its own limit](#limit-one-projects-runs). ## Change the number of runs at work 1. **Open Settings.** Go to **Settings > General**. 2. **Set Concurrent runs.** Under **Concurrent runs**, choose − or +, or type a number from 1 to 16. ![The General settings page, with Appearance, Concurrent runs, Queue method, Default workflow, Default harness and other settings.](/screenshots/settings-general.webp?v=cb0897c671) *Concurrent runs sets how many runs work at once.* The page shows how many slots are in use now. A change applies at once. Lowering the number never stops a run that is already working. The queue waits longer instead. Each run can make many agent turns at once, for example in parallel steps. The number counts runs, not turns. ## Choose the queue method The queue method decides what a run does with its slot while it waits for you or for a pull request. 1. **Open Settings.** Go to **Settings > General**. 2. **Choose a Queue method.** Under **Queue method**, choose one: - **Interleave**, the default. While a run waits for you or for a pull request, another run can use its slot. - **Finish first**. A new run starts only when an earlier run has finished. The choice saves at once and applies at the next decision, without a restart. Switching to **Interleave** lets queued runs into the slots that runs at a gate or waiting on you were holding. Switching to **Finish first** stops nothing that is already working. Under **Finish first**, a queued run's page says "Waiting for a run to finish", with its place. A run at a gate or waiting on you says "Holding its slot until it finishes". ## Limit one project's runs Give a project its own limit, so a project with a long backlog does not take every slot. 1. **Open the project.** Open **Projects** and choose the project. 2. **Set the run limit.** Under **Runs**, set **Run limit** with **−** and **+**, or type a number and press Enter. It saves at once. ![A project page with its folders table and the Runs limit field.](/screenshots/project-details.webp?v=a56bdefccc) *The run limit applies to this project only.* Leave the field empty for no limit of its own. The number in **Settings > General** stays the upper limit across all projects, and the project page shows it, for example "Orbital allows 4 at a time overall." A project limit at or above that number changes nothing, and the page says so. A change applies at the next decision, without a restart. Lowering the limit never stops a run that is already working. A queued run held back by its project's limit shows "Queued · waiting for a free slot in *project*" instead of its place. ## What holds a slot A run takes a slot when it starts. It keeps the slot through every step: agent turns, commands, checks, parallel branches, timed waits and questions the agent asks you. Under **Interleave**, a run gives its slot back when: - it reaches a [human gate](/workflows/gates-and-waits/) (Waiting, "At a gate"); - it stops by itself and shows "Waiting on you"; - it ends. A delivery workflow waiting for its pull request to be reviewed sits at a human gate, so it holds no slot. Many runs can wait on review while others work. Under **Finish first**, a run keeps its slot until it ends, through gates and waits on you. Under either method, a paused run gives its slot back at once, whether you paused it or a control interrupted it, and the first queued run starts. Stopping a run, or resolving it, frees its slot under either method. ## Order in the queue When a slot frees, Orbital skips projects that are at their own limit. It gives the slot to the project that has waited longest since it last got one, so projects take turns. Within one project, runs start in the order they joined the queue. A queued run's badge shows its place, such as "Queued · 2nd". To start a queued run next, open **Runs**, open its row's menu (⋯) and choose **Move to front of queue**. A run that comes back from a gate joins the back of the queue and shows "Continuing when a slot is free". When you resume or retry a paused run, it queues again: under **Finish first** it joins the front, because that method finishes started work first; under **Interleave** it joins the back. A run you resume while it waits at a gate goes back to the gate and later queues like any run coming back from one. It never takes a slot beyond the number you set. ## After a restart The queue is stored with the run history. After a restart, the runs that were working take their slots back first, even beyond the number you set. Queued runs keep their order. A paused run holds no slot after a restart, and still rejoins the queue as started work when you resume it. :::tip Queue only what you can follow. A long queue behind a stuck run holds everything up. [Lead delivery](/delivery-lead/lead-delivery/#rules-that-keep-it-safe) recommends at most one run queued beyond those working. ::: ## Related - [Runs](/runs/): the standings a run moves through, including Queued. - [Pause, stop or resolve a run](/runs/control-a-run/): pause a run to free its slot. - [Projects and folders](/projects/): the project page where the run limit lives. - [Settings](/reference/settings/#general): every General setting, including Concurrent runs. - [Durability after restarts](/runs/durability/): what happens to the queue when Orbital restarts. --- # Write your first workflow > Run the smallest complete workflow: one agent step and one terminal Write a [workflow](/workflows/) with one agent step and one terminal, then run it. It is the smallest workflow that does anything. ```toml title="minimal.toml" version = 3 entry = "steps.hello" [steps.hello] prompt = "Reply with one sentence explaining the goal: {{ inputs.goal }}." next = "steps.done" [steps.done] kind = "terminal" ``` `version = 3` says which workflow format the file uses, and `entry` names the step the run starts at. `hello` is an agent step: a step with a `prompt` and no `kind`. Its `next` sends the run to `done`, a terminal, which ends the run successfully. A reference to a step is always its full path in quotes, such as `"steps.done"`. Every other guide builds on this with more steps and routes. The prompt asks only for a sentence, so the agent has no reason to change files. To keep a step from changing files, say so in its prompt. With Codex, a style can also give the step a read-only sandbox with `codex.sandbox_mode = "read-only"`. ![hello → done. One prompt followed by a successful terminal.](/minimal.svg?v=b911ee569f) ## Before you start You need a harness you have signed in to, and a project in Orbital. Any project works. It does not need to be a Git repository, unless you run the example with Codex, which needs one. See [Projects and folders](/projects/). ## 1. Save the workflow [Download the example](/minimal.zip) and unpack it into your workflow library, `~/.orbital/workflows/`. Keep every relative path. You can also [download minimal.toml](/examples/minimal/minimal.toml) on its own. ## 2. Validate it ```sh orbital validate ~/.orbital/workflows/minimal.toml ``` Validation calls no agent. ## 3. Start a run Choose **New task** and pick any project. Choose the `minimal` workflow. One project serves every example. ![The Start page asks what to do in the selected project, with a box for the goal, a preset picker, a workflow picker and a Chat or Workflow switch.](/screenshots/start-page.webp?v=7454bfb8bf) *Pick the workflow beside the goal box.* Set the goal to "Explain what a workflow is", and start the run. The `hello` step returns a sentence, then `done` ends the run successfully. ## Related - [Write prompts](/workflows/write-prompts/): read run values in a prompt. - [Add branches](/workflows/add-branches/): send a run to different steps. - [Step kinds](/reference/step-kinds/): the kinds of step you can add. - [Research report](/examples/research-report/): a workflow that touches no code. - [Workflows](/workflows/): where Orbital finds workflows and how to validate them. --- # Write prompts > Read run values in a prompt, and use conditions, loops and filters Write the text an agent step receives, and let Orbital fill in the run's values before each visit. One prompt can then say something different on a retry, after a review or in a different project. ```toml title="review.toml" [steps.review] prompt = "Review the change against this goal: {{ inputs.goal }}" ``` The template language is [Nunjucks](https://mozilla.github.io/nunjucks/templating.html). Every example below shows the prompt and the text it renders to. ## Inline prompts and prompt files A short prompt fits in the step's `prompt` key, such as `prompt = "Review the goal for risks."`. The value is a TOML string in double quotes, so write strings inside the template with single quotes, as every example here does. A prompt of several lines goes between triple quotes, `"""`, which keep its line breaks. A longer prompt belongs in its own file. `prompt_file = "prompts/review.md"` names a file relative to the workflow file, and a `prompts` folder beside the workflow is the usual place. A step takes one or the other, never both. Orbital reads a prompt file again before each later visit, so an edit reaches a running workflow. If the edited file is invalid, Orbital reports the problem and keeps the prompt it last checked. ## Read a value `{{ … }}` prints a value. The goal is `inputs.goal`, each declared input is `inputs.`, and anything the run has gathered is under `context`. [Template variables](/reference/template-variables/) lists every name. ```jinja title="prompt" Work towards this goal: {{ inputs.goal }} Extra notes: {{ inputs.notes }} ``` This run was started without the optional `notes` input, so that line ends empty: ```text title="renders" Work towards this goal: Add a dark mode toggle to the settings page Extra notes: ``` A value the run does not hold renders as nothing, never as an error or the word `undefined`. Validation catches most mistakes first. A prompt that reads a context value some earlier path never writes fails `orbital validate`, and so does a name that is not a template variable. ## Conditionals `{% if %}` includes text only when its test holds. `{% elif %}` tries another test and `{% else %}` covers the rest. Compare with `==` and `!=`, and join tests with `and`, `or` and `not`. An empty value counts as false. ```jinja title="prompt" {% if context.verdict == 'reject' -%} Fix every finding the reviewer listed. {% elif context.verdict == 'pass' -%} Tidy the change and write the summary. {% else -%} Review the change from the start. {% endif -%} ``` With `verdict` set to `reject`: ```text title="renders" Fix every finding the reviewer listed. ``` The dashes in `-%}` stop each tag from leaving a blank line behind. [Whitespace control](#whitespace-control) explains them. ## Loops `{% for %}` repeats its text for each item. Inside it, `loop.index` counts from 1, `loop.first` and `loop.last` mark the ends, and `loop.length` is the number of items. `{% else %}` inside a loop renders when there is nothing to loop over. ```jinja title="prompt" Check each folder: {% for name, folder in run.folders -%} {{ loop.index }}. {{ name }} at {{ folder.path }} {% else -%} This project has no folders. {% endfor -%} ``` In a project with the folders `web` and `api`: ```text title="renders" Check each folder: 1. web at /work/shop/web 2. api at /work/shop/api ``` A loop needs a list. The usual sources are the folders under `run.folders` or `project.folders`, and a list written in the prompt itself, such as `{% for area in ['tests', 'types'] %}`. A `json` output is stored as text, and a loop over it walks its characters. ## Filters A filter changes a value on its way into the prompt. Write it after a `|`, and chain as many as you need. | Filter | What it does | | --- | --- | | `default('x')` | Uses `x` when the value is missing. `default('x', true)` also replaces empty text. | | `trim` | Removes spaces and line breaks from both ends. | | `join(', ')` | Joins a list into one piece of text. | | `length` | Counts the items in a list, the folders in `run.folders`, or the characters in text. | | `upper` and `lower` | Change the case. | | `replace('a', 'b')` | Replaces every `a` with `b`. | ```jinja title="prompt" Branch: {{ inputs.title | trim | lower | replace(' ', '-') }} Priority: {{ inputs.priority | default('normal') | upper }} Last failure: {{ context.failure_reason | default('none', true) }} Folders: {{ run.folders | length }} Checks: {{ ['lint', 'tests', 'types'] | join(', ') }} ``` With `title` set to ` Fix Login Redirect `, no `priority` input, no failure yet and one folder: ```text title="renders" Branch: fix-login-redirect Priority: NORMAL Last failure: none Folders: 1 Checks: lint, tests, types ``` `failure_reason` is empty text after a success rather than missing, which is why that line needs `default('none', true)`. Nunjucks has more filters; its [list of built-in filters](https://mozilla.github.io/nunjucks/templating.html#builtin-filters) covers them all. ## Whitespace control Every line break in the prompt stays in the output, including the ones around `{% %}` tags. A condition that is false leaves an empty line where it stood: ```jinja title="prompt" Review the change. {% if context.failure_reason %} The last attempt failed: {{ context.failure_reason }} {% endif %} Report what you found. ``` ```text title="renders" Review the change. Report what you found. ``` A dash inside a tag removes the spaces and line breaks on that side. `{%-` trims before the tag and `-%}` trims after it. `{{-`, `-}}`, `{#-` and `-#}` work the same way. ```jinja title="prompt" Review the change. {%- if context.failure_reason %} The last attempt failed: {{ context.failure_reason }} {%- endif %} Report what you found. ``` ```text title="renders" Review the change. Report what you found. ``` When the step fails and comes back, the same prompt adds the failure on its own line between the other two. ## Comments `{# … #}` holds a note for whoever edits the prompt. The agent never sees it. ```jinja title="prompt" {# The reviewer reads this on a phone, so keep the prompt short. -#} Review the change in three sentences or fewer. ``` ```text title="renders" Review the change in three sentences or fewer. ``` ## Write a literal `{{` Text between `{% raw %}` and `{% endraw %}` renders exactly as written. Use it when the agent must see template syntax of its own, such as a placeholder in a file it edits. ```jinja title="prompt" Add a greeting to the email template. It must say: {% raw %}Hello {{ customer.name }}, your order has shipped.{% endraw %} ``` ```text title="renders" Add a greeting to the email template. It must say: Hello {{ customer.name }}, your order has shipped. ``` ## What a prompt cannot do A prompt cannot read files, so `include`, `import` and `extends` are unavailable, and it cannot run shell commands. Orbital checks everything a prompt reads before the run starts. A prompt that pulled in another file or a command's output would read things that check never saw. To give a prompt a file's contents or a command's result, add a [command step](/reference/step-kinds/#command) before it. Its facts land in context, where the prompt reads them. To share text between prompts, put it in one prompt file and point several steps at it. ## Patterns ### Show the failure only on a retry Orbital writes `context.failure_reason` after every step. It is empty on success and holds the reason on failure. A step reached again through a failure route can explain what went wrong, while its first visit reads as normal. ```jinja title="prompt" Implement the goal: {{ inputs.goal }} {%- if context.failure_reason %} Your last attempt failed: {{ context.failure_reason }} Fix that first. {%- endif %} ``` After the tests failed: ```text title="renders" Implement the goal: Fix the login redirect Your last attempt failed: npm test exited with code 1: 2 of 48 tests failed Fix that first. ``` On the first visit, the prompt ends after the goal. ### Use the goal and a declared input With an `[inputs.ticket_url]` table in the workflow, the new-run form asks for a ticket URL and every prompt can read it. ```jinja title="prompt" Goal: {{ inputs.goal }} Implement the ticket at {{ inputs.ticket_url }}. Keep the change to what the ticket asks for. ``` ```text title="renders" Goal: Ship the ticket as one pull request Implement the ticket at https://github.com/acme/shop/issues/42. Keep the change to what the ticket asks for. ``` `inputs.ticket_url` always renders what the run started with. `context.ticket_url` renders the current value, which a later step may have changed. An input declared with `kind = "ticket"` or `kind = "epic"` carries the ticket itself, not only its reference. Orbital looks it up when the run is created, and a prompt reads its fields, such as `{{ inputs.ticket.title }}` and `{{ inputs.ticket.body }}`. [Declared inputs](/workflows/#declared-inputs) lists the kinds. ### List each branch's result after a fan-in After a [fan-in](/workflows/run-steps-in-parallel/), each branch's result sits under `context.parallel.results`, in the order the branches appear in the file, with its `node`, `outcome`, `failure_reason` and `context`. Quote the index. Read each branch on its own line. Validation checks every value a prompt reads, and it cannot check a loop over the results. ```jinja title="prompt" Two reviewers read the change. Combine their findings into one list. {{ context.parallel.results['0'].node }}: {{ context.parallel.results['0'].context.assessment }} {{ context.parallel.results['1'].node }}: {{ context.parallel.results['1'].context.assessment }} ``` ```text title="renders" Two reviewers read the change. Combine their findings into one list. reviews.security: The coupon code is not escaped before the query. reviews.tests: Nothing covers an expired coupon. ``` ### Point at a folder `run.folders..path` is where the run works on that folder now, which is a worktree once the run has one. `project.folders..path` is the folder as the project holds it. The name is the folder's name in the project. ```jinja title="prompt" Work only inside {{ run.folders.web.path }}. Do not edit {{ project.folders.web.path }}. That folder belongs to the person who started the run. ``` ```text title="renders" Work only inside /home/me/.orbital/worktrees/r7Kq2/web. Do not edit /work/shop/web. That folder belongs to the person who started the run. ``` ### Reuse an earlier agent's answer `context.response.` is the final prose of the agent step with that ID, without the outputs it handed over. It is empty until that step completes, and it names agent steps only. A step inside a group or an import has a dotted ID, so its prose is at a path such as `context.response.reviews.clarity`. ```jinja title="prompt" The planner wrote this plan: {{ context.response.plan }} Implement it step by step. ``` ```text title="renders" The planner wrote this plan: 1. Read the return URL from the query string. 2. Redirect there after sign-in. Implement it step by step. ``` ## Quick reference | Write | To | | --- | --- | | `{{ context.verdict }}` | Print a value. | | `{% if context.verdict == 'reject' %}` … `{% endif %}` | Include text only when a test holds. | | `{% for name, folder in run.folders %}` … `{% endfor %}` | Repeat text for each item. | | `{{ inputs.title \| trim }}` | Change a value with a filter. | | `{%-` and `-%}` | Trim the line break before or after a tag. | | `{# note #}` | Leave a note the agent never sees. | | `{% raw %}` … `{% endraw %}` | Print template syntax as written. | ## Related - [Template variables](/reference/template-variables/): every name a prompt can read. - [Pass context between steps](/workflows/pass-context/): how steps write the values prompts read. - [Context](/reference/context/): what each context value holds and when it exists. - [History and threads](/reference/history-and-threads/): what a prompt already carries from earlier steps. --- # Add branches > Send a run to different steps with routes that test what it has learned Send a run to different steps depending on what it has just learned. Give the step a list of `routes`, each with a condition and a step to go to, and the run follows the first one whose condition holds. ```toml title="conditional.toml" [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" }, ] ``` ## How routes choose the next step Routing lives on the step that decides, not in a separate decision step. `routes` is an ordered list of tables. Each route has an `if`, the condition, and a `to`, the step to go to. `next` is the fallback the run takes when no route holds. A route without `if` is refused, because that is what `next` is for. When a step succeeds, Orbital tries its routes in the order you wrote them and takes the first whose condition holds. Only then does it take `next`. Put the most specific route first. A condition compares one context value with a literal, such as `verdict == 'pass'`, `verdict != 'pass'` or `attempts == 3`. It can also test a value on its own, such as `git.worktree.clean`, or its negation, `!git.worktree.clean`. The `context.` prefix is optional, and an input reads by its name, such as `mode == 'quick'`. You cannot join conditions with `and` or `or` or compare two context values, so every route stays readable in the file. [Conditions](/reference/workflow-file/#conditions) lists every form a condition takes. ![The workflow editor canvas shows arrows leaving an agent step, each labelled with its condition, such as verdict == 'reject', and a dashed arrow labelled else.](/screenshots/workflow-editor.webp?v=85a9c6275a) *Each route shows its condition; the dashed else arrow is the fallback.* ## Choose what to branch on Decide first what produces the value you branch on. The answer changes the cost and the reliability of the branch. - Use a [probe step](/reference/step-kinds/#probe) when Orbital already observes the fact. The [ticket workflow](/examples/workflows/ticket/) routes on `github.pr.state` without asking an agent whether a merge happened. - For another exact observation, use a [command step](/reference/step-kinds/#command) with declared `bool`, `number` or `text` facts, such as `facts.tests.pass = "bool"`. Its last line of output must be the JSON object exactly as declared. Keep the script read-only when it only inspects something. - For judgement, give an agent a `choice` output. Orbital refuses any answer outside its options before it reaches context. So `outputs.verdict = { kind = "choice", options = ["pass", "reject"] }` accepts nothing else. A probe or a script gives an exact answer at no agent cost. The example below branches on judgement, so an agent makes the choice. A choice can describe each option, which helps the agent pick the right one. Give `options` as a table of values and descriptions in the output's own table; the options keep the order you write them in: ```toml [steps.pick.outputs.next] kind = "choice" options.ticket = "A ticket is ready to ship" options.none = "Every ticket is shipped" options.blocked = "The remaining tickets wait on people" ``` A choice without `options` offers the values its routes test. To let a small decision model make the choice instead, with a default route when it cannot, see [Decide with a model](/workflows/decide-with-a-model/). ## Cover every case A branching step needs `next`, unless its routes answer every option of the choice output they test. A run where nothing matches stops with an error, so validation asks for `next` before the run starts. :::note `next` handles successful results outside your named cases. It never handles a failed step. A failed step takes the first route whose condition holds, usually one that tests `outcome == 'failed'`, and stops for you when none does. [Add a loop](/workflows/add-a-loop/) and [Error handling](/reference/error-handling/) explain. ::: ## Before you start You need a harness you have signed in to, and a project in Orbital. Any project works. It does not need to be a Git repository, unless you run the example with Codex, which needs one. See [Projects and folders](/projects/). ## 1. Download the example [Download the example](/conditional.zip) and unpack it into `~/.orbital/workflows/`. Keep every relative path. ![classify selects greet or explain; both reach done.](/conditional.svg?v=56bd802163) ```toml title="conditional.toml" version = 3 entry = "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` declares `route` as a choice of `greet` and `explain`, and Orbital refuses any other answer before it reaches context. Its routes are tried in order, and between them they answer both options, so this step needs no `next`. `greet` and `explain` each name `done` as their `next`. [Download conditional.toml](/examples/conditional/conditional.toml) ## 2. Validate it ```sh orbital validate ~/.orbital/workflows/conditional.toml ``` ## 3. Run it Choose **New task**, pick any project, and choose the `conditional` workflow and your harness. One project serves every example. Use a greeting as the goal to select `greet`, or any other goal to select `explain`. Exactly one response step runs before `done`. The run's **Context** tab shows each value the run holds and the step that set it. ![The run page's Context tab lists the values held by the run, each with the step that set it on the right.](/screenshots/run-context.webp?v=3e79bd8607) *The Context tab shows which step set each value.* :::caution A later step that reads a value produced on only one branch fails validation, unless it gives the value a default with `needs`. Check [Pass context between steps](/workflows/pass-context/#what-a-step-can-read) and [Error handling](/reference/error-handling/) before you join branches back together. ::: ## Related - [Routes](/reference/workflow-file/#routes) and [conditions](/reference/workflow-file/#conditions): every form a route and a condition take, and the fallback rules. - [Add a loop](/workflows/add-a-loop/): send rejected work back for repair, with a bound. - [Pass context between steps](/workflows/pass-context/): the values a condition can read. - [Error handling](/reference/error-handling/): what happens when a step fails. --- # Decide with a model > Route a run with a small decision model instead of an agent turn, and say what happens when it cannot answer Let a small, fast decision model choose the next step. A **decide step** asks it a structured question about the run and follows the route its answer selects. A **deciding fan-out** asks it which branches to start. Neither spends an agent turn. ```toml title="decide.toml" [steps.ready] kind = "decide" state = "Change: {{ diff }}" routes = [{ if = "verdict == 'ship'", to = "steps.done" }] default = "steps.fix" [steps.ready.questions.verdict] kind = "choice" options.ship = "The change is complete and correct." options.rework = "The change is incomplete or wrong." ``` ## The default path Every decide step and every deciding fan-out must name a **default path** in `default`: the one step, or the branches, that a run takes when no answer can be used. Validation refuses a step without one. The default path is taken when: - no decision model is configured, or the step's `decider` names one that is not; - the model cannot be reached, refuses the request or answers with something unusable; - any answer's confidence is below the step's `threshold`. No agent answers in its place, and no other model is tried. A run on a computer with no decision model set up still finishes; every decision takes its default path, and each one says so on the run page. ## Ask a question A decide step is a step with `kind = "decide"`. It declares: - `state`, or `state_file`: what the model is asked about. It is a template, like an agent prompt, and reads `{{ context.x }}` and `{{ inputs.x }}`. - One or more questions, each a `[steps..questions.]` table with a `kind` of `choice`, `score` or `noul` (yes or no), and an optional `description`. - For a `choice`, one `options.` per option. For a `score`, one `levels.` per level. - Optionally `decider`, to name the model, and `threshold`, from 0 to 1. The answers go in the run context as `` and `_confidence`. A route tests an answer with `if`, such as `verdict == 'ship'`, and a later step can read the confidence. A `noul` answer is `true` or `false`. A `score` answers with the value of its most probable level. The run tries the routes in the order you list them and follows the first whose condition holds. `default` takes the place of `next`. ## Put the diff in the question `{{ diff }}` is the run's change against its base. Only the `state` of a decide step or a deciding fan-out can read it. When the diff does not fit the model's context, Orbital cuts it and ends it with `[… diff truncated …]`. A model with a small context, such as Laya with 512 tokens, sees only the start of a large change, so give it a question that a small part of the diff can answer. ## Choose the model and the threshold A step's `decider` names a [decision model you set up in Settings](/settings/decision-models/). A step that names none asks the model marked as the default. The `threshold` is how sure the model must be of every answer. A step that sets none takes the threshold of its model, which starts at 0.5. The threshold applies to the whole step. If any answer falls below it, the step takes its default path, even if the other answers were sure. A yes or no takes its confidence from the probability of its side. A `score` takes it from its most probable level. ## Start only some branches A [fan-out](/reference/step-kinds/#fan-out) that declares `state` and a question decides which of its branches start. `choices.` lists the branches each answer starts, every branch is listed under at least one answer, and `default` lists the branches that start when no decision is made. - With one `choice` question, the option the model picks starts every branch listed under it. - With several `noul` questions, each answered yes starts every branch listed under its name. - When the model picks nothing that starts a branch, or its answer cannot be used, the `default` branches start. The fan-in joins only the branches that started. The fan-out records which branches started, and a run restarted at the fan-out starts the same ones. ## Read a decision on the run page Each decide step and deciding fan-out shows as a **Decision** card in the run's Session tab. It lists each question with the answer and its confidence, marks a confidence below the threshold, says why the run took the default path when it did, and names the step it went to or the branches it started and the model that was asked. The run's Logs tab records the same decision as a `DecisionMade` event. ## A restart does not ask again The decision is recorded with the step. A run that restarts, or replays its history, reads the recorded decision and takes the same route, even if the model would answer differently now. A step the run reaches again, in a loop or after a retry, is a new visit and asks again. ## In the shipped workflows Two shipped workflows decide with a model, and both default to the careful path: - Every workflow that delivers a pull request asks, in `pr-delivery`'s `triage_feedback`, whether new feedback is a code review or only automated or informational noise. Noise goes back to waiting and is recorded as handled, so the same comments are not judged again. The default path addresses the feedback. - `pursue-goal` asks, in `frontend_changes`, whether the change makes significant changes to the frontend. The default path runs `review_frontend` before `validate`. [Shipped workflows](/reference/shipped-workflows/) describes both. ## Before you start You need a project in Orbital and one decision model set up in Settings, or you can run the example without one and watch it take its default path. See [Set up decision models](/settings/decision-models/). ## 1. Download the example [Download the example](/decide-review.zip) and unpack it into `~/.orbital/workflows/`. Keep every relative path. ```toml title="decide-review.toml" version = 3 entry = "steps.implement" [steps.implement] prompt = "Make the change the goal asks for: {{ inputs.goal }}" next = "steps.ready" [steps.ready] kind = "decide" state = "Goal: {{ inputs.goal }}. Change: {{ diff }}" decider = "jev" threshold = 0.7 default = "steps.fix" routes = [ { if = "verdict == 'ship'", to = "steps.done" }, ] [steps.ready.questions.verdict] kind = "choice" description = "Does the change do what the goal asks, and nothing that breaks it?" options = { ship = "The change is complete and correct.", rework = "The change is incomplete or wrong." } [steps.fix] prompt = "The change is not ready. Compare it with the goal and fix what is missing: {{ inputs.goal }}" max_visits = 2 next = "steps.ready" [steps.done] kind = "terminal" ``` [Download decide-review.toml](/examples/decide-review/decide-review.toml) `ready` asks one `choice` question about the change. The `ship` answer goes to `done`. The default path goes to `fix`, which also runs when `jev` answers with less than 0.7 confidence, and then asks again. `max_visits` bounds that loop. ## 2. Validate it ```sh orbital validate ~/.orbital/workflows/decide-review.toml ``` ## 3. Run it Start a run of `decide-review` with a goal. When the run reaches `ready`, open the Session tab to read the decision card. ## Decide which branches start ```toml title="deciding-fan-out.toml" version = 3 entry = "steps.implement" [steps.implement] prompt = "Make the change the goal asks for: {{ inputs.goal }}" next = "steps.areas" [steps.areas] kind = "fan_out" state = "What the change touches: {{ diff }}" decider = "jev" join = "steps.join" branches = ["steps.security", "steps.docs", "steps.tests"] default = ["steps.tests"] choices.security = ["steps.security"] choices.docs = ["steps.docs"] choices.tests = ["steps.tests"] [steps.areas.questions.security] kind = "noul" description = "The change touches authentication or secrets." [steps.areas.questions.docs] kind = "noul" description = "The change touches documentation." [steps.areas.questions.tests] kind = "noul" description = "The change touches tests." [steps.security] prompt = "Review the change for security problems." outputs.assessment = "text" next = "steps.join" [steps.docs] prompt = "Review the documentation the change touches." outputs.assessment = "text" next = "steps.join" [steps.tests] prompt = "Review the tests the change touches." outputs.assessment = "text" next = "steps.join" [steps.join] kind = "fan_in" prompt = "Summarise the reviews that ran." next = "steps.done" [steps.done] kind = "terminal" ``` [Download deciding-fan-out.toml](/examples/deciding-fan-out/deciding-fan-out.toml) `areas` asks one yes or no question per area, and each area answered yes starts its review. With no usable answer it starts `tests` alone, because `default` lists only that branch. ## Related - [Decide](/reference/step-kinds/#decide) and [Fan-out](/reference/step-kinds/#fan-out): every key of the two kinds. - [Set up decision models](/settings/decision-models/): Jev, Clef, Laya and Kev. - [Workflow file](/reference/workflow-file/): every key a workflow and its steps take. - [Add branches](/workflows/add-branches/): route on a value an agent or a command produced. --- # Add a loop > Send work back to an earlier step, and stop the loop after a set number of rounds Send work back to an earlier step when a review rejects it. A loop is any route or `next` that points back at an earlier step, and nothing else marks it. ```toml title="bounded-retries.toml" [steps.review] 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" }, ] [steps.repair] next = "steps.review" ``` Writing a loop is easy. The work is in stopping it. ## How a loop stops Orbital gives you two ways to stop a loop, and two ways to make a long loop cheaper. ### Budgets A budget limits how many rounds one piece of work may take through a loop. It is the bound to reach for when an agent may need several corrections but must not correct forever. Declare it at the top of the file with its number of rounds, five by default: ```toml [budgets.answer] rounds = 5 ``` Then name it on the routes of the step that decides: | Key on a route | What it does | | --- | --- | | `spends = "budgets.answer"` | Taking the route uses one round. Once no rounds are left, Orbital skips the route and takes the next one whose condition holds. | | `resets = "budgets.answer"` | Taking the route fills the budget again. Put it on progress, such as a passing review or new work. | `next` written as a table can reset a budget too, as `next = { to = "steps.select", resets = "budgets.answer" }`, so work that moves on past every check starts with full rounds. It cannot spend one: only a route with a condition spends. Write the route for an empty budget straight after the route that spends it, with the same condition. That route is where the loop ends: a terminal, or a step that reassesses the work. `rounds` may read an input, as `rounds = "{{ inputs.max_rounds }}"`, so the person who starts the run sets the bound. A budget is shared by name across the whole workflow, imports included, and it survives a restart, a resume and a loop restart. [Budgets](/reference/workflow-file/#budgets) gives the full rules. In the editor, each route that loops back shows its condition. ![The workflow editor shows a pull-request workflow in which arrows labelled with conditions such as review_verdict == 'reject' lead from later steps back to earlier ones.](/screenshots/workflow-editor-loop.webp?v=4dacb0ba6a) *Routes that loop back carry a condition like any other route.* ### Visit caps `max_visits` on a step caps every visit to that step for the life of the run, whatever the reason. `max_visits` at the top of the workflow caps the run's agent turns, 200 by default. Probes, actions, commands and waits do not spend it. | Limit reached | Result | | --- | --- | | The workflow's limit | The run ends Failed. | | A step's own limit | That visit fails, and the run is Paused with "Waiting on you", whatever failure routes the step has. | Visit caps stay independent of budgets. Treat them as the outer guard, not as the way the loop is meant to stop. A budget counts the rounds of one piece of work, fills again on progress and runs out into a route you wrote. A visit cap counts every visit and stops the run. See [`max_visits`](/reference/workflow-file/#max_visits). ### Waits A poll loop watches something outside the run, such as a pull request, a build or a queue. Put a `wait` step in it. Waiting spends no agent visits, and you can choose **Skip** to check again now. [Add gates and waits](/workflows/gates-and-waits/) covers the wait step itself. ![The run page's Waterfall tab shows a poll loop: probe, route and wait steps repeat as probe@2, route@2, wait@2 and so on, each with its duration.](/screenshots/run-steps.webp?v=fee718c962) *Each pass of a poll loop adds a new visit to the probe, route and wait steps.* ### Loop restart `loop_restart = true` on the route back clears the values the loop wrote to context, the history of finished steps that later prompts show, and every thread's session. It keeps the original inputs, the run's identity, the visit counts and the budgets. A route takes it as a key, `{ if = "verdict == 'pass'", to = "steps.select", loop_restart = true }`, and `next` takes it in a table, `next = { to = "steps.select", loop_restart = true }`. Use it when the next pass is a new piece of work, not another attempt at the same one. Without it, each prompt in the loop carries the history of every earlier pass. See [`loop_restart`](/reference/workflow-file/#loop_restart). ## Failure is not a loop A failed step takes the first route whose condition holds and never takes `next`. When the workflow knows how to recover, route `outcome == 'failed'` to a repair prompt that reads `{{ context.failure_reason }}`. Then return to the observation or review that proves the recovery worked. :::caution Read the failure reason before you decide to retry. An authentication failure needs credentials. Asking the same unauthenticated harness again cannot repair it. ::: Make exhaustion visible with a terminal or a step that reassesses the work. A failed terminal ends the run as Failed. A failed step with no route for the failure leaves the run Paused, with "Waiting on you". [Troubleshooting](/troubleshooting/) explains how to tell them apart in the app, and [Error handling](/reference/error-handling/) gives the exact rules. ## Before you start You need a harness you have signed in to, and a project in Orbital. Any project works. It does not need to be a Git repository, unless you run the example with Codex, which needs one. See [Projects and folders](/projects/). ## 1. Download the example This workflow reviews an answer and sends rejections to a repair step. It gives up after five unsuccessful rounds instead of looping forever. [Download the example](/bounded-retries.zip) and unpack it into `~/.orbital/workflows/`. Keep every relative path. ![review passes to done or rejects into repair; repair returns to review. Five unsuccessful repair rounds lead to exhausted.](/bounded-retries.svg?v=94f1e69514) ```toml title="bounded-retries.toml" version = 3 entry = "steps.review" max_visits = 30 [budgets.answer] rounds = 5 [steps.review] prompt = "Review the goal and any previous repair. 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" }, ] [steps.repair] prompt = "Revise the proposed answer to the goal." next = "steps.review" [steps.done] kind = "terminal" [steps.exhausted] kind = "terminal" outcome = "failed" label = "Five repairs did not pass review" ``` Three routes leave `review`, and Orbital tries them in order: | Route | What it does | | --- | --- | | `verdict == 'pass'` to `done`, with `resets` | Goes to `done` and fills the budget again. In a workflow that carries on, the next piece of work would get five rounds. | | `verdict == 'reject'` to `repair`, with `spends` | Uses one round and sends the answer for repair. | | `verdict == 'reject'` to `exhausted` | Taken once the budget is empty, because Orbital then skips the route above it. | The routes answer both options of `verdict`, so `review` needs no `next`. `max_visits = 30` is the outer guard. [Download bounded-retries.toml](/examples/bounded-retries/bounded-retries.toml) ## 2. Validate it ```sh orbital validate ~/.orbital/workflows/bounded-retries.toml ``` ## 3. Run it Choose **New task**, pick any project, and choose the `bounded-retries` workflow and your harness. One project serves every example. Write the goal the review step should judge, and start the run. The first passing verdict ends the run successfully. A rejection starts up to five repairs. A sixth rejecting review ends at `exhausted` with a failed outcome. ## Choosing how to stop a loop | You want to | Use | | --- | --- | | Allow a few corrections of one piece of work | A budget, spent on the route back and reset on the route for progress, with a route for when it runs out. | | Guard against a loop that never ends | `max_visits` on the step or the workflow. | | Check on something outside the run | A wait in the loop. | | Start each pass as new work | `loop_restart = true` on the route back. | | Recover from a failed step | An `outcome == 'failed'` route to a repair prompt. | ## Related - [Add branches](/workflows/add-branches/): how routes choose the next step. - [Add gates and waits](/workflows/gates-and-waits/): pause a loop for time or for a person. - [Pass context between steps](/workflows/pass-context/): what a loop restart clears from context. - [Error handling](/reference/error-handling/): how failed steps and budgets behave. --- # Run steps in parallel > Run independent review steps at once and collect their results Run independent steps at the same time with a fan-out, then collect their results with a fan-in. In practice, independent work means reviews. ```toml title="parallel-reviews.toml" [steps.reviews] kind = "fan_out" max_parallel = 2 join = "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 }}" ``` Orbital keeps each branch's context apart, but not its files. Two branches that write to the same checkout will fight over it. ## How the fan-out works `reviews` is a [fan-out](/reference/step-kinds/#fan-out), a step with `kind = "fan_out"`. Each step inside it starts a branch, and a fan-out needs at least two. A fan-out can also name steps written elsewhere in the file, as `branches = ["steps.clarity", "steps.risks"]`, and each of those starts a branch too. Up to `max_parallel` branches run at a time, four by default. Instead of `next`, a fan-out has `join`, which names the [fan-in](/reference/step-kinds/#fan-in) where the branches meet. Each branch runs from its step until it reaches that fan-in. A branch may not hold a gate, and its steps may not loop, set `thread` or send the run out of the branch. Each branch keeps its own context, so `clarity` cannot read what `risks` produced. After the fan-in, results arrive under `parallel.results` in the order the fan-out names its branches, the `branches` list first and then the steps inside it in file order, with each branch's step, outcome, failure reason and context. Each branch agent's final prose is at its dotted ID, such as `context.response.reviews.clarity`. The fan-in prompt reads `{{ context.parallel.results['0'].context.assessment }}`. The index is quoted, and the complete example validates that spelling. A fan-in without a prompt is only the meeting point. ![The editor shows a fan-out step with a maximum of three, three parallel steps below it, and a prompted fan-in step that combines their findings.](/screenshots/workflow-editor-roles.webp?v=3ff23d4252) *Every branch leaves the fan-out and meets again at the fan-in.* A fan-out can also ask a [decision model](/workflows/decide-with-a-model/) which branches to start, and takes its default branches when none can answer. Set `max_parallel` to the number of steps your harness can run at once. If branches must write, give them separate files and review the combined result after the fan-in. :::caution If a branch fails and the fan-out has no route for the failure, the run is Paused. It waits for you and names the failed branches. Add a route that tests `outcome == 'failed'` on the fan-out when partial completion has a safe recovery. See [Runs](/runs/). ::: ## Before you start You need a harness you have signed in to that can run two steps at once, and a project in Orbital. Any project works. It does not need to be a Git repository, unless you run the example with Codex, which needs one. See [Projects and folders](/projects/). The branches share a folder, and their prompts ask only for a review of the goal. When branches must not change a shared folder, say so in their prompts. With Codex, a style can also give them a read-only sandbox with `codex.sandbox_mode = "read-only"`. ## 1. Download the example [Download the example](/parallel-reviews.zip) and unpack it into `~/.orbital/workflows/`. Keep every relative path. ![reviews forks into clarity and risks; both meet at collect, which summarises them and reaches done.](/parallel-reviews.svg?v=e668a01a0b) ```toml title="parallel-reviews.toml" version = 3 entry = "steps.reviews" [steps.reviews] kind = "fan_out" max_parallel = 2 join = "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" ``` [Download parallel-reviews.toml](/examples/parallel-reviews/parallel-reviews.toml) ## 2. Validate it ```sh orbital validate ~/.orbital/workflows/parallel-reviews.toml ``` Validate again whenever you change where a branch starts or ends. ## 3. Run it Choose **New task**, pick any project, and choose the `parallel-reviews` workflow and your harness. One project serves every example. Write the goal both reviews should assess, and start the run. Both reviews run. Their `assessment` outputs stay inside `parallel.results`. The `collect` step summarises both before `done`. ## Related - [Fan-out](/reference/step-kinds/#fan-out) and [fan-in](/reference/step-kinds/#fan-in): what the two steps take and do. - [Parallel branches](/reference/history-and-threads/#parallel-branches): the exact rules for branches, their context and their results. - [Pass context between steps](/workflows/pass-context/): why a branch cannot read its sibling's outputs. - [Give steps a style](/styles/configure-steps/): give each branch its own style. --- # Reuse a workflow > Move a shared sequence of steps into its own workflow file and import it When several workflows share a sequence of steps, move the sequence into its own workflow file and import it. One step with an `import` key stands in for the whole file. ```toml title="epic.toml" [steps.ticket] exits.delivered = { to = "steps.select", loop_restart = true } exits.rejected = "steps.stalled" ``` ## How an import works An imported workflow is an ordinary workflow file that another workflow imports. It has its own version, entry and terminals, so you can validate and run it on its own before any parent depends on it. It has to be a complete workflow, not a fragment. The import step becomes a [group](/workflows/#group-steps) that holds the imported steps, so the editor, the validator and the run all see one workflow. - Imported step IDs take the import step's ID as a prefix, so `ticket.review` is the imported `review` step, and its final prose is `context.response.ticket.review`. - A route to the import step reaches the imported entry. - Every terminal of the imported file is an exit, named by its ID. `exits.` on the import step says where the run goes when it leaves through that exit: a step, or `{ to = "steps.select", loop_restart = true }` when arriving there starts a new pass. An exit you leave unmapped ends the run with that terminal's outcome. A file with one exit still needs it named, and an imported terminal may not be `aborted`. - Outputs and facts keep their names across the boundary, because the parent and the imported steps share the run's context. That is what lets a parent condition read a value the imported workflow produced. Routes into a terminal inside the imported file decide why the run leaves it, and the parent only says where it goes. So conditions and budgets belong inside the imported file, and the import step carries only its `exits`. Budgets are shared by name across the parent and every file it imports. Paths in `import` and `prompt_file` resolve from the file that names them. A cycle of imports is refused. The [import](/reference/step-kinds/#import) reference gives the rest of the rules. ## Pass inputs in An imported workflow that declares inputs gets them from the import step. `inputs.` on the import step is a template that Orbital renders each time an imported step reads that input, so it may read the parent's inputs and the run's context. Every required input of the imported file must be passed, and an input it does not declare is refused. An optional input left out reads as its default, or empty. An import step that passes no inputs at all leaves its steps reading the parent's inputs, as an import converted from a DOT file does. ```toml title="repeat.toml" [steps.deliver] inputs.ticket = "{{ context.ticket }}" exits.shipped = "steps.shipped" exits.rejected = "steps.stuck" ``` Here an earlier step hands over a ticket output, and the imported workflow reads it as its own `ticket` input, `{{ inputs.ticket }}`. Orbital looks up only the inputs a run starts with, so the imported steps read the reference, not the ticket's fields. A looked-up input passed on unchanged, as `inputs.ticket = "{{ inputs.ticket }}"`, keeps its fields. [Repeat a workflow](/workflows/repeat-a-workflow/) walks through that example. ## Share settings with the steps inside An import step is a group for the steps it brings in, so it can carry a `label` and a `style`. The style applies to every imported step that does not set the same thing itself, through its own style or through a group inside the imported file. The imported file's own styles join the parent's, so its steps still find the styles they name. [Give steps a style](/styles/configure-steps/) explains the order. An import step cannot change the prompts of the steps inside. Put shared configuration on the import step or in the parent workflow. ## Before you start You need the [epic example](/epic.zip), unpacked into `~/.orbital/workflows/` with every relative path intact. It holds the parent and the imported workflow shown below. ## 1. Write the imported workflow This one implements and reviews a single piece of work. Its two terminals, `delivered` and `rejected`, become the exits the parent maps. Its own `sub_ticket` budget bounds the review loop inside it. ```toml title="imports/ticket.toml" version = 3 entry = "steps.implement" description = "Implement and locally review one sub ticket" [budgets.sub_ticket] rounds = 5 [steps.implement] prompt = "Implement the current work described below. Satisfy only its requirements. {{ current_work }}" outputs.implementation_summary = "text" next = "steps.review" [steps.review] prompt = "Review the implementation against the current work described below. Choose pass when it satisfies the work, or reject when it does not. {{ current_work }}" outputs.verdict = { kind = "choice", options = ["pass", "reject"] } routes = [ { if = "verdict == 'pass'", to = "steps.delivered", resets = "budgets.sub_ticket" }, { if = "verdict == 'reject'", to = "steps.implement", spends = "budgets.sub_ticket" }, { if = "verdict == 'reject'", to = "steps.rejected" }, ] [steps.delivered] kind = "terminal" [steps.rejected] kind = "terminal" outcome = "failed" label = "Five reviews rejected the same sub ticket" ``` It reads `{{ current_work }}` rather than a declared input, so the parent decides what one piece of work is. Any parent that sets current work can use it. ## 2. Validate it alone ```sh orbital validate ~/.orbital/workflows/imports/ticket.toml ``` ## 3. Add an import step to the parent `[steps.ticket]` with `import = "imports/ticket.toml"` is the import step. The path is relative to the parent, because relative paths resolve from the file that names them. ```toml title="epic.toml" version = 3 entry = "steps.select" description = "Work through an epic one sub ticket at a time" max_visits = 200 [inputs.work] kind = "epic" label = "Epic" hint = "The epic whose sub tickets this run delivers" [steps.select] prompt = """ Read the epic at {{ inputs.work.url }} and every sub ticket under it. Choose the next unfinished sub ticket and report it as the current work. Choose ticket while one remains, or none once every sub ticket is finished. """ outputs.current_work = "work" outputs.remaining = { kind = "choice", options = ["ticket", "none"] } routes = [ { if = "remaining == 'ticket'", to = "steps.ticket" }, { if = "remaining == 'none'", to = "steps.finished" }, ] [steps.ticket] import = "imports/ticket.toml" exits.delivered = { to = "steps.select", loop_restart = true } exits.rejected = "steps.stalled" [steps.finished] kind = "terminal" [steps.stalled] kind = "terminal" outcome = "failed" label = "A sub ticket could not be delivered" ``` ## 4. Map every exit Each `exits` key names the exit it maps. Here `delivered` returns to the `select` step with a loop restart, so the next sub ticket starts fresh, and `rejected` goes to the `stalled` terminal. ## 5. Validate the parent ```sh orbital validate ~/.orbital/workflows/epic.toml ``` The imported steps sit inside the import step's group, so the workflow contains `ticket.implement` and `ticket.review`. The editor draws the import step as one step, with each exit on its own arrow. Choose **Expand imports** to see a flat, read-only view with each import expanded, and **Show authored workflow** to return. ![The workflow editor shows an import step named landing, with two outgoing arrows labelled exit closed and exit merged, and an Expand imports button above the canvas.](/screenshots/workflow-editor-imports.webp?v=3f00eb3b61) *The import step maps each exit on its own arrow.* ## Share it Include every imported file and prompt file, and keep their relative paths. The [epic example](/epic.zip) shows the layout. The [ticket workflow](/examples/workflows/ticket/) is a parent with two imported workflows and twelve prompt files, shipped as one archive. If someone pastes the parent into **Import** on the Workflows page instead, the prompt files it refers to become empty prompts to fill in. ![The Import a workflow page has one large text box with Import and Cancel buttons below it.](/screenshots/import-workflow.webp?v=b5c29c87bd) *Pasted TOML is not stored until you save.* ## Related - [Import](/reference/step-kinds/#import) and [group](/reference/step-kinds/#group): the rules for import steps, IDs and exits. - [Ticket to merge](/examples/workflows/ticket/): a parent with two imported workflows. - [Outer loop over an epic](/examples/workflows/epic/): the same parent and imported workflow, explained step by step. - [Give steps a style](/styles/configure-steps/): set styles on imported steps. --- # Pass context between steps > Hand a value from one step to the next through the run's context Hand a value from one step to a later one by declaring it as an output, then reading it from context. Context is the one store a run reads and writes as it moves through the workflow. ```toml title="review.toml" [steps.review] prompt = "Review the change." outputs.verdict = { kind = "choice", options = ["pass", "reject"] } outputs.findings = "text" routes = [{ if = "verdict == 'reject'", to = "steps.fix" }] next = "steps.done" [steps.fix] prompt = "Address these findings: {{ context.findings }}" next = "steps.review" ``` Context starts with the goal and the declared inputs. It then gains an entry for every accepted agent output, every probe and command fact, what each action records, and each step's `outcome`. Prompts read it, conditions read it, and nothing else carries information between steps. The run page's **Context** tab lists every value the run holds and the step that set it. ![The run page's Context tab lists fourteen values held by the run, such as next, summary, pr and github.pr.state, each with the step that set it on the right.](/screenshots/run-context.webp?v=3e79bd8607) *Each value shows the step that set it, such as select, implement or a probe visit.* ## What goes in | Source | What it adds | | --- | --- | | Declared inputs | Copied into context when the run starts. | | An agent step | Only the outputs it declares. | | A probe step | Per-folder facts such as `web.github.pr.raw`, and rolled-up facts such as `github.pr.state`. | | A command step | The facts it declares under `facts`. | | An action step | What the action records, such as `github.pr.url` after `github.pr.open`, or `git.worktree.clean` after `worktree.remove`. | | Every step | Its `outcome`, and `failure_reason` when it fails. | [Context](/reference/context/#command-facts) in the reference lists the facts each probe writes. ## Typed inputs `{{ inputs.work }}` always renders the original input value. `{{ context.work }}` renders whatever the run holds now, which a later step can change. [Workflows](/workflows/#declared-inputs) explains how to declare inputs. An input's `kind` decides what the run holds. A `text` or `url` input holds the text it was given. A `ticket` or `epic` input also holds the ticket Orbital read from GitHub or Linear when the run was created, so a prompt reads `{{ inputs.ticket.title }}` and `{{ inputs.ticket.body }}` without an agent fetching the ticket first. `{{ inputs.ticket }}` on its own renders the reference the run was given. [Input kinds](/reference/workflow-file/#input-kinds) lists the fields. Inputs reach more than prompts. Any value in the workflow file may read one, such as `rounds = "{{ inputs.max_rounds }}"` on a budget, and the validator checks that the input's kind fits the key. ## Declare outputs An agent contributes only what it declares under `outputs`. It finishes its turn by calling the `handoff` tool with them, and the tool refuses undeclared keys. When an output is malformed, the agent gets up to two turns to correct it, and then the step fails. Context changes only once Orbital accepts the outputs. The final prose stays readable as `{{ context.response.review }}`, named after the step's ID. The short form gives only the kind, as `outputs.findings = "text"`. An inline table adds more on one line, as `verdict` does above with its options. An output with more to say gets a table of its own: ```toml [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"] } ``` `description` tells the agent what the output is for. An output is required unless it sets `required = false`. `required_if` makes it required only when a condition over the same step's outputs holds, so here the review owes findings only when it rejects; when the condition does not hold, the agent may still give them. The output a `required_if` reads may not be conditional itself, and a route may test only outputs that are always required. A `list` output holds items, and each `item.` declares one field of an item with its kind, description and options. Every item field is required, and an item field cannot be a list. `allow_empty = true` lets the list arrive with no items. A `ticket` output names a ticket. The handoff refuses a value that is not written as a ticket reference, a URL, `owner/repository#N`, `#N` or a key such as `ENG-123`, and later templates read the reference as text, such as `{{ context.ticket }}`. Orbital does not look an output's ticket up. [Output kinds](/reference/workflow-file/#output-kinds) lists every kind. ## What a step can read The validator checks, before a run starts, that a value is produced on every path that leads to the step reading it. This catches the common mistake: a prompt that reads a verdict only one branch produces. It fails validation rather than rendering empty at three in the morning. When a value may be missing on purpose, say so with `needs`. Each need names a context path and the default the step uses when the value arrives empty or not at all: ```toml [steps.summarise] prompt = "Summarise the work. The plan: {{ context.plan }} The implementer reported: {{ context.implementation_summary }}" needs.plan = "" needs.implementation_summary = "No summary was given." ``` `""` means no default, so that value must arrive on every path. A step without `needs` needs what its prompt reads. [`needs`](/reference/workflow-file/#needs) gives the full rules. Two consequences follow: - A parallel branch cannot read its sibling's outputs. After the fan-in, read them through `parallel.results`. See [Run steps in parallel](/workflows/run-steps-in-parallel/). - A repair prompt reached by a failure route can read `{{ context.failure_reason }}`, because Orbital writes it whenever a step fails. ## Clear context on purpose A route with `loop_restart = true` clears the values the loop wrote to context, among other things. Use it when a loop starts a new piece of work, so the next pass does not reason about the last one. [Add a loop](/workflows/add-a-loop/) lists what it clears and what it keeps. ## Related - [Template variables](/reference/template-variables/): every name a prompt can read. - [Context](/reference/context/): the exact fact names and output kinds. - [Add branches](/workflows/add-branches/): how routes read context to choose the next step. - [How Orbital works](/concepts/how-orbital-works/): how context is saved and restored across a restart. --- # Add gates and waits > Stop a run for a person with a gate, or for a set time with a wait Stop a run until a person lets it continue with a gate, or for a set time with a wait. Use a gate where the process needs a person, and a wait where it needs time to pass. ```toml title="release.toml" [steps.approve] kind = "gate" label = "Approve the release" next = "steps.publish" [steps.pause] kind = "wait" duration = "30m" next = "steps.check" ``` Both survive a restart. | | Gate | Wait | | --- | --- | --- | | Kind | `gate` | `wait` with a `duration` | | Ends when | A person lets it continue, or a watched pull request changes | The duration passes | | Run slot | Given up | Kept | | After a restart | Stays at the gate | Wakes at the time it first set | ## Gates A gate is a step with `kind = "gate"`. It does no work. When a run reaches one, its standing is Waiting. It shows an eye and "Waiting:" followed by the gate's label, not the pause mark, so you can tell it apart from a run you paused. The runs list's Status filter calls it **At a gate**. It holds no run slot, and it stays at the gate across a restart. ```toml title="gate.toml" version = 3 entry = "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" ``` Here the run writes a plan, then stops at `approve` so you can read it before anything changes. To let the run continue, open it and choose **Continue** or **Skip** on the gate, or **Resume**. The run then joins the back of the queue, continues when a slot is free, and follows the gate's `next`. A coding agent can do the same with the `control_run` MCP tool and the `SkipWait` or `Resume` action. A gate needs `next`. A gate inside a fan-out branch is refused. ### The pull request gate The shipped delivery workflows stop at a gate labelled "The pull request is waiting for review or merge". The run page shows how long it has waited and when Orbital checks next. ![A gate step reads The pull request is waiting for review or merge, with the time waited, the time until the next check, and a Skip button.](/screenshots/gate-wait.webp?v=fdf72f63d4) *The gate shows how long it has waited and when it checks next.* When the run's context holds `github.pr.state`, Orbital looks at the pull request once a minute. The run continues by itself once the `github.pr` probe's rolled-up `github.pr.state` is no longer `WAITING`: the pull request merged, closed, conflicts, failed CI or needs changes. It then joins the queue and follows the gate's `next`, as when you let it continue. An approved pull request that is not merged yet goes back to the gate. [Worktrees and delivery](/projects/worktrees-and-delivery/) explains the whole loop. When the run has read its tickets with the `tracker.tickets` probe and could read every one, the gate waits for the tickets' text to change instead, which lets a person answer a run by editing its ticket. Set `resume = "operator"` on the gate to make it wait for you alone, whatever the run has read: ```toml [steps.approve] kind = "gate" label = "Approve the release" resume = "operator" next = "steps.release" ``` ## Waits A wait is a step with `kind = "wait"` and a `duration`: a whole number followed by `ms`, `s`, `m` or `h`, such as `duration = "30m"`. The run sleeps for that time, then continues. A wait keeps its run slot, but waiting costs no agent visits. ```toml title="wait.toml" version = 3 entry = "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" ``` This one checks a pull request, and while it is neither merged nor closed, waits ten minutes and checks again. Neither the probe nor the wait spends an agent turn. The run page shows a countdown. Choose **Skip** to end the wait now. ![A wait step reads Wakes in 16m 48s, with a progress bar and a Skip button.](/screenshots/wait-countdown.webp?v=ece53f53a5) *Choose Skip to end the wait early.* Orbital records the skip, so it still applies if Orbital restarts at that moment. A wait that was running during a restart wakes at the time it first set. The epic workflows, for example, wait thirty minutes when every remaining ticket is blocked, then look again. [Add a loop](/workflows/add-a-loop/) explains why a poll loop needs a wait. ## Questions from the agent An agent can also stop a run by asking you something. The run stands as Question until you answer in the composer. That is not a gate, because the workflow did not plan it. See [Message a run](/runs/message-a-run/). ![An agent's question card asks which cap to rename, offers two options to pick from, and has a box to write your own answer with a Send button.](/screenshots/agent-question.webp?v=92fd44eff4) *Pick an option or write your own answer.* ## Related - [Gate](/reference/step-kinds/#gate) and [wait](/reference/step-kinds/#wait): the keys both steps take. - [Add a loop](/workflows/add-a-loop/): use a wait inside a poll loop. - [Worktrees and delivery](/projects/worktrees-and-delivery/): the delivery loop around the pull request gate. - [Pause, stop or resolve a run](/runs/control-a-run/): resume a run by hand. --- # Push and open pull requests without an agent > Take a checkout, commit, push and open a pull request with action steps, and spend no agent turn on publishing Publishing a change is the same work every time: commit it, push the branch and open a pull request. An action step does each of these itself, so no agent turn goes on it and no prompt has to explain how. ```toml title="action.toml" [steps.publish.commit] kind = "action" do = "git.commit" message = { write = "A conventional commit message for the change", style = "quick" } next = "steps.publish.push" ``` ## Why an action and not an agent An agent asked to publish spends a turn running commands that never change, and may run them differently each time. An action does the same thing every time, and is safe to repeat. Committing with nothing to commit does nothing. Pushing an unchanged branch does nothing. Opening a pull request reuses the one already open for the branch. So a run that comes back to its publishing steps, after a review asks for changes or after a restart, never makes a second pull request. Actions spend no agent turns, so they do not count towards the workflow's `max_visits`. ## The actions An action step has `kind = "action"`, and `do` names the action. | Action | What it does | Keys | | --- | --- | --- | | `worktree.create` | Makes an isolated checkout of each repository folder, all on one branch for the run, and moves the run into them, so the run never touches your working copy. | None | | `worktree.remove` | Removes the checkouts when nothing would be lost, and records `git.worktree.clean` and `git.worktree.status`. | `force` | | `git.commit` | Commits every change in each repository's checkout. | `message`, which it needs | | `git.push` | Pushes each repository's branch. | None | | `github.pr.open` | Opens a pull request for each repository, or reuses the one open for the branch, and records `github.pr.url`, `github.pr.number` and `github.pr.state`. | `title`, which it needs, and `body`, `draft`, `base`, `auto_merge` | `force = true` on `worktree.remove` removes the checkouts even when they hold local work, and discards their uncommitted changes and untracked files; the branch and its commits stay. The default is `false`, and a checkout borrowed from a chat is never removed. `draft = true` opens the pull request as a draft; the default is `false`. `base` is the branch to merge into: unless you set it, the branch the run's checkout was made from, which is usually the repository's default branch. `auto_merge` asks GitHub to merge the pull request by `squash`, `merge` or `rebase`. After `github.pr.open`, context holds the same `github.pr` paths a [probe](/reference/step-kinds/#probe) writes, so a later step can route on `github.pr.state`. This workflow takes a checkout, lets an agent make the change, then publishes it with three actions: ```toml title="action.toml" version = 3 entry = "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 = true title = { 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" ``` The prompt tells the agent not to commit, so the commit, the push and the pull request all happen in the actions after it. [Action](/reference/step-kinds/#action) gives every key. ## Let a model write the text `message`, `title` and `body` take plain text, or a table that asks a model to write it: `{ write = "A short title naming the change" }`. A write runs a short model session with no tools. It reads the run's context and returns only the text, so the commit message and the pull request describe the change this run made. A write does not count towards `max_visits`, and it appears in the run history. A write takes its model settings the way an agent step in the same place would. Its own `style` comes first, as `{ write = "...", style = "quick" }`, then the styles of the groups holding the action, then the workflow's `style`. So a group is a tidy place to set them once for every write inside it. Every built-in harness can write a text: Claude Code, Codex, OpenCode and Pi. The write runs on the harness, model, effort and provider its settings resolve to, with the program, credentials and variables you set for that harness. A harness added by a plugin may not write texts; when a write's settings resolve to one, the step fails with a message naming the harness, so give the write a style on a harness that can. ## Before you start You need a project whose primary folder is a Git repository with a GitHub remote you can push to, Git credentials, and a harness you have signed in to. See [Projects and folders](/projects/). ## 1. Download the example The one-shot workflow takes a checkout, implements once, reviews once, publishes the change and puts the checkout away. [Download the example](/one-shot.zip) and unpack it into `~/.orbital/workflows/`. Keep every relative path. ![checkout, implement, review. A passing review publishes and cleans up; a rejected review keeps the checkout.](/one-shot.svg?v=5abb76da02) ```toml title="one-shot.toml" version = 3 entry = "steps.checkout" description = "Implement one change in an isolated checkout, review it once and publish it" [inputs.work] label = "Work" hint = "The change to make, in plain words or as a ticket URL" [styles.quick] effort = "low" [steps.checkout] kind = "action" do = "worktree.create" next = "steps.implement" [steps.implement] style = "implementer" prompt = "Implement the change described by {{ inputs.work }}. Follow the conventions already in the repository. Do not commit or push." outputs.implementation_summary = "text" next = "steps.review" [steps.review] style = "reviewer" prompt = "Review the change in the working folder. The implementer reported: {{ context.implementation_summary }}. Choose pass when the change is complete and correct, or reject when it is not. Do not change files." outputs.verdict = { kind = "choice", options = ["pass", "reject"] } routes = [ { if = "verdict == 'pass'", to = "steps.publish.commit" }, { if = "verdict == 'reject'", to = "steps.rejected" }, ] [steps.publish] style = "quick" [steps.publish.commit] kind = "action" do = "git.commit" message = { write = "A commit message that says why the change exists" } 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" title = { write = "A short title naming the change" } body = { write = "Why the change exists, what it changes and how it was checked" } next = "steps.cleanup" [steps.cleanup] kind = "action" do = "worktree.remove" routes = [{ if = "git.worktree.clean", to = "steps.published" }] next = "steps.retained" [steps.published] kind = "terminal" [steps.rejected] kind = "terminal" outcome = "failed" label = "Checkout kept for inspection" [steps.retained] kind = "terminal" outcome = "failed" label = "Checkout retained to preserve local work" ``` `checkout` takes the isolated checkout. A passing review sends the run into the `publish` group, whose `quick` style sets low effort for every write inside it, so the commit message, title and body each cost a quick model session and no agent turn. `cleanup` removes the checkout and routes on `git.worktree.clean`. Removal refuses when any checkout would lose work, so the run then ends at `retained`, a failed terminal that says what happened. [Download one-shot.toml](/examples/one-shot/one-shot.toml) ## 2. Validate it ```sh orbital validate ~/.orbital/workflows/one-shot.toml ``` ## 3. Run it Choose **New task** and pick the project on the repository. Choose the `one-shot` workflow, set **Work** to a description or ticket URL, and write the goal. A passing review ends with a pull request open on the run's branch and the checkout removed. A rejected review ends the run Failed and keeps the checkout, so you can read the diff. ## Related - [Action](/reference/step-kinds/#action): every action, its keys and what it records. - [One-shot implementation](/examples/workflows/one-shot-implementation/): the same workflow, explained step by step. - [Worktrees and delivery](/projects/worktrees-and-delivery/): checkouts, branches and the pull request loop. - [Give steps a style](/styles/configure-steps/): the styles a write and a group use. --- # Repeat a workflow > Start a new run of the same workflow each time one succeeds, until a final ending Some work comes in pieces, such as every sub ticket of an epic, shipped one per run. Add a `[repeat]` table, and each time a run of the workflow ends on a successful terminal, Orbital starts a new run of the same workflow with the same inputs. ```toml title="repeat.toml" [repeat] wait = "5m" max_runs = 50 ``` ## How the chain ends A repeating workflow says it has finished with a terminal that sets `final = true`. A run that reaches one ends the chain. A run that ends `failed` or `aborted` ends it too, so a broken run is never repeated. Validation requires at least one `final` terminal, so every repeating workflow has a way to say the work is done, and `final` is allowed only in a repeating workflow. | The run ends | What happens next | | --- | --- | | On a `success` terminal | Orbital starts the next run after `wait`. | | On a terminal with `final = true` | The chain ends. | | `failed` or `aborted` | The chain ends. | | As run number `max_runs` | The chain ends. | `wait` is the pause between one run and the next, such as `"5m"`; without it the next run starts straight away. `max_runs` caps the chain, 100 by default. ## Each run starts with the same inputs Every run in the chain starts with the inputs the first run was given. It does not start from what the previous run learned, so the workflow finds out for itself what is left to do. A `ticket` or `epic` input is looked up again when each run starts, so each run reads the ticket's title and description as they stand then. A run whose ticket can no longer be read does not start, and the chain ends there. Each run is a separate run in the Runs list, with its own history, and each run's page shows the chain it belongs to. ## Before you start You need a project whose primary folder is a Git repository with a GitHub remote you can push to, Git credentials, a harness you have signed in to, and an epic, a GitHub or Linear issue whose sub tickets describe work in that repository. See [Projects and folders](/projects/). ## 1. Download the example This workflow ships an epic one sub ticket per run. [Download the example](/repeat.zip) and unpack it into `~/.orbital/workflows/`. Keep every relative path. ![pick chooses the next sub ticket, deliver ships it, and the run ends at shipped; when no sub ticket is left or the rest wait on people, the run ends at a final terminal.](/repeat.svg?v=1008caaf64) ```toml title="repeat.toml" version = 3 entry = "steps.pick" description = "Ship an epic one sub ticket per run until none is left" [repeat] wait = "5m" max_runs = 50 [inputs.epic] kind = "epic" label = "Epic" hint = "A GitHub or Linear epic whose sub tickets this workflow ships" [steps.pick] style = "planner" prompt = """ Ship the sub tickets of {{ inputs.epic.key }}, {{ inputs.epic.title }}, one per run. The epic is at {{ inputs.epic.url }} and reads: {{ inputs.epic.body }} Read its sub tickets as they stand now and pick the next one to ship. Prefer tickets whose dependencies have merged. Answer none when every sub ticket is shipped, or blocked when the rest wait on people. """ routes = [ { if = "next == 'ticket'", to = "steps.deliver" }, { if = "next == 'none'", to = "steps.all_shipped" }, { if = "next == 'blocked'", to = "steps.waiting_on_people" }, ] [steps.pick.outputs.next] kind = "choice" options.ticket = "A sub ticket is ready to ship" options.none = "Every sub ticket is shipped" options.blocked = "The remaining sub tickets wait on people" [steps.pick.outputs.ticket] kind = "ticket" description = "The sub ticket to ship next" required_if = "next == 'ticket'" [steps.deliver] import = "imports/ship-ticket.toml" inputs.ticket = "{{ context.ticket }}" exits.shipped = "steps.shipped" exits.rejected = "steps.stuck" [steps.shipped] kind = "terminal" label = "Shipped one ticket" [steps.all_shipped] kind = "terminal" final = true label = "Every sub ticket of the epic is shipped" [steps.waiting_on_people] kind = "terminal" final = true label = "The remaining sub tickets wait on people" [steps.stuck] kind = "terminal" outcome = "failed" label = "A ticket could not be shipped" ``` `inputs.epic` has `kind = "epic"`, so the new-run form asks for the epic's address and Orbital looks the epic up when each run starts. `pick` reads its key, title, address and description from the input, then reads the sub tickets itself, because Orbital does not list them. `pick` answers `next` with `ticket`, `none` or `blocked`, and hands over the ticket to ship as its `ticket` output. `required_if = "next == 'ticket'"` makes that output required only when there is a ticket to ship, so `pick` can answer `none` or `blocked` without one. Because the output's kind is `ticket`, the handoff refuses a value that is not written as a ticket reference. `deliver` imports `imports/ship-ticket.toml` and passes the ticket in as its `ticket` input. That input renders the reference `pick` gave, and the agent reads the ticket itself: Orbital looks up only the inputs a run starts with. A shipped ticket ends the run at `shipped`, a success terminal without `final`, so Orbital starts the next run five minutes later. `all_shipped` and `waiting_on_people` are final, so the chain ends once nothing is left or the rest wait on people. A ticket that cannot be shipped ends at `stuck`, a failed terminal, which ends the chain too, so you can look before anything else ships. The imported workflow takes a checkout, implements and commits the ticket, reviews it, then pushes the branch, opens a pull request and asks GitHub to squash-merge it: ```toml title="imports/ship-ticket.toml" version = 3 entry = "steps.checkout" description = "Implement one ticket, review it and open its pull request" [inputs.ticket] kind = "ticket" label = "Ticket" [steps.checkout] kind = "action" do = "worktree.create" next = "steps.implement" [steps.implement] style = "implementer" prompt = """ Implement the ticket {{ inputs.ticket }}. Read it first, with its description and comments. Commit the change using the repository's conventions. Do not push. """ next = "steps.review" [steps.review] style = "reviewer" prompt = "Review the committed change against the ticket {{ inputs.ticket }}. Choose pass when it is ready to publish, or reject. Do not change files." outputs.verdict = { kind = "choice", options = ["pass", "reject"] } routes = [ { if = "verdict == 'pass'", to = "steps.push" }, { if = "verdict == 'reject'", to = "steps.rejected" }, ] [steps.push] kind = "action" do = "git.push" next = "steps.open" [steps.open] kind = "action" do = "github.pr.open" auto_merge = "squash" title = { write = "A short title naming the change" } body = { write = "Why the change exists and what it changes. End with Closes followed by the ticket reference." } next = "steps.shipped" [steps.shipped] kind = "terminal" [steps.rejected] kind = "terminal" outcome = "failed" ``` [Push and open pull requests without an agent](/workflows/use-actions/) explains its actions, and [Reuse a workflow](/workflows/reuse-a-workflow/) explains the import. ## 2. Validate it ```sh orbital validate ~/.orbital/workflows/repeat.toml ``` ## 3. Run it Choose **New task** and pick the project on the repository. Choose the `repeat` workflow, set **Epic** to the epic's address, and write the goal. Each shipped ticket is its own run in the Runs list, five minutes apart. The chain stops at the first final ending, at the first run that fails, or after fifty runs. ## Related - [`[repeat]`](/reference/workflow-file/#repeat): the keys and the rules for a chain of runs. - [Terminal](/reference/step-kinds/#terminal): `outcome` and `final`. - [Pass context between steps](/workflows/pass-context/): outputs, `required_if` and ticket outputs. - [Push and open pull requests without an agent](/workflows/use-actions/): the actions the imported workflow uses. - [Reuse a workflow](/workflows/reuse-a-workflow/): import steps, inputs and exits. --- # One-off workflows > Let an agent propose a workflow for one piece of work, and approve it before it runs Some work needs a process no saved workflow has, and is not worth saving. A One-off Workflow is written for one run. An agent proposes its TOML text, Orbital checks it, and the run waits for you as a held Run until you approve it. It is an experimental feature: turn on **One-off workflows** in **Settings > Experimental**. ## The text A One-off Workflow is the same `version = 3` TOML as any [workflow file](/reference/workflow-file/), with a few extra rules so one run cannot grow without bounds: - it is at most 64 KiB and has at most 40 steps; - it is self-contained: no `import`, no `prompt_file` and no `[repeat]`; - it sets a workflow-level `max_visits` of at most 50, and no failure route loops back without a bound. Text from a source Orbital does not trust also may not use `auto_merge` or `permissions = "full"`. An agent checks text with [`validate_workflow`](/reference/mcp-tools/#validate_workflow) and its `source` argument before proposing it. Text that breaks a rule is refused with each problem named, and no run is created. A One-off Workflow never appears in a workflow list, the start composer, the trigger picker or `list_workflows`. Its run keeps its own snapshot, so Resume, Retry, Jump, Restart and Run Export never look its name up, even if a saved workflow has the same name. ## Approve a held Run A held Run is listed as **One-off** and **Needs approval**, and raises a desktop notification when notifications are on. Its page shows what you need to decide: - where the text came from, with a link, and whether Orbital trusts that source; - the canvas, the harnesses and styles its steps use, and its warnings; - the keys with effects outside the worktree, such as `command` scripts, `git.push`, `github.pr.open`, `worktree.remove`, `auto_merge` and `permissions = "full"`. Every agent step has every tool, so read the prompts too; - a cost caution, the TOML and when the approval expires. Start the run to approve it: it runs exactly the text you read. Abort it to decline it. A held Run nobody decides on within 24 hours ends Aborted with "Approval expired". A project holds at most 10 runs waiting for approval. ## Where proposals come from | Source | Trusted | What happens | | --- | --- | --- | | The delivery lead's chat | Yes | Always held, whatever **Start runs** says, unless it says **Never**, which refuses it. A new proposal under the same name replaces the earlier one. | | A trigger rule | Yes, once you approve the rule | The rule pins the text, and each firing runs it without asking. | | A watcher wake-up | No | Always held, with the wake-up's reason. The delivery lead is woken with your decision. | | A step of a run | No | Always held as a child run of the run that proposed it. | | An agent outside Orbital | No | **Settings > MCP** decides. | ### From the delivery lead's chat Ask the [delivery lead](/delivery-lead/) for work no saved workflow fits. It writes a One-off Workflow, checks it and proposes it with [`start_run`](/reference/mcp-tools/#start_run) and `source`. The held Run appears in the chat's Run tab. If you ask for changes, its next proposal under the same name replaces the earlier one, which ends Aborted with "Replaced by a newer version". ### From a trigger rule A [trigger rule](/triggers/make-a-rule/)'s Start action can pin a One-off Workflow's text and name. Every firing runs exactly that text, so the rule itself is what you approve. A rule an agent creates or changes with pinned text is saved switched off and marked **Needs approval**, and only you can switch it on, in the app. A rule never takes a definition from the event that fired it: event fields reach the run only through the goal, as for any rule. ### From a watcher wake-up When the watcher wakes the delivery lead about a run, such as one that failed, and it proposes a One-off Workflow before you have written anything, the proposal is untrusted. It is held with the reason it woke and the run it was about, and a newer proposal about the same run replaces it. The delivery lead is woken again with what you decided. ### From a step of a run A step can propose a child run with the `run.propose` action. A planner step writes the TOML into the run's context, and the action proposes it: ```toml [steps.propose] kind = "action" do = "run.propose" source = "{{ context.plan }}" name = "triage" goal = "Triage the failing tests" wait = true routes = [ { if = "run.child.outcome == 'succeeded'", to = "steps.done" }, ] next = "steps.failed" ``` The child is held in the same project and links back to the step that proposed it. The step writes `run.child.id`. With `wait = true`, the run gives back its Run Slot and parks until the child ends, then writes `run.child.outcome`: `succeeded`, `failed`, `aborted`, `declined` or `expired`. A proposed run cannot propose another, and one run proposes at most 3 children. Invalid text fails the step and creates no child. ### From an agent outside Orbital An agent connected over [MCP](/reference/mcp-tools/) proposes with `start_run` and `source`. **One-off workflows from agents outside Orbital** in **Settings > MCP** decides what happens: **Ask me first**, the default, holds the run; **Do it** starts it at once; **Never** refuses it. The run shows the client name the agent reported, marked as unverified. Restarting such a run holds it for approval again. ## Save a One-off Workflow A One-off Workflow that worked can become a saved workflow. Open the run, then choose **Save as workflow** from its Actions menu. The text opens in the editor as an Import, and you save it as any workflow. --- # Triggers > Rules that start, stop, pause or message runs when something happens A trigger rule acts on your [runs](/runs/) when something happens: a pull request opens, a Linear issue moves, or a time of day comes round. You write the rule once, and it can start a run, or stop, pause or message the runs already working on that item. :::caution[Beta] Triggers are an experimental feature. They are off until you turn them on in **Settings > Experimental features**. ::: Rules belong to a [project](/projects/). They are never part of a workflow file. ![The Triggers page lists rules with their source, an on or off switch, what each rule does and its recent firings, and one rule shows an error.](/screenshots/triggers-page.webp?v=5fe1ad8f4b) *Each rule shows its source, whether it is on, and what it did lately.* ## Turn triggers on 1. **Open the experimental features.** Open **Settings > Experimental features**. 2. **Turn on Triggers.** Turn on **Triggers**. Its switch reads "Rules that start, pause, stop or message runs when GitHub, Linear or a schedule says so." 3. **Find your rules.** **Triggers** now appears in the sidebar. Each project's page also gets a section for its trigger rules. ![Settings, Experimental features, shows switches for Delivery lead, Triggers and Accounts.](/screenshots/settings-experimental.webp?v=e8e4d1269c) *The Triggers switch turns the whole feature on or off.* While the feature is off, Orbital checks nothing and the Triggers page says "Triggers are turned off". Orbital keeps your rules, and when you turn the feature back on, each rule resumes from then, and changes made while it was off are skipped. A rule you switch off and on again also resumes from now. ## What can set a rule off Each rule has one source, and the source says what event the rule waits for. | Source | When | What it needs | | --- | --- | --- | | Schedule | "At set times, such as every weekday morning." Under "How often?", pick a preset, choose "Pick days and a time" and select the days and time, or write a cron expression. It uses the time zone of the machine that runs Orbital. | Nothing. | | GitHub | "When a pull request or issue changes": opened, closed, reopened, labelled, unlabelled, assigned, unassigned, a review requested, or new commits pushed to a pull request. Choose issues, pull requests or both. For pull requests, **Draft status** picks "Draft only", "Ready only" or "Draft and ready". | A GitHub token; see [Settings](/reference/settings/#github). | | Linear | "When a Linear issue changes": created, moved to another state, handed to someone else, a label added or removed. | A Linear key in **Settings > Linear**. | Orbital checks each source on a schedule, not by webhooks. You choose how soon it notices a change: "Within about a minute (recommended)", within five or fifteen minutes, or within an hour. "New commits are pushed to a pull request" fires once for each new head commit Orbital sees on an open pull request, including a force push and a pull request from a fork. Several pushes between two checks count as one, at the newest commit. Comments, reviews, labels, title edits and draft status changes are not pushes. When a rule starts watching pushes, the pull requests already open are not reviewed until they get new commits. Each check reads the commits of the open pull requests, a hundred to a request and at most a thousand, and the next check carries on until it has read them all, then starts again. So in a repository with more than a thousand open pull requests, a push may be noticed a few checks later, and a pull request that closes while Orbital reads the list may shift another past it until the next time round. Commits pushed to a pull request before Orbital has noted it become where it starts, so they start no review, because Orbital cannot tell them from a title edit. A dry run cannot look back at pushes, because GitHub keeps no record of when a pull request's commits arrived. **Draft status** decides which pull requests the rule acts on, for openings and new commits alike. It never marks a pull request as draft or ready on GitHub. A rule that asks for "Draft only" or "Ready only" skips a pull request whose draft status GitHub did not report. ## What a rule does Each rule has one action. | Action | What it does | | --- | --- | | Starts a run | "Orbital starts a workflow and hands it a goal." Choose the workflow, and optionally the harness and model. | | Stops runs | "Stops the runs already working on this item." | | Pauses runs | "Pauses the runs working on this item so you can look." | | Messages runs | "Sends the runs working on this item a note." | Stop, pause and message act only on runs a trigger started for the same pull request or issue. "A schedule can only start runs." A rule starts at most one run at a time for each pull request or issue. When an event arrives while a run the rule started for that item is still working, the rule starts nothing and its recent firings show "Skipped", naming the run. Runs other rules or you started do not count, and "Run now" always starts. A schedule tick that passes while Orbital is closed or asleep is not lost. When Orbital returns, the latest missed tick runs once, with that tick's `{{ date }}` and `{{ time }}`, and its recent firing says "as scheduled, 40 min late". Earlier missed ticks start nothing and show as "Missed". If Orbital is not back before the next tick, that tick runs on time and the missed one shows as "Missed". A schedule switched off and on again starts from then and shows nothing missed for the time it was off. A rule that starts runs starts at most five in one check. When a check matches more, such as after a bulk label edit or a long time offline, the first five in event order start, and each of the others is recorded as "Skipped" in the rule's recent firings with the number of matches. Skipped events never start later. Stop, pause and message rules have no limit. A check reads only the newest changes: up to 1,000 per kind of change on GitHub, and about 500 issues on Linear. When a repository or team changed more than that since the last check, Orbital acts on what it read and skips the older changes. The rule's row says which scope it was and the time before which changes were skipped, until the next complete check, and the check stays in the recent firings. The goal of a started run is a template that can use the values the event carries: | Source | Values | | --- | --- | | Schedule | `{{ date }}`, `{{ time }}` | | GitHub | `{{ url }}`, `{{ number }}`, `{{ title }}`, `{{ author }}`, `{{ repository }}`, `{{ head_branch }}`, `{{ head_sha }}`, `{{ base_branch }}`, `{{ event }}` | | Linear | `{{ identifier }}`, `{{ url }}`, `{{ title }}`, `{{ team }}`, `{{ state }}`, `{{ assignee }}`, `{{ event }}` | For example, `Review {{ url }}: {{ title }}`. ## Conditions Add conditions under "Only if" to narrow a rule down, so it acts only on what matters. | Part | What it does | | --- | --- | | Value | What the condition looks at, such as the author, a label or the target branch. | | Comparison | "is any of", "is none of" or "contains". | | "and" or "or" | Combines the conditions in one group. | | "Or when…" | Adds another group. Any group that matches sets the rule off. | On a label or assignee event, the Label and Assignee conditions test only the label or person that changed. "When a pull request gets a label, labelled `needs-review`" fires when `needs-review` is added, not when `docs` is added to a pull request that already has it, and "is none of" fires for any added label except the ones named. The same holds for GitHub labelled, unlabelled, assigned and unassigned, and for Linear labels added or removed. On any other event, such as opened or pushed, these conditions test all the item's current labels and assignees. ## Quick reference | You want to | Source | Action | | --- | --- | --- | | Review every new pull request | GitHub, a pull request is opened | Starts a run | | Improve the codebase every weekday morning | Schedule | Starts a run | | Stop a run when its Linear issue is cancelled | Linear, an issue moves to another state | Stops runs | | Look at a run before it goes further | GitHub or Linear | Pauses runs | | Tell a working run that its item changed | GitHub or Linear | Messages runs | ## Related - [Make a trigger rule](/triggers/make-a-rule/): create, test and follow a rule. - [Review pull requests](/examples/review-pull-requests/): a complete rule that reviews every new pull request. - [Settings](/reference/settings/#github): where Orbital finds your GitHub token. - [Troubleshooting](/troubleshooting/): fixes for a rule that does not fire. --- # Make a trigger rule > Create a rule that acts on runs, test it, and follow what it does You make a [trigger rule](/triggers/) in a project, test it against the last seven days, and save it. From then on it starts, stops, pauses or messages runs when GitHub, Linear or a schedule says so. ## Before you start - Turn on **Triggers** in **Settings > Experimental features**. See [Turn triggers on](/triggers/#turn-triggers-on). - For a GitHub rule, save a GitHub token. See [Settings](/reference/settings/#github). - For a Linear rule, save a Linear key in **Settings > Linear**. ## Make a rule 1. **Open the rules.** Open **Triggers** in the sidebar, or the trigger rules section of a project's page. ![A project page with triggers turned on shows a section for the project's trigger rules.](/screenshots/project-triggers.webp?v=f689fb22f3) *A project's page lists the rules that belong to it.* 2. **Start a rule.** Choose **New rule**. Or start from a ready-made rule: "Review every new pull request", "Improve the codebase every weekday morning" or "Stop the run when its Linear issue is cancelled". 3. **Name the rule and pick what sets it off.** Give the rule a name. Then pick its source and the event it waits for. [Sources and events](#sources-and-events) shows the events for each source. 4. **Pick what the rule does.** Choose whether it starts a run, or stops, pauses or messages runs. To start a run, choose the workflow and write its goal. You can also pick the harness and model. [What a rule does](/triggers/#what-a-rule-does) lists the values the goal can use. 5. **Add conditions if you need them.** Under "Only if", add the conditions that narrow the rule down. See [Conditions](/triggers/#conditions). 6. **Preview.** Choose **Preview**. Orbital looks back over the last seven days and reports what the rule would have done. Saving turns the rule on. :::caution A saved rule acts on its own. It can start, stop or pause runs without you pressing anything. Read what **Preview** reports before you rely on the rule. ::: ## Sources and events **GitHub** A GitHub rule sets off "When a pull request or issue changes". Choose issues, pull requests or both, and the event: opened, closed, reopened, labelled, unlabelled, assigned, unassigned, or a review requested. ![The New rule dialog set to GitHub pull requests, with the event 'A pull request is opened' chosen.](/screenshots/trigger-rule-github.webp?v=7d933426ed) *Pick pull requests, issues or both, then the event.* **Linear** A Linear rule sets off "When a Linear issue changes". Choose the event: created, moved to another state, handed to someone else, or a label added or removed. ![The New rule dialog set to Linear, with the event 'An issue moves to another state' chosen.](/screenshots/trigger-rule-linear.webp?v=466473fe60) *Pick the change to the Linear issue that sets the rule off.* **Schedule** A schedule rule sets off "At set times, such as every weekday morning." Under "How often?", pick a preset, choose "Pick days and a time" and select the days and time, or write a cron expression. It uses the time zone of the machine that runs Orbital. A schedule can only start runs. ![The New rule dialog set to a schedule of every weekday at 09:00, with the next run times listed.](/screenshots/trigger-rule-schedule.webp?v=5f91dd32fb) *The dialog lists the next times the rule will fire.* ## Follow a rule The rules list shows whether each rule is on and when Orbital last checked. It also shows what the rule did, such as "Started a run" or "Fired 2 times in the last 7 days". A rule that cannot check its source says why, for example that GitHub or Linear refused the saved token. Deleting a rule also deletes its record of what it did, including failed and skipped firings. Runs it started carry on and keep its name. To stop a rule but keep its record, switch it off instead. ## Related - [Triggers](/triggers/): sources, actions, goal values and conditions. - [Review pull requests](/examples/review-pull-requests/): a complete rule, from start to finish. - [Start a run](/runs/start-a-run/): what a started run does. - [Permissions](/styles/#permissions): what a run asks you before its agent acts, on each harness. - [Troubleshooting](/troubleshooting/): fixes for a rule that does not fire. --- # Delivery lead > A standing chat that watches your runs and steps in when one needs help You turn on the delivery lead to have a [chat](/chats/) that looks after your runs while you are away. It leads delivery inside Orbital: when a run needs help, it steps in, it asks you about what it cannot decide, and it tells you what happened. A watcher wakes it, and it acts within the limits you set. :::caution[Beta] The delivery lead is an experimental feature. It is off until you turn it on in **Settings > Experimental features**. ::: This page describes the built-in delivery lead. To run a delivery lead from a coding agent instead, or to learn the loop a delivery lead follows, see [Lead delivery](/delivery-lead/lead-delivery/). Orbital attaches its own MCP server to chats as `orbital_app`. On Claude, load its tools with `ToolSearch`, for example `select:mcp__orbital_app__list_runs`. This server is separate from an `orbital` server you register in your own agent configuration. ## Turn it on 1. **Open the experimental features.** Open **Settings > Experimental features**. 2. **Turn on the delivery lead.** Turn on the **Delivery lead** switch. It reads "A standing chat that watches your runs and steps in when one needs help." 3. **Open its chat.** A chat named **Delivery lead** now appears, pinned at the top of your chat list. It is never archived. While the feature is off, no part of the delivery lead runs or shows. ## What it watches Orbital keeps a record of every run. A built-in watcher reads that record, and the watcher spends no model tokens. It wakes the delivery lead only for these reasons: | Reason | What counts | | --- | --- | | Needs a decision | A run stops by itself and is Paused, with the badge "Waiting on you". A run at a gate, such as one waiting for a pull request review, does not count, because that is your process working. | | Failed | A run ends Failed. | | Out of quota | A harness runs out of quota. | The watcher also wakes the delivery lead to look over runs that are still going, every hour unless you change it. It also wakes the delivery lead when you approve or decline something it asked about, and for a daily summary if you turn that on. The watcher looks at the record every 15 seconds. The same reason wakes the delivery lead only once. If the app was closed, the watcher catches up when you open it again. A restart neither repeats a wake-up nor loses one. Orbital retries a turn that stalls by itself, twice in one step, and wakes the delivery lead only when that does not help. A run stops, and the watcher wakes the delivery lead, in three cases: Orbital gives up on a stalled turn, a step reaches its visit limit, or the working folder cannot be used. Your runs already handle some problems themselves, so the delivery lead leaves them alone: failing checks, merge conflicts and review changes that the delivery workflow repairs, and closing tickets. A run that waits days for a person to review its pull request is normal. The delivery lead does not chase it. ## What it will and will not do The delivery lead acts inside Orbital. In your tracker and pull requests, it writes only when you ask it to or approve it. | It can | It does not | | --- | --- | | Send a running step a message, or answer its question | Comment on, review, approve or merge pull requests without your go-ahead | | Pause, resume, retry, jump, skip a wait, restart or abort a run | Turn on auto-merge | | Revise a run's goal | Ping reviewers | | Move a run to another harness | Post in Slack or anywhere else | | Commit a run's uncommitted work on the run's own branch and push it, never with force and never to main | Create, edit, comment on or close tickets without your go-ahead | | Start runs | | | Archive runs | | | Check what a run delivered | | It follows these safety rules: - It secures a run's work before it aborts the run, and aborts only a run that holds no unpushed work. - It never deletes a branch that holds work nobody has pushed. - It does what you asked and no more. Each action appears in the run's thread as coming from the delivery lead, with its reason. A short line also appears in the delivery lead chat, for example "Delivery lead paused run …". To have an action undone, ask in the chat. When something in your tracker or pull requests needs saying, the delivery lead drafts the text in its chat and offers to post it. Tell it to post, and it posts the text itself through the tools its harness offers. The delivery lead can read connected sources, such as your tracker or GitHub, to answer a real question. It writes through them only when you ask it to or approve it. ## How its calls are scoped The delivery lead gets every tool its harness offers, the same as a coding agent: it can read and change files, run commands, use the web, and call MCP servers and connectors, apart from the MCP servers you block in its settings. It acts on runs through Orbital's own [MCP tools](/reference/mcp-tools/), and Orbital checks every call it makes to them against these rules: - It must give a reason with every action. The reason is recorded on the run and in its chat. - It acts only on runs it owns. - Each action follows **What it may do**: **Do it**, **Ask me first** or **Never**. - It stops acting when it is off, paused for today, or over its daily limit. [MCP tools](/reference/mcp-tools/#how-a-delivery-leads-calls-are-scoped) lists every rule. ## Talk to it Open the pinned delivery lead chat and write to it as you would to a person who leads delivery. For example: - "Start the triggers epic." - "Why is this run stuck?" - "What landed today?" You can attach files, as in any chat. ![The delivery lead chat shows a watcher message about a stalled run, the delivery lead's reply, and the Ask the delivery lead box.](/screenshots/delivery-lead-chat.webp?v=5f6e9c6341) *The watcher's message and the delivery lead's reply share one chat.* When an action is set to **Ask me first**, the delivery lead asks before it acts. A card appears in the chat, marked "Waiting for you". It says what the delivery lead wants to do, where and why. Choose **Approve** to let it act once, or **Decline** to refuse. Either way, the delivery lead carries on with your answer. If desktop notifications are on, a notification opens the chat at that card. ### Start it fresh The delivery lead carries on one session from turn to turn, so an old conclusion can linger. Choose **Start fresh** in its chat header while it is waiting. The chat keeps its messages, and the next turn begins a new session. Its notes carry over. The delivery lead tries Orbital's tools before it says they are unavailable. If a turn cannot connect to Orbital, or Orbital refuses the connection, the chat shows it. Before the next turn, Orbital checks its own tools again, even after a restart, and tells the delivery lead whether they answered. ## A delivery lead for one project A [project](/projects/) can have a delivery lead of its own. Open the project's page and choose **Add delivery lead**. Its pinned chat is named "Delivery lead · " followed by the project's name. It reads only that project. Each run has one owner. If the run's project has its own delivery lead, that delivery lead looks after the run. Otherwise the Orbital-wide delivery lead does. A project's delivery lead can hand a matter, or a single run, to the Orbital-wide delivery lead. The run records the handover. To stop, choose **Remove delivery lead** on the project's page. Its runs go back to the Orbital-wide delivery lead. Its chat is archived, and its history stays readable. You can still act on any run yourself. ## Its settings Open **Settings > Delivery lead**. Each change saves as you make it. ![Settings, Delivery lead, shows its status and pause control, the watched projects, and what it may do.](/screenshots/settings-delivery-lead.webp?v=8363eb55f2) *Each action can be set to Do it, Ask me first or Never.* A picker at the top chooses which delivery lead you change: Orbital-wide, or a project's own delivery lead. A project's delivery lead follows the Orbital-wide settings until you change a field. Such a field shows "Same as Orbital-wide". A changed field has a button to go back to the Orbital-wide value. The page shows these groups, in this order: | Group | What you can set | | --- | --- | | **Status** | The **Delivery lead** switch turns it on or off. **Pause for today** stops it acting until the next 07:00. | | **Watched projects** | **Watches**: All projects, or "Only the projects I pick". Only the Orbital-wide delivery lead has this group. | | **What it may do** | For each action: **Do it**, **Ask me first** or **Never**. Every action starts at **Do it**. | | **What it may read** | A switch for each connected source. | | **What wakes it** | A switch for each of the three reasons, and **Look over runs at work**: Every hour, Every few hours or Off. | | **Who it is** | **Role**: the role sets its prompt, and the preset in use sets its harness, model and effort. Edit both in **Model settings**, which the link under **Role** opens. The settings reference describes them under [Personas](/reference/settings/#personas) and [Styles](/reference/settings/#presets). | | **Quota fallbacks** | **Move a run to**: the presets to move a run to when its harness runs out of quota, tried first to last. See [The fallback preset](#the-fallback-preset). | | **Spending** | **Daily limit**: No limit, tokens a day, or an estimated cost a day in USD. It counts only the delivery lead's own turns. **Runs it started, at work at once**: No cap, or at most a number you choose. | | **Notifications** | **Tell me about**: Decisions only, Decisions and failures, or Every action. **Quiet hours**: during these hours you only hear about decisions it needs from you. **Desktop notifications**: On or Off. | | **Reports** | **When it acts, say**: Brief says what it did, Detailed also says why. **Daily summary**: Off, or every day at a time you choose. | | **Notes** | What the delivery lead keeps in mind between conversations, as it reads them at the start of each turn. It writes them itself, one dated observation at a time, and reflects on them once they grow long, folding what is finished into one summary. You can add, edit, pin, unpin and resolve an observation, each kept as a new one, read each reflection with the observations it folded under History, and clear everything, history included. | | **Your instructions** | **For all projects**, and one field for each project. | | **Housekeeping** | **Archive finished runs**: Never, or after a number of days. **Keep its activity log**: how many days to keep it. | | **Start over** | Resets every field to its default. For a project's delivery lead, it resets to the Orbital-wide settings. | The actions under **What it may do** are these: | Action | | --- | | Message and control runs | | Save and push a run's work to its own branch | | Switch a run's harness | | Start runs | | Archive runs | When the delivery lead reaches its daily limit, it stops acting for the rest of the day. It says so once in its chat. ## The fallback preset When a run's harness runs out of quota, the delivery lead can move the run to the first preset in its **Quota fallbacks** list that can take it. It pins the run to that preset and retries the step with `control_run` `ChangePreset`. The retried step and every step after it run each role on the preset's harness, model and effort. The run continues in place and keeps its work. A fallback whose preset no longer exists, or whose harness is not installed or not signed in, shows as unavailable, and the delivery lead skips it. [Switch a run's harness](/harnesses/switch-harness/#quota-fallbacks) describes the move and where to set the list. ## Choosing how to switch it off Choose the lightest option that does what you need. The options run from lightest to heaviest. | Option | How | What happens | | --- | --- | --- | | Stop one kind of action | Under **What it may do**, set that action to **Never**. | It no longer takes that action. | | Pause it for the day | Under **Status**, choose **Pause for today**. | It carries on by itself at 07:00, or when you choose **Resume now**. | | Turn the delivery lead off | Under **Status**, turn off the **Delivery lead** switch. | It takes no actions and nothing wakes it. You can still talk to it in its chat. | | Remove a project's own delivery lead | On the project's page, choose **Remove delivery lead**. | Its runs go back to the Orbital-wide delivery lead. | | Turn off the feature | Open **Settings > Experimental features** and turn off **Delivery lead**. | No part of the delivery lead runs or shows. Orbital keeps its settings for when you turn it back on. | ## Related - [Lead delivery](/delivery-lead/lead-delivery/): the loop a delivery lead follows, and how to run it. - [MCP tools](/reference/mcp-tools/#how-a-delivery-leads-calls-are-scoped): every rule Orbital applies to the delivery lead's calls. - [Switch a run's harness](/harnesses/switch-harness/): how a quota fallback moves a run. - [Chats](/chats/): how the delivery lead's chat works like any other chat. - [Troubleshooting](/troubleshooting/): fixes when the delivery lead does not act. --- # Lead delivery > Put a delivery lead over your runs, as a coding agent or the built-in one You put a delivery lead over your [runs](/runs/) so you can keep the product decisions and hand off the rest. The delivery lead makes tickets ready, writes goals, starts runs on them and checks in on a steady cadence. It unsticks runs without losing work, and checks what they delivered before it closes anything. The delivery lead is either a coding agent connected over MCP or the [built-in delivery lead](/delivery-lead/). Both follow the same loop. [Choosing a coding agent or the built-in delivery lead](#choosing-a-coding-agent-or-the-built-in-delivery-lead) compares them. ## The loop The delivery lead repeats these six steps. 1. **Refine the ticket against main.** An open ticket is not always ready to build. Before any run starts on it, check these points: | Check | What to do | | --- | --- | | Is it already delivered? | Search merged pull requests for its number, and read the code on the default branch. Close a delivered ticket with a pointer to the pull request. | | Is it still true? | Recent merges rename files and remove what a ticket relies on. Fix every stale name and claim. | | Is it measured? | A ticket about speed, size or cost carries today's numbers. | | Does it fit the product now? | Check it against the repository's rules files, such as `CLAUDE.md` or `AGENTS.md`, and recent decisions. | | Is it one piece of work? | Split a ticket too large for one run. | | Can a run prove it is done? | Every "done when" item must be something a check or a test can show. | 2. **Write a goal that carries every rule.** Most work runs on `pursue-goal`. Its validate step sees only the goal and the code, so write every rule validate needs into the goal: - Link the ticket and say "exactly as the ticket describes". - Tell the Implementer to commit at the end of every round, and never to leave work uncommitted between rounds. - Tell validate to check every "done when" item one by one, and to run the repository's checks. Tell it that a blocker the agent cannot fix is never a pass. - Name the repository's own checks and rules. - Ask for one closing keyword per ticket in the pull request. For several tickets in one run, list them in order. Tell validate to reject with the next ticket's name while tickets remain. 3. **Start runs.** Start the run with the goal. Start runs that touch the same files one after another, and independent runs side by side. Keep the [queue](/runs/queue/) short: at most one run queued beyond those working. 4. **Check in on a cadence.** Check every active run about every 30 minutes. For each run, read its standing, the steps it entered, validate's verdicts and feedback, its worktree's commits and uncommitted changes, and its pull request's checks. A run is making progress when it adds commits, when validate's findings shrink, or when its pull request moves. Treat it as stuck when: - validate repeats the same findings for three rounds with no new commit; - uncommitted work sits in the worktree across two rounds; - the implement step is within three visits of its cap; - a step has been silent for over 20 minutes with no tool calls; - it loops between two steps with nothing new; - it is [Paused](/runs/#standings) with a reason, such as the badge "Waiting on you"; - its pull request fails checks, or GitHub will not merge it; - it is queued behind slots that stuck runs hold. A run whose badge reads "Waiting: The pull request is waiting for review or merge" for days is not stuck. That is your review process working. 5. **Unstick without losing work.** Secure the work first. When a stopped run has uncommitted changes, commit and push them to its own branch with `secure_run_work`. Then take the least disruptive step that puts the run back on track, in this order: | Order | Step | | --- | --- | | 1 | Message the running step with the concrete next action. | | 2 | Resume a paused run whose cause is fixed. | | 3 | Retry a failed step whose cause has passed. | | 4 | Split an oversized ticket, and start a new run from the pushed branch. | | 5 | Fix a small cause yourself, when it is in reach. | | 6 | File a bug for an Orbital defect, with the run and the evidence. | A message reaches only the step that is running. When the scope changes, revise the run's goal as well. Record the change on the ticket, where validate reads it. 6. **Verify and close.** Never take a run's word for it: - Confirm the pull request merged and every ticket it names is closed. - Check the result yourself on the default branch: read the code and run the proving check. - Close an epic when every sub-issue is closed. - Archive finished runs, and remove worktrees they left once nothing in them is unpushed. ## Rules that keep it safe These rules apply to any delivery lead, the skill or the built-in delivery lead. | Rule | What it means | | --- | --- | | Never abort unpushed work. | Abort a run only when its work is committed and pushed and a replacement run has started from that branch, or when it holds no work at all. | | Keep the queue short. | At most one run queued beyond those working. A long queue behind a stuck run holds everything up. | | A blocker is never a pass. | Validate keeps rejecting while a blocker stands, and says so. The delivery lead fixes or escalates the blocker; it never lowers the bar. | | Respect the review process. | The delivery lead does not approve or merge pull requests, and does not chase reviewers. | | Never check out a run's branch. | A run's worktree holds it. | | Give a reason with every action. | It is recorded on the run. | ## Set up the delivery lead **Coding agent over MCP** 1. **Connect the agent.** [Connect the agent to Orbital over MCP](/mcp/connect/). 2. **Install the skills.** [Install the shipped skills](/mcp/skills/) from **Settings > Command line and skills**, or run `orbital skill install`. 3. **Start a session.** Start a new session of the agent in the repository. 4. **Ask it to lead.** Give it the ticket, the project and the cadence. For example: ```text Use the orbital-delivery-lead skill. Deliver https://github.com/acme/shop/issues/412 with Orbital in the Shop project. Refine the ticket first, then start a pursue-goal run and check in every 30 minutes until it merges. ``` ![Settings, MCP, shows a switch for each tool group, with Archiving and Changing Orbital's settings off.](/screenshots/settings-mcp-tools.webp?v=73920e6114) *Archiving is its own tool group; turn it on so the agent can archive and resolve runs.* The agent works through the loop with Orbital's [MCP tools](/reference/mcp-tools/), plus `gh` and `git`: | Loop step | Tools | | --- | --- | | Refine | `gh issue view`, `gh pr list --search`, reading the code | | Start | `list_projects`, `list_workflows`, `start_run` | | Check in | `list_runs`, `get_run`, `read_run_history`, `read_run_thread`, `wait_for_run`, `read_run_worktree`, `read_pull_request` | | Unstick | `secure_run_work`, `message_run`, `revise_goal`, `control_run` | | Close | `gh pr view`, `gh issue close`, `archive_run`, `resolve_run` | `archive_run` and `resolve_run` belong to the Archiving tool group, which is off until you turn it on in **Settings > MCP**. To keep the cadence, ask the agent to repeat its check-in, for example with Claude Code's `/loop` command. Or use `wait_for_run`, which returns as soon as a run's standing changes. **Built-in delivery lead** 1. **Turn on the delivery lead.** Open **Settings > Experimental features** and turn on **Delivery lead**. A chat named **Delivery lead** appears, pinned at the top of your chats. 2. **Choose what it may do.** Open **Settings > Delivery lead**. Each action is **Do it**, **Ask me first** or **Never**. Start with **Ask me first** for **Start runs** and **Switch a run's harness** until you trust it. 3. **Write your rules.** Under **Your instructions**, write your rules, such as your goal template and "Never abort a run that holds unpushed work." 4. **Talk to it.** Ask it for work in its chat. For example: ```text Start implement-ticket on https://github.com/acme/shop/issues/412 in Shop, and tell me when the pull request is ready for review. ``` ![The delivery lead chat shows a watcher message about a stalled run, the delivery lead's reply, and the Ask the delivery lead box.](/screenshots/delivery-lead-chat.webp?v=5f6e9c6341) *The watcher wakes the delivery lead in its chat, and you answer there.* It asks you only for decisions that are yours. With **Ask me first**, a card marked "Waiting for you" appears with **Approve** and **Decline**. The watcher reads the run record every 15 seconds and spends no tokens. It wakes the delivery lead when a run stops and needs a decision, except at a gate. It also wakes it when a run fails or a harness runs out of quota. The hourly look over runs at work wakes it too, and so does your answer to something it asked about. It does not wake the delivery lead for what workflows already handle: failing CI, conflicts, review changes. Orbital retries a stalled turn itself. The run stops, and the watcher wakes the delivery lead, once Orbital gives up, a step reaches its visit limit, or the working folder cannot be used. [What it watches](/delivery-lead/#what-it-watches) has the details. Inside the limits you set, the delivery lead messages and controls runs, answers their questions and revises goals. It [switches a run to a quota fallback](/harnesses/switch-harness/), starts runs and archives runs. Every action carries a reason, shows on the run's thread, and appears as a line in its chat. It can use every tool its harness offers, such as commands, MCP servers and connectors, to read context. It writes to your tracker or pull requests only when you ask it to or approve it. Otherwise it drafts the text in its chat and offers to post it. [The delivery lead](/delivery-lead/) describes every setting. ## A worked example
From a ticket to a merged pull request Issue #412 in `acme/shop` asks for a "Copy link" button on each invoice row. The delivery lead is Claude Code with the `orbital-delivery-lead` skill. **Refine.** The agent reads #412 and searches merged pull requests for "412": none. It reads `src/invoices/InvoiceRow.tsx` on main and finds the ticket names `InvoiceTable.tsx`, which a recent pull request renamed. It fixes the name in the ticket, and adds a measurable "done when": "a unit test covers the link format". **Write the goal.** From the one-ticket template: ```text Implement https://github.com/acme/shop/issues/412 exactly as the ticket describes. Read the ticket first. Follow CLAUDE.md. Commit your progress at the end of every implement round, even when the work is not finished. Never leave work uncommitted between rounds. Validate checks every "done when" item of the ticket one by one and runs npm test. It rejects when any item is missing and says which, with the evidence. A blocker you cannot fix is never a pass: say so in the feedback and keep rejecting. It passes only when every item holds and npm test passes. Put `Closes #412` in the pull request description. ``` **Start.** `list_projects` gives the Shop project's id. `start_run` starts `pursue-goal` with that goal and the title "Copy link on invoice rows". Orbital answers with the run's id and its page. **Check in.** Thirty minutes later, `get_run` shows the run in its fourth validate round. `read_run_history` shows validate rejecting three times with the same finding, "no unit test for the link format". There has been no new commit since the first round. Three rounds of the same findings with no new commit is a stuck signal. **Unstick.** The agent sends `message_run` with delivery `Resume`: "Commit your progress now. Then add the unit test validate asks for in src/invoices/link.test.ts." Its reason: "validate repeated one finding for three rounds with no new commit". The next round adds a commit with the test, and validate passes. **Delivery.** The run opens pull request #418 with `Closes #412`, and stops at the gate. Its badge reads "Waiting: The pull request is waiting for review or merge". It holds no slot while it waits. CI fails on a lint rule; the run's fix-CI step repairs it and publishes again. A person reviews and merges #418. **Verify and close.** The agent confirms #418 merged and #412 closed, pulls main and runs `npm test`: it passes. It checks the new test is in main. It archives the run with `archive_run`, and tells you: "#412 delivered in #418, merged and verified on main."
## Choosing a coding agent or the built-in delivery lead | | A coding agent over MCP | The built-in delivery lead chat | | --- | --- | --- | | What it is | Claude Code, Codex or OpenCode, with the `orbital-delivery-lead` skill, connected to Orbital's MCP endpoint. | A standing chat inside Orbital, woken by a watcher. See [The delivery lead](/delivery-lead/). | | What wakes it | You, or a loop you set up in the agent. | The watcher, when a run stops, fails or runs out of quota, and for an hourly look over runs at work. | | What it can do | Everything in [the loop](#the-loop): tickets, runs, `git`, `gh`, fixes. | Act on runs inside Orbital. It writes to your tracker or pull requests only when you ask it to or approve it. Otherwise it drafts that text in its chat and offers to post it. | | Best for | Driving a backlog end to end, including refining tickets. | Keeping runs moving while you are away. | Many people use both: a coding agent to plan and start the work, and the built-in delivery lead to watch it overnight. ## Related - [The delivery lead](/delivery-lead/): every setting of the built-in delivery lead. - [Connect a coding agent](/mcp/connect/): connect Claude Code, Codex or OpenCode to Orbital. - [Use the shipped skills](/mcp/skills/): install the `orbital-delivery-lead` skill. - [MCP tools](/reference/mcp-tools/): every tool the delivery lead can call. - [Deliver an epic](/examples/deliver-an-epic/): a delivery lead working through an epic. --- # Chats > Talk to one coding agent in its own worktree, with no workflow You start a chat to talk to one coding agent in a [project](/projects/), with no workflow behind it. The agent works in a worktree of its own, and you can attach files to your messages. Use a chat for work you do not repeat, such as a question, a quick fix or an explanation. Use a [run](/runs/) when you want the same process every time. ## Start a chat 1. **Open the Start page.** Choose **New task** in the sidebar. 2. **Switch to chat.** Set the switch under the text box to **Chat**. 3. **Write your first message.** Type it in the box that reads "Ask for a change, a fix, or an explanation". 4. **Start the chat.** Choose **Start chat**. The chat appears under **Recent chats** in the sidebar. ![The Start page in chat mode shows the message box and the Chat and Workflow switch set to Chat.](/screenshots/start-chat.webp?v=cc57633966) *With the switch on Chat, the Start page starts a chat, not a run.* ## Where a chat works Each chat gets a worktree of every repository folder in its project, on a branch named after the chat. The agent works in the primary folder's worktree, so nothing it does touches your own checkout. A chat takes no [run slot](/runs/queue/), and its agent's permission requests are accepted for it. It can use Orbital's own [MCP tools](/reference/mcp-tools/). ## Talk to the agent Each message starts a turn. While the agent is answering, a new message steers it, where the harness allows. Choose **Interrupt** to stop the answer but keep the session, so your next message carries on the same conversation. ![A chat with an agent shows the conversation, the workflows started from the chat, and the message box at the bottom.](/screenshots/chat-page.webp?v=e0ef602f2c) *Write in the box at the bottom to start the next turn.* The agent can ask you a question with options to choose from. ![A chat where the agent asks a question and offers options to choose from.](/screenshots/chat-question.webp?v=0a554c8359) *Pick an option to answer the agent.* ## Attach files Attach files to a chat message with the attach control, or paste an image. You can attach images (PNG, JPEG, GIF, WebP), PDFs, text files and other documents. | Limit | Value | | --- | --- | | Files per message | 100 | | Size of one image | 10 MB | | Images per message, in total | 80 MB | | Size of any other file | 50 MB | Orbital shrinks an image that is too large before it sends it. Each harness receives some kinds of file directly, and gets the rest as a file path it can read. | Harness | Receives directly | | --- | --- | | Claude Code | Images and PDFs | | Codex | Images | | OpenCode | Images, PDFs and text files | Orbital keeps attachments in `~/.orbital/attachments/`. Runs have no attach control, so attach files in a chat. ## Start a workflow from a chat Open the chat's **Start workflow** tab and choose a workflow. The run works in the chat's worktree and shows as a tab of the chat, not in the runs list. You can keep chatting while it runs. ## Archive a chat Archive a chat when you are done with it. Archiving removes its worktree and branch, and keeps the conversation. :::caution Archiving removes the chat's worktree and branch, whatever they hold. Push anything you want to keep before you archive. ::: ## After a restart A chat that was answering when Orbital restarted goes back to waiting, with a note that the answer failed. Nothing resumes by itself. Send a message to carry on. ## The delivery lead chat With the delivery lead feature on, a delivery lead chat is pinned at the top of the chat list. You cannot archive or delete it. See [The delivery lead](/delivery-lead/). ## Choosing a chat or a run | | Chat | Run | | --- | --- | --- | | Behind it | No workflow. You steer each turn. | A [workflow](/workflows/), the same process every time. | | Where it works | A worktree of its own, on a branch named after the chat. | A worktree when its workflow makes one, otherwise your own folders. See [Worktrees and delivery](/projects/worktrees-and-delivery/). | | Run slot | Takes none. | Holds one while it works. See [The run queue](/runs/queue/). | | Attachments | Yes. | No attach control. | | After a restart | Goes back to waiting. Send a message to carry on. | Carries on where it was, unless you paused it. See [Durability after restarts](/runs/durability/). | You can have both: start a workflow from a chat, and the run works in the chat's worktree. ## Related - [Runs](/runs/): when to use a run instead of a chat. - [The delivery lead](/delivery-lead/): the pinned chat that looks after your runs. - [Worktrees and delivery](/projects/worktrees-and-delivery/): how worktrees keep your checkout safe. - [Harnesses and models](/harnesses/): the agents a chat can use. --- # Create a project > Add a project and its folders, then set its run limit and actions Create a [project](/projects/) for each piece of work you hand to agents, such as a product with a web app and an API. A project is a name and the folders a run works in. Start by choosing **Projects** in the sidebar. On first launch, Orbital opens a setup wizard that adds your first project for you. [Start your first run](/get-started/first-run/) walks through it. Use the steps below for every project after that. ![The first-launch wizard reads Give your agents a place to work, with the steps Welcome, Project and First run, and a Set up a project button.](/screenshots/first-launch.webp?v=5ece35762f) *On first launch, the wizard adds your first project.* ## Before you start - Install Orbital. See [Install Orbital](/get-started/install/). - Know where each folder lives on the machine that runs Orbital. A folder can be a Git repository or a plain folder. [Plain folders and Git](/projects/#plain-folders-and-git) says when you need a repository. ## Create a project and add folders 1. **Open Projects.** Choose **Projects** in the sidebar. 2. **Make the project.** Choose **New project** and give it a name, such as `Platform`. 3. **Add a folder.** Open the project and choose **Add folder**. Browse the machine that runs Orbital, or type the folder's absolute path. 4. **Name the folder, if you want.** Give the folder a short name: lowercase letters, digits and underscores. Leave it blank to use the folder's own name. 5. **Add the other folders.** Choose **Add folder** beside the folder list for each further folder. The first folder you add is the primary folder, where each agent step starts. ![A project page shows its folders with name, path, type and role, then the Runs group and a settings group for each folder.](/screenshots/project-details.webp?v=a56bdefccc) *Each folder shows its type and whether it is primary.* ## Limit runs at a time A project can limit how many of its runs work at once. On the project's page, find **Run limit** under **Runs**. It reads, for example, "At most 2 runs at a time". Choose **−** or **+**, or type a number and press Enter. Each change saves at once. Leave it empty for no limit of its own. When a project reaches its limit, its next runs wait, and other projects' runs go first. [The run queue](/runs/queue/#limit-one-projects-runs) explains how the limit works with the one in Settings. ## Add custom actions Custom actions are commands and links you add to a project for its runs. They appear in each run's **Run actions** menu. 1. **Add the action.** On the project's page, choose **Add action**. 2. **Name it.** Give it a **Key**, such as `build`, and a **Label**, such as `Build`. 3. **Choose its kind.** Pick a **Kind** from the table below, and fill in its command or URL template. | Kind | What it holds | Example | | --- | --- | --- | | **Command** | A command. It runs on the machine that runs Orbital, in the run's folder you choose. | `pnpm build` | | **Link** | A URL template. | `https://example.com/{path}` | Saving an action runs nothing. ## Related - [Projects and folders](/projects/): what folders, roles and project settings mean - [Start a run](/runs/start-a-run/): give the new project its first goal - [The run queue](/runs/queue/): how project limits and the Settings limit work together --- # Worktrees and delivery > Code workflows work in a worktree, open a pull request and see it merged Give a workflow a `worktree.create` action when it must change files without touching your own checkout. A worktree is an isolated copy of a repository, on a branch of its own. This workflow makes one, changes it, and removes it only when nothing would be lost: ```toml title="worktree-removal.toml" version = 3 entry = "steps.checkout" [steps.checkout] kind = "action" do = "worktree.create" next = "steps.implement" [steps.implement] prompt = "Make the change the goal describes, commit it and push it." next = "steps.cleanup" [steps.cleanup] kind = "action" do = "worktree.remove" routes = [{ if = "git.worktree.clean", to = "steps.done" }] next = "steps.kept" [steps.done] kind = "terminal" [steps.kept] kind = "terminal" outcome = "failed" label = "The worktree still holds work" ``` A worktree is optional. A run gets one only when its workflow has a `worktree.create` action. `explain-repo`, the research, follow-up and triage guides, and most workflows that only read and write text have none. The shipped code workflows create a worktree first. When the work passes review, they open a pull request and repair it until it merges or closes. Then they remove the worktree, unless that would lose work. ## Worktrees The `worktree.create` action makes one worktree for each repository folder of the [project](/projects/). All of them share one branch name, `orbital/`. They live under `~/.orbital/worktrees/`. Plain folders, which are not Git repositories, are used where they are. See [plain folders and Git](/projects/#plain-folders-and-git). From then on, each agent step starts in the primary folder's worktree and reaches the other worktrees as additional folders. Before a worktree exists, a step works in your own folders. That is why `explain-repo`, which makes no worktree, only reads. Its prompt tells it not to change files. The dock's **Changes** tab shows what the run changed in its worktree, and **Files** shows the worktree's files. **Open in** opens the worktree in your editor. ## Removing a worktree The `worktree.remove` action removes the run's worktrees only when nothing in them would be lost. It keeps a worktree that holds uncommitted changes or commits nobody has pushed, and records why in `git.worktree.status`, with `git.worktree.clean` false. The shipped workflows then end on a terminal that says the worktree was kept, so you can look. Set `force = true` on the step to remove the worktrees anyway. A forced removal discards uncommitted changes and untracked files; the run's branch, with any unpushed commits, stays. A checkout the run borrowed from a chat is never removed. Never delete a kept worktree to make a run look finished. Commit and push what you need first. Archiving a run also removes its worktrees. ## Delivery The shipped delivery workflows share one delivery sub workflow, `pr-delivery`. It runs these stages: | Stage | What happens | | --- | --- | | Publish | An agent step pushes the branch and opens the pull request, following the repository's conventions. When the step ends, Orbital asks GitHub to squash-merge the pull request once its checks and reviews allow it. A pull request that can merge already merges at once. If the repository does not allow auto-merge, a rule blocks it, or the token lacks permission, the run's history records that and delivery carries on. | | Watch | A probe reads the pull request's state from GitHub. | | Wait at a gate | While the pull request waits for review or merge, the run waits at a human gate, "The pull request is waiting for review or merge". It shows an eye and "Waiting: The pull request is waiting for review or merge", and takes no run slot. Orbital looks at the pull request once a minute and carries on by itself when something changes. | | Repair | Conflicts, failing CI and requested changes each have a repair step. A repair that changes the branch is reviewed against its reason alone, then published again. Before addressing new comments, a [decision model](/workflows/decide-with-a-model/) judges whether they are a code review or only noise, such as a deploy bot's preview status. Noise goes back to the wait and is not judged again; with no decision model, the run addresses every new comment. | | End | A merged pull request ends the run Succeeded. A pull request closed without merging ends it Failed. | A workflow of your own can also push the branch and open the pull request with actions rather than an agent step. [Push and open pull requests without an agent](/workflows/use-actions/) shows how. Choose **Skip** at the gate, or **Resume**, to make the run look at the pull request at once. [Add gates and waits](/workflows/gates-and-waits/) explains gates. ![A gate step reads The pull request is waiting for review or merge, with the waiting time, the next check and a Skip button.](/screenshots/gate-wait.webp?v=fdf72f63d4) *Skip makes the run check the pull request now.* :::caution[Pull requests can merge with no review] Delivery never approves a pull request. It asks GitHub to merge it once your branch rules allow. On a repository that requires no review and no checks, the pull request merges as soon as it opens. Your branch rules decide, so keep required reviews and checks on the branches you care about. ::: ### Turn auto-merge off No setting turns auto-merge off. It comes from `auto_merge = "squash"` on the publish step of `pr-delivery`. That is a sub workflow, not a workflow you can start or open by name. Each delivery workflow imports its own `pr-delivery` file from beside it. A separate `pr-delivery` of your own is never used. Instead, take your own copy of each delivery workflow you start, and delete `auto_merge = "squash"` from that copy's `pr-delivery`. Every shipped workflow except `explain-repo` delivers through `pr-delivery`. `implement-ticket` and `implement-epic` reach it through `deliver-ticket`. To do it in the app: 1. **Open the workflow.** Open **Workflows** and open the shipped workflow you use, for example `pursue-goal`. 2. **Find the delivery file.** Open the **Prompt docs** tab and select the `pr-delivery` file. It shows "Imported by delivery", or "Imported by a nested workflow" for `implement-ticket` and `implement-epic`. 3. **Remove the key.** Delete `auto_merge = "squash"` from the `publish` step, then save. Saving stores your own copy, with its imported files, under the same name. The list shows it as Custom with "Hides Shipped". **Revert** brings the shipped one back. To do it with files, copy the workflow's file, with the files it imports and its `prompts/` folder, into `~/.orbital/workflows/` or the project's `.orbital/` folder. Keep every relative path. Delete `auto_merge = "squash"` from the copy's `pr-delivery` file. A workflow there wins over a shipped workflow of the same name. The [Ticket to merge](/examples/workflows/ticket/) example holds the same delivery files. [`auto_merge`](/reference/workflow-file/#auto_merge) describes the key. ### When GitHub never runs a check Sometimes GitHub does not run a required check at all, for example because of billing or no runner. The run is then Paused on the probe step, with GitHub's reason and the check names. See [standings](/runs/#standings). Fix the cause on GitHub, then choose **Resume**. ### Several repositories A project with several repository folders gets one pull request per repository that changed. The run treats the set as one delivery. It merges only when every pull request merged, and it repairs the worst state first. The dock's **Pull requests** section lists them all. ![The dock's Pull requests list shows open and merged pull requests, each with its branch.](/screenshots/pull-requests-panel.webp?v=612ac483b1) *Every pull request of the run, with its state.* ## The GitHub token Delivery reads pull requests, checks and review threads through a GitHub token. Orbital tries these in order and uses the first it finds: 1. the token saved in **Settings > GitHub** 2. `GH_TOKEN` 3. `GITHUB_TOKEN` 4. an existing `gh auth login` The page shows which one is in use. See [Settings](/reference/settings/#github). ![Settings, GitHub page shows a saved token and the token sources in order: saved token, GH_TOKEN, GITHUB_TOKEN and gh auth login.](/screenshots/settings-github.webp?v=f898ede9d3) *The page names the token source in use.* ## Related - [Add gates and waits](/workflows/gates-and-waits/): how the run waits for a pull request - [Implement a feature](/examples/feature-pull-request/): a delivery workflow from goal to merge - [Ticket to merge](/examples/workflows/ticket/): the delivery sub workflow file by file - [Troubleshooting](/troubleshooting/#a-worktree-remains-after-cleanup): what to do when a worktree remains --- # Switch a run's harness > Retry a run's current step on another harness, by hand or automatically Move a run to another [harness](/harnesses/) part way through when its harness runs out of quota, is missing, or does a step badly. Open the run's **Run actions** menu (⋯) and choose **Retry with another harness…**. The current step starts again on the new harness, and the rest of the run stays as it is. ## Before you start - Install and sign in to the harness you want to move to. **Settings > Harnesses** shows its status. ## Switch the harness 1. **Stop the step.** If the step is still working, choose **Pause** and wait until the step stops. A run that has already stopped needs no pause. 2. **Open the dialog.** Open the **Run actions** menu (⋯) and choose **Retry with another harness…**. 3. **Pick the harness and retry.** Pick the new harness under **Harness**, then choose **Change harness and retry**. ![The Retry with another harness dialog with a Harness picker and a Change harness and retry button.](/screenshots/change-harness-dialog.webp?v=ddbef0b3da) *Pick the harness, then retry.* ## What changes and what stays The current step starts again on the new harness, in a fresh session. The new harness then does every later step of the run too, even a step whose style names another harness. The switch removes `harness`, `model`, `effort`, `connection` and `provider` from every layer a step's settings come from: the step's own keys, its styles, its groups' styles, the workflow's top-level `style` and the run's preset. Harness sub-tables, `persona` and `permissions` stay. | Part of the run | After the switch | | --- | --- | | Each step's persona and permissions | Kept. | | Settings made for the new harness in a sub-table, such as `codex.model` and `codex.effort` | Kept. | | A harness, model, effort, connection or provider set on a step or in a style | Dropped. Unless a sub-table for the new harness sets them, it uses its own default model and effort, or the **Default model** and **Default effort** you set for it. | | Finished steps, the run's context and history | Kept as they are. | | Worktrees and file changes | Kept as they are. | | The old harness's session | Does not carry over. | :::caution[Retrying can repeat work] Retrying the step can repeat what it already did outside Orbital, such as a comment it posted. Orbital does not undo it. ::: ## Switch from a coding agent A coding agent can do the same with the [`control_run`](/reference/mcp-tools/#control_run) MCP tool and the `ChangeHarness` action. That action takes an optional `model`, but Orbital refuses the call when you give one. It changes a run's harness, not its model. To move a run onto a [preset](/reference/settings/#presets) instead, use the `ChangePreset` action with a `preset`. It pins the run to that preset and retries the step. The retried step and every step after it run each role on the preset's harness, model and effort, as a run started with that preset would. `ChangePreset` is allowed whenever `ChangeHarness` is. A preset that does not exist is refused, and the run is left as it was. ## Quota fallbacks A quota fallback lets [the delivery lead](/delivery-lead/) move a run for you when its harness [runs out of quota](/harnesses/#when-a-harness-runs-out-of-quota). A fallback names a preset. Set it under "When a harness runs out of quota" in **Settings > Harnesses**: **Move to preset** names the preset to move to. The delivery lead settings show the same list as **Quota fallbacks**, with **Move a run to**. A change in one place changes the other. The delivery lead applies a fallback with `ChangePreset`: it pins the run to the fallback's preset and retries the step, so each role runs on that preset's harness, model and effort. It moves a run only to a preset in that list whose harness can take it. It does so only when its **Switch a run's harness** permission allows it. That permission is **Do it** unless you change it. Without the delivery lead, Orbital never moves a run by itself. Earlier versions saved fallbacks as harnesses. When Orbital starts, it converts each one to the first preset on that harness. A fallback no preset runs on is dropped. **Settings > Recent changes** records the conversion. ## Related - [Harnesses and models](/harnesses/): which harness a step uses and what each status means - [Pause, stop or resolve a run](/runs/control-a-run/): pause a step before you switch - [Delivery lead](/delivery-lead/): the agent that applies quota fallbacks - [MCP tools](/reference/mcp-tools/#control_run): the `ChangeHarness` and `ChangePreset` actions --- # Use different models > Run different steps on different harnesses, models and efforts Run cheap steps on a small model and expensive steps on a large one by giving each step its own style. Define the styles in the workflow, or use the ones in **Settings > Presets**, then name one per step: ```toml [styles.codex_review] harness = "codex" model = "gpt-6-sol" effort = "high" [steps.implement] style = "balanced" prompt = "Implement this goal: {{ inputs.goal }}" next = "steps.review" [steps.review] style = "codex_review" prompt = "Review the change. Report what you find. Do not change files." next = "steps.done" ``` `balanced` ships with Orbital and names no harness, so `implement` runs on the harness of the run's preset. `review` always runs on Codex. [Styles and personas](/styles/) defines styles, and [Give steps a style](/styles/configure-steps/) walks through a complete example. Orbital works out the harness, model, effort, connection and provider separately for every step. So a step that changes only its effort keeps the model it would have had. ## Before you start - Sign in to each harness you want to use. Follow [Install Orbital](/get-started/install/#2-install-and-sign-in-to-a-harness) before you try a model. ## Name models in styles A style names a model with `harness` and `model`, and how hard it thinks with `effort`. Put styles in the workflow, in a style file that several workflows `use`, or in **Settings > Presets**, where every workflow can name them. A style in Settings wins over a file style of the same name. The harness names are claude, codex, opencode and pi. Effort accepts minimal, low, medium, high, xhigh, max and ultra, but each harness accepts only some of them. Orbital refuses a harness and effort that do not go together before any turn starts, so a run does not fail halfway through. ## Pick a preset for the run **Settings > Presets** marks one preset as in use. On a fresh install it is the shipped `claude`; Orbital also ships `codex`. A run started without a preset follows the preset in use: switching the preset in use moves the run at its next turn. Choose another style in the Start page's preset picker, or pass `style` to `start_run`, to pin the run to it. A later switch of the preset in use then does not move the run. The run's preset comes after everything the workflow sets, so a step's own keys and styles, its groups' styles and the workflow's `style` still come first. [Which harness a step uses](/harnesses/#which-harness-a-step-uses) explains which steps follow it. When the harness of the preset in use runs out of quota, a banner offers to switch the preset in use. Orbital never switches it by itself. To start a run on a harness by name, use `--harness` with `orbital run start`, or `harness` and `model` in `start_run` or the API. A harness named this way removes `harness`, `model`, `effort`, `connection` and `provider` from every layer, so every step runs on it, as when you [switch a run's harness](/harnesses/switch-harness/). Harness sub-tables, personas and permissions stay. A model named this way becomes the run's model on its harness: a sub-table model on the step, in its styles or in its groups' styles still wins over it, and the workflow's own `style` does not. [Models and prices](/reference/models/) lists every suggested model and its price, and explains how Orbital estimates a run's cost. ## Resolution order Orbital decides each setting on its own, from the first of these that sets it: 1. the step's own keys 2. the step's `style`, where later names in a list win 3. the styles of the groups that hold the step, the innermost first; an import step is a group for the steps it brings in 4. the workflow's top-level `style` 5. the run's preset: the one it was started with, or else the preset in use 6. the defaults in **Settings > Harnesses** A key in a harness sub-table, such as `codex.model`, applies only when that harness runs the step, and there it beats the plain `model` beside it. [Workflow file](/reference/workflow-file/#how-a-steps-settings-resolve) gives the exact order. Sub-table keys such as `claude.model`, `codex.effort` and `opencode.provider` work in styles and on agent steps. Orbital checks their names and values even when the step runs on another harness. So a typo shows up when you validate the workflow, not on the turn that needs it. [Harness-specific keys](/reference/workflow-file/#harness-specific-keys) lists them all. An imported workflow's styles join the styles of the workflow that imports it, and the importer's own styles of the same name win. The import step's `style` reaches every step it brings in. See [Reuse a workflow](/workflows/reuse-a-workflow/). :::note Permissions default to auto-accept. Each harness treats `permissions` differently. Neither permissions nor the prompt limits what a step can do. [Permissions](/styles/#permissions) explains them. ::: ## See which model a run used The run page names the harness and the model the latest turn reported. They appear in the footer under the composer, beside the workflow, the branch and the pull request. Where the composer is not on screen, such as on an ended run, the header names them instead. Before a turn reports a model, the page shows the model the turn asked for, marked "(configured)", or "Model not known yet". A run that used several models shows the latest with a count of the others. The transcript records the settings each turn ran with. It is the fastest way to check that your settings did what you meant. The **Tokens by model** table on the run's **Cost** tab lists each model the run used. A step whose spend ran on several models, such as Claude's main model with its auxiliary Haiku calls, names every one, and **Tokens by model** splits the step's spend between them. A cost marked ≥ is the part Orbital could price, and a note under the table says what is missing: - Claude reports no usage for the helper calls it makes outside its query pipeline, such as the permission classifier and token-count probes. Its SDK leaves them out of every report it gives, so no Claude step's cost is complete. - Claude reports sub-agent and auxiliary spend only as running totals for its session. Orbital records those totals with the run, so a session the run resumes, after a restart or in a paused conversation, counts on from them. When it cannot tell what the totals counted from, such as after they reset or after a model call was cut off mid-reply, it records the main loop's own calls and leaves the rest out rather than count spend twice. ![A run's Cost tab shows tokens and cost for each step and a Tokens by model table.](/screenshots/run-billing.webp?v=01a803de81) *Tokens by model lists every model the run used.* ## Change harness during a run You can move a stopped run to another harness and retry its step there. [Switch a run's harness](/harnesses/switch-harness/) describes how, what carries over, and what the quota fallback does. ## Related - [Give steps a style](/styles/configure-steps/): a worked example with step, group and workflow styles - [Styles and personas](/styles/): what a style holds and what ships - [Harnesses and models](/harnesses/): which harness a step uses - [Switch a run's harness](/harnesses/switch-harness/): move a stopped run to another harness - [Models and prices](/reference/models/): suggested models and how cost is estimated - [Workflow file](/reference/workflow-file/#how-a-steps-settings-resolve): the exact resolution order --- # Give steps a style > Choose each step's harness, model, effort and persona with styles Give a step a style to decide in one name which harness runs it, which model it uses, how hard it thinks and which persona it speaks as. Set `style` on the step, on a group of steps, or at the top of the workflow for every step. This guide walks through one plan, implement and review workflow that uses each of them. ![plan leads to coder.implement, coder.implement leads to coder.check, coder.check leads to review, and review reaches done.](/styles.svg?v=061c1b9c45) ```toml title="styles.toml" version = 3 entry = "steps.plan" use = ["shared/team.toml"] style = "balanced" [styles.careful_review] extends = "reviewer" effort = "max" codex.sandbox_mode = "read-only" [steps.plan] style = "planner" prompt = "Plan a small change that meets this goal: {{ inputs.goal }}. Do not change files." outputs.plan = "text" next = "steps.coder.implement" [steps.coder] style = ["implementer", "codex"] [steps.coder.implement] prompt = "Implement this plan: {{ context.plan }}" next = "steps.coder.check" [steps.coder.check] effort = "low" prompt = "Run the project's checks and fix what they find." next = "steps.review" [steps.review] style = ["careful_review", "thorough"] prompt = "Review the change against this plan: {{ context.plan }}. Report what you find. Do not change files." next = "steps.done" [steps.done] kind = "terminal" ``` ## Before you start - Know how to write a workflow. See [Write your first workflow](/workflows/write-a-workflow/). - Know what styles and personas are. [Styles and personas](/styles/) defines them. ## Set a base for every step The top-level `style` applies to every step that does not choose otherwise: ```toml style = "balanced" ``` `balanced` ships with Orbital and sets `effort = "medium"`. Put the settings every step shares here, such as `permissions`, in a style of your own and name it at the top. ## Give a step a style Only steps that take a turn use styles: agent steps, a fan-in with a prompt, and the `write` of an action. A command, a wait or a terminal does not. ```toml [steps.plan] style = "planner" prompt = "Plan a small change that meets this goal: {{ inputs.goal }}. Do not change files." ``` `plan` uses the shipped `planner` style, so it speaks as the `planner` persona and thinks at `high`. Its style beats the workflow's `balanced`. ## Give a group a style A table under `steps` that holds steps is a group. A `style` on the group reaches every step inside it: ```toml [steps.coder] style = ["implementer", "codex"] ``` `coder.implement` and `coder.check` both run with the `implementer` persona, on Codex with `gpt-6`, because the `codex` style says so. The list combines two styles, and a later name wins where both set the same key. ## Override one key on a step A key set on the step itself beats every style: ```toml [steps.coder.check] effort = "low" prompt = "Run the project's checks and fix what they find." ``` `coder.check` thinks at `low`. It keeps the persona, harness and model it takes from its group. ## Combine styles in a list ```toml [steps.review] style = ["careful_review", "thorough"] ``` `careful_review` extends the shipped `reviewer`, so `review` speaks as `strict_reviewer`. `thorough` comes later in the list, so its `effort = "high"` wins over the `max` in `careful_review`. ## Set something for one harness only ```toml [styles.careful_review] extends = "reviewer" effort = "max" codex.sandbox_mode = "read-only" ``` `codex.sandbox_mode` applies only when Codex runs the step. `review` names no harness, so it runs on the harness of the run's preset: the preset you picked on the Start page, or else the preset in use. On Codex it runs in a read-only sandbox and cannot write to the repository. On any other harness the line does nothing. The `claude.*`, `opencode.*` and `pi.*` sub-tables work the same way, and so does a plugin harness's sub-table of its own name. ## Share styles between workflows `use` pulls in style files, with paths relative to the workflow file: ```toml use = ["shared/team.toml"] ``` ```toml title="shared/team.toml" version = 3 [styles.thorough] effort = "high" [styles.codex] harness = "codex" model = "gpt-6" codex.sandbox_mode = "workspace-write" ``` `thorough` comes from this file. Orbital also ships a style called `codex`, which sets only `harness = "codex"`. The app's style wins on the key it sets, so the file's `model` and `codex.sandbox_mode` still merge in. A style the workflow defines itself beats a style of the same name from a file it uses. ## Change a style for every workflow Presets in **Settings > Presets** win over styles of the same name in files. Edit the shipped `reviewer` there, and every workflow that names it follows, including through `careful_review`, which extends it. To change a shipped preset for one workflow only, extend it under a new name in that workflow, as `careful_review` does. A copy called `reviewer` in the file would lose to your Settings. 1. **Open Settings > Presets.** Open **Settings** from the sidebar and choose **Styles**. 2. **Edit the style.** Select a style and change its settings. 3. **Undo it if you need to.** **Reset to default** returns an edited shipped preset to Orbital's version. Deleting a shipped preset hides it until you choose **Restore**. ## Choose the run's preset Steps that name no harness, such as `plan` and `review`, run on the harness of the run's preset. A run started without a preset follows the preset in use in **Settings > Presets**, which is `claude` on a fresh install. To run one run on another style, pick it in the Start page's preset picker, or pass `style` to `start_run`. That pins the run to it. The run's preset comes after everything the workflow sets, so the `coder` group still runs on Codex. ## Check what a step resolved to Select a step in the workflow editor. The inspector sums up the settings the step resolves to, such as its persona, harness, model and effort, and says where each comes from: the step itself, one of its styles, a group that holds it, or the workflow. Select a part to open its definition. Once a run has started, the run page shows the settings each step ran with, and the transcript records the settings of every turn. It is the fastest way to check that your styles did what you meant. ## Try the example The `coder` group runs on Codex, so sign in to Codex first. Codex refuses to start in a folder that is not a Git repository, so pick a practice project that is one. See [plain folders and Git](/projects/#plain-folders-and-git). 1. **Download the files.** [Download the files](/styles.zip). 2. **Unpack them.** Unpack the download into your workflow library, `~/.orbital/workflows/`. Keep every relative path, so the workflow finds `shared/team.toml`. 3. **Check the workflow.** Run `orbital validate ~/.orbital/workflows/styles.toml`. Open the `styles` workflow in the editor and select each step to see what it resolves to. Then choose **New task**, pick your practice project, choose the `styles` workflow and write a goal. :::caution[This example changes your folder] `coder.implement` changes files in the project's folder itself, because the workflow makes no worktree. Pick a practice project, not one you care about. ::: ## Expected behaviour `plan` writes a plan without changing files. `coder.implement` and `coder.check` run on Codex and change the files. `review` reports what it finds without changing files. The run page shows the settings each step ran with. ## Complete source [Download styles.toml](/examples/styles/styles.toml) and [shared/team.toml](/examples/styles/shared/team.toml), or [all the files](/styles.zip). [Workflow file](/reference/workflow-file/#styles) describes every key in these files. ## Quick reference | To | Do this | | --- | --- | | Give every step a style | Set `style` at the top of the workflow. | | Give a group of steps a style | Set `style` on the group's table. | | Give one step a style | Set `style` on the step. | | Combine styles | Give `style` a list. Later names win. | | Change one setting of a step | Set the key on the step, such as `effort = "low"`. | | Set something for one harness only | Use a sub-table key, such as `codex.sandbox_mode`. | | Share styles between workflows | Put them in a style file and list it in `use`. | | Change a preset for every workflow | Edit it in **Settings > Presets**. | | Run one run on another preset | Pick it in the Start page's preset picker. | | Change a shipped preset for one workflow | Extend it under a new name with `extends`. | ## Related - [Styles and personas](/styles/): what a style holds and how styles cascade - [Use different models](/harnesses/use-different-models/): run steps on different harnesses and models - [Reuse a workflow](/workflows/reuse-a-workflow/): how import steps pass styles to the steps inside - [Workflow file](/reference/workflow-file/#how-a-steps-settings-resolve): the exact resolution order --- # Settings > Where Settings is and what each of its sections is for Change how Orbital runs, harnesses, styles, connected services and the app itself work in Settings. Open **Settings** from the sidebar, then pick a section from the list on the left. Every value saves as soon as you finish with it. A text field saves when you press Enter or leave the field. A **Reset** button appears next to a label whose value you changed from its default. A line under a field says when the change applies, for example "Applies to runs started afterwards." ![Settings, General page with Appearance set to match the computer, light or dark, followed by concurrent runs, default workflow, default harness, stalled turn limit and open worktrees in.](/screenshots/settings-general.webp?v=cb0897c671) *General holds the appearance and the settings for runs, worktrees and the server.* ## The sections | Section | What it is for | | --- | --- | | [General](/reference/settings/#general) | Appearance on this device, and the settings for runs, worktrees and the server. | | [Harnesses](/reference/settings/#harnesses) | Whether each harness can start, how Orbital starts it, and where a run goes when its harness runs out of quota. | | [Delivery lead](/reference/settings/#delivery-lead) | How the delivery lead looks after your runs. It appears only while the delivery lead experimental feature is on. | | [Presets](/reference/settings/#presets) | Named sets of turn settings, such as harness, model, effort and persona, that every workflow can name, and the preset in use that runs follow. | | [Personas](/reference/settings/#personas) | Named prompts that tell an agent who it is. Styles pick one with `persona`. | | [Decision models](/settings/decision-models/) | The models that decide steps and deciding fan-outs ask, such as Jev, Clef, Laya and Kev, with their keys and thresholds. | | [GitHub](/reference/settings/#github) | The GitHub token Orbital uses for its GitHub requests, and where it looks for one. | | [Linear](/reference/settings/#linear) | The Linear key that Linear trigger rules use to check Linear. | | [MCP](/reference/settings/#mcp) | The MCP endpoint that lets coding agents outside Orbital read and control your runs. | | [Costs](/reference/settings/#costs) | The prices, in USD per million tokens, that Orbital uses to estimate what runs cost. | | [Command line and skills](/reference/settings/#command-line-and-skills) | Installs the `orbital` command and Orbital's agent skills. | | [Experimental features](/reference/settings/#experimental-features) | Switches for features still being tried out. Each is off until you turn it on. | | [Recent changes](/reference/settings/#recent-changes) | The newest changes to your settings, with who or what made each one and when. | | [About](/reference/settings/#about) | The version that is running, its updates, where your settings are kept, and the licenses of the open source packages Orbital ships. | ## Harnesses **Settings > Harnesses** shows whether each [harness](/harnesses/) is installed and signed in. Below the status table, each harness has its own settings, such as its program and default model. ![Settings, Harnesses page with a status table showing whether Claude, Codex and OpenCode are installed and signed in, followed by Claude's settings for turned on, program, default model, default effort and environment variables.](/screenshots/settings-harnesses.webp?v=6446e71ff0) *Check here first when a run cannot start its harness.* ## GitHub **Settings > GitHub** saves a token for Orbital's GitHub requests. It also lists the places Orbital looks for a token, in order. The first source with a token is used. ![Settings, GitHub page showing a saved token and the token sources in order: saved token, GH_TOKEN, GITHUB_TOKEN and gh auth login.](/screenshots/settings-github.webp?v=f898ede9d3) *The token sources list shows which token Orbital uses.* ## Related - [Settings reference](/reference/settings/): every field, its default and its allowed values - [Harnesses and models](/harnesses/): how Orbital picks and starts each harness - [Styles and personas](/styles/): what the Styles and Personas sections do for steps - [Connect a coding agent](/mcp/connect/): use the MCP section to register a client - [Troubleshooting](/troubleshooting/): fixes that name each switch in Settings --- # Set up decision models > Add Jev, Clef, Laya or Kev in Settings so decide steps can ask them Add a decision model so [decide steps](/workflows/decide-with-a-model/) and deciding fan-outs have something to ask. Until one is set up, every decision takes its default path. Open **Settings > Decision models**. The page lists the models you have, whether each has a key saved, and its threshold. Choose **Add a model**, pick a preset under **Start from**, fill in what it asks for and choose **Add model**. Every model has these fields. | Field | What it does | Default and values | | --- | --- | --- | | Name | What a step's `decider` calls the model. | Lowercase letters, digits and hyphens. It cannot change after you add the model. | | Model | The model id the provider is asked for. | Filled in by the preset. | | Base URL | Where the provider is reached. Cloudflare asks for its account id instead. | Filled in by the preset, or blank when you must say. | | Threshold | The confidence a step needs from the model when it sets no `threshold` of its own. | 0.5. A number from 0 to 1. | | API key | The key the provider asks for. It is kept in the secret store, never shown again and never put on a command line or in a log. Leave it blank when you edit to keep the saved one. | None. Self-hosted models may need none. | The first model you add becomes the default, the one a step uses when it names no `decider`. **Make default** moves it. Removing a model removes its key; steps that name it then take their default path. Choose **Test** on a model to ask it a sample question. The page shows its answer and confidence, or why it failed. Orbital refuses to save a model whose address, account or key does not set its provider up. ## The four models | Preset | Where it runs | Fill in | Notes | | --- | --- | --- | --- | | Jev | Hosted by Typesafe | Your key | 32k context, $0.042 per million input tokens. Reached at `https://api.typesafe.ai/v1`. **Jev via OpenRouter** reaches it through your OpenRouter key instead. | | Clef, Clef-flash | Cloudflare Workers AI | Your Cloudflare account id and an API token | 64k context, reads images, $0.24 per million tokens. The model is `clef` or `clef-flash`. | | Laya | Your machine | The address of `laya-serve`, which defaults to `http://127.0.0.1:20000/v1` | Install it with `pip install laya[serve]`. 421M parameters, 512-token context, runs on a CPU. | | Kev | Your machine or a server of yours | The address of `kev.serve` | 0.8B to 27B parameters, 8k context assumed. | **Custom System One** reaches any other server that speaks System One, such as LiteLLM. Orbital assumes an 8k context for it. Jev, Laya, Kev and a custom server speak System One over HTTP: Orbital sends `POST {base URL}/systemone` with the model, the state and the questions, and sends the key as a bearer token when one is saved. Clef speaks the same questions through Cloudflare's Workers AI. A request that gets no answer within ten seconds counts as unreachable, and the step takes its default path. ## Related - [Decide with a model](/workflows/decide-with-a-model/): write the steps that ask. - [Settings reference](/reference/settings/#decision-models): every field of the page. --- # Run on your other computers > Pair a worker, run Runs in its environment, and revoke, restart or recover it A worker is another of your own computers that runs Runs in its environment, while one Orbital server, the control plane, keeps every Run's history and drives its Workflow. Without a worker nothing changes: every Run executes on the control plane's own computer, with no network in between. Each worker needs Orbital installed, the repositories and project folders its Runs work in, and its own signed-in harnesses. Provider logins stay on the worker and never cross the link. ## Pair a worker 1. Start the control plane with a port for workers to link to: ```sh orbital serve --bind 0.0.0.0 --worker-port 4400 ``` Without `--worker-port` it listens for no worker. 2. Open **Settings > Workers**, give the worker a name, such as `attic`, and choose **Pair worker**. The control plane and secret are shown once. 3. On the worker, save them as a pairing file only you can read, such as `~/.orbital/attic.json`: ```json { "worker": "attic", "controlPlane": "homelab", "secret": "the secret shown" } ``` ```sh chmod 600 ~/.orbital/attic.json ``` 4. Start Orbital on the worker, naming the control plane's address and worker port: ```sh orbital serve --works-for homelab.local:4400 --pairing ~/.orbital/attic.json ``` The worker appears as connected in **Settings > Workers**, with its environment and the harnesses it can run. Choose it as a Project's default worker, or for one Run, and the Run executes there. The link is encrypted with TLS 1.3. The key is derived from the worker's own secret, so nobody without the pairing file can connect as the worker or read what it carries. The control plane keeps only a digest of the secret. Desktops on other computers connect with their own credential, which `orbital client add ` issues and `orbital client revoke ` withdraws. ## Revoke a worker Choose **Revoke** beside the worker. Its link closes at once and every later connection is refused, so the worker stops. Runs already placed on it keep their history and environment, and revoking does not prove its processes stopped. **Pair** it again to issue a new pairing file; the old one stays refused. To stop new Runs going to a worker while its current ones finish, choose **Drain** instead. ## Restart a worker Stop the worker's `orbital serve` and start it again with the same pairing file. It reconnects under the same name, and the control plane carries on the operations it had already asked for: one that finished is answered from its recorded result rather than run again, and one whose outcome the worker cannot know is reported as uncertain instead of repeated. Only one process may hold a pairing at a time. When a second one connects, the first is superseded and stops. ## When the control plane restarts or the network drops A worker finishes the operations it already accepted and then waits. It dials the control plane again every second until it is back, with no step from you. The control plane keeps its pairings in `~/.orbital/workers.json`, so workers rejoin a restarted control plane with their existing pairing files, and each Run carries on in the environment it recorded. Both ends ask the operating system to probe a link that has been quiet for 15 seconds, so a computer that vanished without closing its connection is noticed. How soon depends on the operating system's keepalive settings: on Linux, by default, about eleven minutes after the probes start. Until then the worker shows as connected, and its Runs wait rather than fail. --- # Connect a coding agent > Register Claude Code, Codex, OpenCode or any MCP client with Orbital Connect Claude Code, Codex, OpenCode or another MCP client to Orbital, so the agent can list, start, follow, message and control your runs from a conversation. Copy your client's setup from **Settings > MCP** and run it in a terminal. With your address and token in place of `` and ``, it looks like this: **Claude Code** ```sh claude mcp remove --scope user orbital >/dev/null 2>&1; claude mcp add --transport http --scope user orbital '' --header 'Authorization: Bearer ' ``` **Codex** ```sh codex mcp add orbital --url '' && printf '%s\n' '' '[mcp_servers.orbital.http_headers]' 'Authorization = "Bearer "' >> ~/.codex/config.toml ``` This adds the server, then adds the token to `~/.codex/config.toml`: ```toml [mcp_servers.orbital.http_headers] Authorization = "Bearer " ``` :::caution The command appends this table each time you paste it. A file with the table twice is not valid. Before you paste it again, for example after rotating the token, delete the old `[mcp_servers.orbital.http_headers]` table from `~/.codex/config.toml`. ::: **OpenCode** ```sh opencode mcp add orbital --url '' --header 'Authorization=Bearer ' ``` **Other client** Any client that speaks Streamable HTTP works. Give it the address and one header: ```text URL: Authorization: Bearer ``` Each setup registers Orbital for your user, not one folder. It replaces an earlier Orbital entry, so pasting it again leaves one entry. The Codex setup is the exception, as its tab says. MCP, the Model Context Protocol, is how coding agents connect to tools. Orbital serves MCP over Streamable HTTP, on the same address as the app, at `/mcp`. A connected agent works with your runs the same way you do in the app. It can also [lead delivery](/delivery-lead/lead-delivery/) through the [`orbital-delivery-lead` skill](/mcp/skills/#orbital-delivery-lead). ## Connect a client 1. **Turn the endpoint on.** Open **Settings > MCP**. Check that **Turned on** is on. It is on unless you turned it off. **Status** should say "Listening". ![Settings, MCP page with the endpoint turned on, its status Listening, the endpoint address and token, and a Copy setup button for each client.](/screenshots/settings-mcp.webp?v=ab5e9e42f5) *Check that the endpoint is turned on and listening.* 2. **Find the address and the token.** Under **Connection**, find the **Endpoint address**, such as `http://127.0.0.1:42121/mcp`, and the **Token**. Choose **Reveal** to see the token and **Copy** to copy it. Every call must carry the token as `Authorization: Bearer `. That holds even for `orbital serve`, whose app needs no login. 3. **Copy your client's setup.** Under **Register a client**, Settings shows one ready-made setup for each client, with your address and token already in it. Choose **Copy setup** for your client. 4. **Paste it into a terminal.** Paste the setup into a terminal and run it. Then start a new session of that agent. 5. **Check that it works.** In the new session, ask `List my Orbital projects.` The agent calls `list_projects` and answers with your projects and their folders. If the agent cannot see the tool, check three things. The endpoint is on. The address matches the one in Settings. You started a new session after registering. [Troubleshooting](/troubleshooting/#an-mcp-client-cannot-reach-orbital) has more fixes. ## Choose what agents may do **Settings > MCP** has a switch for each tool group. A tool in a group that is off answers that the group is off in Settings. | Tool group | Tools | Default | | --- | --- | --- | | Reading runs and workflows | `list_runs`, `get_run`, `list_projects`, `read_run_thread`, `read_run_history`, `read_run_log`, `export_run`, `list_workflows`, `read_workflow`, `validate_workflow`, `list_dot_workflows`, `wait_for_run`, `read_pull_request`, `read_run_worktree` | On | | Starting runs | `start_run`, `convert_dot_workflows` | On | | Controlling runs | `control_run`, `revise_goal`, `secure_run_work` | On | | Messaging and answering runs | `message_run`, `answer_question` | On | | Archiving | `archive_run`, `resolve_run` | Off | | Changing Orbital's settings | `change_setting` | Off | :::caution With the defaults, a connected agent can start, pause, retry, restart and abort your runs, and message them. Turn off a group you do not want agents to use. ::: Turn on **Archiving** if you want an agent to tidy up finished runs. Turn on **Changing Orbital's settings** if an agent should change settings. [MCP tools](/reference/mcp-tools/) describes every tool, its arguments and when to use it. ## Which tool for which job | You want the agent to | It uses | | --- | --- | | Find a project and its workflows | `list_projects`, `list_workflows` | | Start a run | `start_run`, with a project, a goal and optionally a workflow and its inputs | | See how a run is doing | `get_run`, then `read_run_thread` or `read_run_history` for detail | | Wait for a run to change | `wait_for_run`, which returns when the standing changes, the agent asks a question, or the run ends | | Find out why a step failed | `read_run_log` | | Steer the step at work | `message_run` with delivery `Resume` | | Leave a note for the next step | `message_run` with delivery `Hold` | | Change what a run should achieve | `revise_goal` | | Answer a run's question | `answer_question` | | Pause, resume, retry, jump, [switch harness](/harnesses/switch-harness/), restart or abort | `control_run` with the position from `get_run` | | Check a run's pull request: its checks, reviews and merge state | `read_pull_request` | | See a run's uncommitted changes and commits ahead | `read_run_worktree` | | Save a stopped run's work before replacing it | `secure_run_work`, which commits on the run's branch and pushes without force | | Mark a failed run as dealt with, or archive a finished one | `resolve_run`, `archive_run` | Run content, such as the thread and the log, came from agents. An agent reading it treats it as data, not as instructions. ## The address and the token | Topic | What to know | | --- | --- | | The port | The Mac app serves on port 42121 at every launch, so a registration keeps working after you quit and reopen it. When another program, or an Orbital server on another home, holds that port, the app starts on a free port and **Settings > MCP** says so. Update each registration with the address it shows. A second Orbital server on the same home refuses to start and names the one already running. | | The host rule | When Orbital listens only on your own machine, which is the default, the endpoint answers only to the names `localhost`, `127.0.0.1` and `[::1]`. When you start the server with `--bind` on a network address, it answers to any host name, and the token is still required. It always refuses a change that another web site asks for. | | Where the token is kept | The token is kept encrypted in Orbital's secret store, never in a file. It stays the same across restarts until you regenerate it. A token an earlier version kept in `~/.orbital/mcp-token` moves into the store unchanged, so your clients keep working. | ### Rotate the token Choose **Regenerate** next to the token in **Settings > MCP**. Then copy each client's setup again and paste it, which replaces the old entry. :::caution The old token stops working at once. Every client you registered with it loses access until you paste its new setup. ::: ## The delivery lead's calls The built-in [delivery lead](/delivery-lead/) uses the same tools, but under its own rules instead of the tool group switches. It must give a reason with every action. It acts only on runs it owns. Each action follows its **What it may do** setting. See [how a delivery lead's calls are scoped](/reference/mcp-tools/#how-a-delivery-leads-calls-are-scoped). Orbital attaches its own server to agent turns as `orbital_app`. Keep the `orbital` name in your client configuration above. The two names let your configured connection and the turn-specific connection coexist. ## Related - [MCP tools](/reference/mcp-tools/): every tool, its arguments and its results - [Use the shipped skills](/mcp/skills/): give a connected agent Orbital's delivery lead and workflow designer skills - [Lead delivery](/delivery-lead/lead-delivery/): the loop a connected agent can follow to deliver tickets - [Troubleshooting](/troubleshooting/#an-mcp-client-cannot-reach-orbital): fixes when a client cannot reach Orbital --- # Use the shipped skills > Install Orbital's two agent skills and ask coding agents to use them Install Orbital's two skills so your coding agent can deliver tickets through Orbital runs and design workflows with you. From a terminal, run: ```sh orbital skill install ``` A skill is a folder of instructions an agent, such as Claude Code or Codex, loads when a task matches it. Orbital ships these two: | Skill | What it is for | When an agent uses it | | --- | --- | --- | | `orbital-delivery-lead` | Delivering tickets and epics through Orbital runs, as a delivery lead. | You ask it to deliver tickets with Orbital, to start or watch runs, or to act as delivery lead. | | `orbital-create-workflow` | Designing a new Orbital workflow with you, one decision at a time. | You ask it to turn a process into a workflow, or to design or change one. | ## Install them **App** 1. **Open the skills settings.** Open **Settings > Command line and skills**. 2. **Install each skill.** Under **Agent skills**, choose **Install** next to each skill. Choose **Update** when a newer version ships. ![Settings, Command line and skills page showing the orbital command installed and the agent skills installed.](/screenshots/settings-command-line.webp?v=2453b7527a) *Each skill shows whether it is installed and up to date.* **Command line** ```sh orbital skill install ``` The command installs every shipped skill and reports on each one: | Report | What it means | | --- | --- | | Installed | The skill was not there, and now is. | | Updated | The skill now matches the version this release ships. | | Already up to date | Nothing changed. | | Kept | Your copy differs, so Orbital left it alone. | | Replaced | With `--replace`, Orbital installed its own and names the backup of yours. | | Could not install | Something went wrong, and the report gives the reason. | The command exits with an error when a skill could not be installed. See [`orbital skill install`](/reference/command/#orbital-skill-install). ## Where they go Each skill is copied to `~/.agents/skills/` and linked from `~/.claude/skills/`. Claude Code finds it through the link. Other agents that read skills from `~/.agents/skills` find the copy. The skills work for every project on your machine. ## When a different copy is there Orbital never overwrites a copy you changed. When the installed copy, or the link, differs from the skill Orbital ships, Orbital keeps it and says so. In Settings the skill reads "A different copy with this name is installed. Orbital will keep it unless you replace it." To replace it, choose **Replace…** in Settings and confirm, or run: ```sh orbital skill install --replace orbital-delivery-lead ``` Orbital moves your copy to a dated backup under `~/.agents/skill-backups` or `~/.claude/skill-backups`, then installs its own. ## orbital-delivery-lead The delivery lead refines tickets until they are true and worth building, and starts Orbital runs on them. It checks in on the runs, unsticks them without losing work, and verifies and closes what they deliver. It uses Orbital's MCP tools, and `gh` and `git`. [Lead delivery](/delivery-lead/lead-delivery/) describes the loop it follows. The skill adds rules of its own for keeping delivery safe. It needs Orbital's MCP endpoint connected to the agent. See [Connect a coding agent](/mcp/connect/). Without MCP, it falls back to Orbital's [HTTP API](/reference/http-api/). :::caution The delivery lead starts runs and acts on them by itself, within what its MCP tool groups allow. Check the switches in **Settings > MCP** before you ask it to lead. ::: Ask it, for example: ```text Use the orbital-delivery-lead skill to deliver the open sub-issues of https://github.com/acme/shop/issues/400 with Orbital, one run per ticket. Refine each ticket against main first. Check in every 30 minutes. ``` It comes with goal templates for one ticket and for several tickets in order. Each template carries the rules a run's validate step needs: commit every round, check every "done when" item, and a blocker is never a pass. ## orbital-create-workflow The workflow designer turns a process you run by hand into an Orbital [workflow](/workflows/). It interviews you one decision at a time, with a recommendation for each. It covers the goal and inputs, the unit of work, whether the workflow ends or runs on, and what each step may do. It also covers parallel work, failures and retries, and waits. It draws the workflow as it grows and asks you to confirm the design. It asks you to approve each prompt before it writes it. Then it writes the files and validates them without running them. It carries the complete workflow format and worked examples, so it needs nothing else. Ask it, for example: ```text Use the orbital-create-workflow skill. Every Monday I read our open support tickets, group them by product area and write a summary for the team. Turn that into an Orbital workflow. ``` Check the result yourself before you run it. For a workflow saved in your library as `.toml`: ```sh orbital validate ~/.orbital/workflows/.toml ``` Save the finished workflow in a repository's `.orbital/` folder or in your library, `~/.orbital/workflows/`. It then appears in the new-run menu. [Workflows](/workflows/) explains where Orbital looks. ## Related - [Connect a coding agent](/mcp/connect/): give the delivery lead the tools it needs - [Command line](/command-line/): install skills and validate workflows from a terminal - [Settings reference](/reference/settings/#command-line-and-skills): the fields in **Command line and skills** - [Write your first workflow](/workflows/write-a-workflow/): check what the workflow designer writes --- # The orbital command > Run Orbital, validate workflows and start runs from a terminal Use the `orbital` command to run Orbital from a terminal. For example, start the server and the browser app, then check a workflow without running it: ```sh orbital serve orbital validate ~/.orbital/workflows/.toml ``` The command also opens Orbital in a window of its own, starts runs and changes settings on a running server, and installs the shipped [skills](/mcp/skills/). Run `orbital` with no arguments to print its help. ## Before you start The Mac app carries the command. Install it from **Settings > Command line and skills**, or with **Orbital > Install Command Line Tool** in the menu bar. On Linux or Windows, install it with npm. [Install Orbital](/get-started/install/#1-install-orbital) gives both routes. ![Settings, Command line and skills page showing the orbital command installed and the agent skills installed.](/screenshots/settings-command-line.webp?v=2453b7527a) *The orbital command shows as installed once the link is in place.* ## The subcommands | Subcommand | What it does | | --- | --- | | [`orbital serve`](/reference/command/#orbital-serve) | Runs the server and the browser app on one port, prints its address and opens it in your browser. | | [`orbital app`](/reference/command/#orbital-app) | Opens Orbital in a window that runs its own server. Closing the window stops the server. | | [`orbital validate`](/reference/command/#orbital-validate) | Checks a workflow without running it. It starts neither the server nor an agent. | | [`orbital run start`](/reference/command/#orbital-run-start) | Queues a run on the server running on this machine, the desktop app's or `orbital serve`'s, and prints the run's ID and page address. | | [`orbital settings set`](/reference/command/#orbital-settings-set) | Changes one setting on the server running on this machine. **Settings > Recent changes** lists the change. | | [`orbital skill install`](/reference/command/#orbital-skill-install) | Installs every skill that ships with Orbital, for agents and for Claude. | Every subcommand takes `--version`, which prints the version the command is running. A usage error, such as an unknown flag, prints the problem and the help. [The command line reference](/reference/command/) lists every flag, exit code and environment variable. ## Related - [Command line reference](/reference/command/): every flag, exit code and environment variable - [Install Orbital](/get-started/install/): install the command from the Mac app or with npm - [Use the shipped skills](/mcp/skills/): what `orbital skill install` gives your agents - [Start a run](/runs/start-a-run/): the other ways to start a run - [Permissions](/styles/#permissions): what a run asks you before its agent acts, on each harness --- # Troubleshooting > Find the fix for a symptom, or report a bug with the right files Find the symptom in the contents list and apply the fix under it. Each fix says what causes the problem and what to do. To check that Orbital and your harness work at all, run `explain-repo` as in [Start your first run](/get-started/first-run/). It only reads. ## Install and harnesses A [harness](/harnesses/) is the coding agent program that runs a step, such as Claude Code, Codex or OpenCode. ### Installation fails A 401 or 403 from GitHub Packages points at your setup. Check the registry entry, and check that your own classic token has the `read:packages` scope. A 404 can mean your account lacks package access, or that the scope points at the wrong registry. npm access is by invitation during the beta. [Install Orbital](/get-started/install/#1-install-orbital) says how to ask. :::danger Never paste tokens into workflow prompts or logs. ::: If the server cannot load better-sqlite3 after a pnpm install, repeat the install with `--allow-build=better-sqlite3`. If pnpm has no global bin directory, run `pnpm setup` and open a new shell. Then follow [Install Orbital](/get-started/install/). ### The first turn fails Read the command or harness named in the failure. Check its version and login in the shell that starts the server. The server does not inherit credentials exported after it started. Restart it after you change environment variables, then retry the stopped step. If authentication succeeds but model discovery fails, check the selected model and provider. ### A harness is missing or switched off A step does not fail when its harness is not installed, not signed in, not set up or turned off. The run stops on that step and names the cause. The run is [Paused](/runs/#standings), and its badge reads "Waiting on you". 1. **Read the status table.** Open **Settings > Harnesses** and find the harness the step uses. ![Settings, Harnesses page with a status table showing whether Claude, Codex and OpenCode are installed and signed in, followed by Claude's settings.](/screenshots/settings-harnesses.webp?v=6446e71ff0) *The status table says whether each harness is installed and signed in.* 2. **Install the harness if it says "Not installed".** Install it in the shell that starts Orbital, or set its **Program** to the full path. The Mac app reads your login shell's `PATH`. Restart the app after you change it. 3. **Sign in if it says "Signed out".** Run `claude`, `codex login` or `opencode auth login`. 4. **Turn it on if it says "Off".** Turn **Turned on** back on. 5. **Check again and resume.** Choose **Check again**. Then open the run and choose **Resume**. To carry on with another harness instead, [switch the run's harness](/harnesses/switch-harness/). A Codex step on Linux can also stop because Codex's sandbox needs bubblewrap. Install your distribution's `bubblewrap` package, then resume. ### A harness ran out of quota The step stops with an "out of quota" reason. The reason names the harness and, when known, when the quota resets. Wait and choose **Resume**, or [switch the run's harness](/harnesses/switch-harness/). That page also explains how the delivery lead can switch it for you. ### Codex rejects the working folder Codex needs a Git repository. Other harnesses work in a plain folder. See [plain folders and Git](/projects/#plain-folders-and-git). Open the project's page and check the primary folder. Its **Type** must be "Repository". To use a plain folder with Codex, run `git init` in it first, then retry the failed step. ## Runs and workflows ### Validation rejects the workflow Run `orbital validate` with the explicit saved file path. Then match the error: | Error | What to do | | --- | --- | | Missing entry | Name a declared step, such as `entry = "steps.plan"`. | | Unknown key | The error names the key and where it is. Look for a typo, or a key that kind of step does not take. | | Missing prompt file or import | Put the companion file at the path relative to the file that names it. | | Read before production | Add an earlier producer on every incoming path. Give the step a [`needs`](/reference/workflow-file/#needs) default only when that value is a real answer. | Routes are tried in order, and the first whose `if` holds wins. Cover every option of a choice output the routes test, or give the step a `next`. See [step kinds](/reference/step-kinds/) and [add branches](/workflows/add-branches/). ### Starting reports missing inputs Fill in the goal and every required workflow input on the new-run page. An optional input sets `required = false` or a `default` in its `[inputs.]` table. If the workflow is not the one you expected, check for a custom workflow that shadows a file of the same name. Confirm that the project includes the folder that holds the workflow. ### A run appears stopped A server restart does not stop a run. The next start carries on every run you did not pause, from its last finished activity. A run that stays Paused was either paused by you, or stopped by itself and shows the badge "Waiting on you". ![A run page for an interrupted run, with a Paused badge in its header.](/screenshots/run-interrupted.webp?v=700d8cf233) *A stopped run shows the Paused badge.* Open the transcript to find the last active step. Then choose one: | Action | What it does | | --- | --- | | **Resume** | Carries the step on in its session when it can. | | **Retry** | Starts the step again in a new session. | Neither is the same as starting an unrelated run. See [how Orbital works](/concepts/how-orbital-works/). A run whose harness ran out of quota is Paused too. [The delivery lead can switch its harness](/harnesses/switch-harness/) for you. ### A failed step does not take its fallback That is intentional. A failed step takes only a route whose `if` holds, such as `outcome == 'failed'`, and never its `next`. With no matching failure route, the run is Paused, with the badge "Waiting on you". Add an explicit failure route if recovery belongs in the workflow. Otherwise resolve the cause and retry. A failed terminal ends the run and records a terminal failure. A probe that keeps retrying is still inside its step. Check GitHub authentication, repository access and the diagnostic before you change workflow routes. ### A step cannot stop a process That is intentional. Every process a step starts can signal only the processes it started itself. So no step can stop the Orbital server or another run. | System | What the step sees | | --- | --- | | Linux | The step runs in its own process namespace through bubblewrap. `ps` and `pgrep` see only the step's processes, and `kill` answers `No such process` for any other. | | macOS | A sandbox profile refuses the signal, and `kill` answers `Operation not permitted`. | The step's output, in the run's transcript, shows what was refused. Record the PID of anything a step starts, and stop it by that PID. :::caution On Linux without bubblewrap, or where bubblewrap cannot create a user namespace, steps run without this protection. The server logs a warning at start. Install your distribution's `bubblewrap` package. ::: ### A worktree remains after cleanup Inspect `git.worktree.status` and the cleanup step. Orbital keeps all checkouts if removal could lose work. Commit and push the changes you need, or complete the right pull request action, then retry cleanup. Do not delete the directory to make a run look successful. [Worktrees and delivery](/projects/worktrees-and-delivery/) says when Orbital removes a worktree and when it keeps one. ## MCP, triggers and the delivery lead ### An MCP client cannot reach Orbital First check that **Turned on** is on in **Settings > MCP**, and that **Status** says "Listening". While the endpoint is off, `/mcp` answers 404. ![Settings, MCP page with the endpoint turned on, its status Listening, the endpoint address and the token.](/screenshots/settings-mcp.webp?v=ab5e9e42f5) *Check the endpoint switch, its status and its address.* Then match what the client reports: | The client reports | Cause and fix | | --- | --- | | 401 | The token is missing or old. Copy the client's setup from **Settings > MCP** again, paste it, and start a new session. Regenerating the token makes every old registration fail. | | 403 "this server does not answer to that host" | The client used a host name other than `localhost`, `127.0.0.1` or `[::1]`, while Orbital listens only on your machine. Use the address Settings shows. | | A tool's group "is off in Settings" | Turn that tool group on in **Settings > MCP**. | The Mac app moves to a free port when another program, or an Orbital server on another home, holds port 42121. **Settings > MCP** then says so. Register the new address. When an Orbital server already runs on the same home, a second one refuses to start and names the running server's URL and process; use that server, or stop it first. The Mac app and `orbital app` open their window on that running server instead of starting another, and closing the window leaves it running. If the window says the recorded server answers as another home's, stop that server and open the app again. See [Connect a coding agent](/mcp/connect/). ### A trigger rule does nothing - Check that **Triggers** is on in **Settings > Experimental features**. - Open **Triggers** and read the rule's line. A rule that cannot check its source says why, for example that GitHub or Linear refused the saved token. Fix the token in **Settings > GitHub** or **Settings > Linear**. The rule tries again within 15 minutes, or at once when you save it. - Stop, pause and message rules act only on runs that a trigger started for the same item. - Choose **Preview** to see what the rule would have done over the last seven days. ![The Triggers page listing rules with their source, an on or off switch, what each does and its recent firings, with an error shown on one rule.](/screenshots/triggers-page.webp?v=5fe1ad8f4b) *A rule that cannot check its source shows the error on its line.* ### The delivery lead does not act - Check that **Delivery lead** is on in **Settings > Experimental features**. Then check that its **Status** in **Settings > Delivery lead** is on and not paused for today. - Check "What it may do". An action set to "Never" is refused. An action set to "Ask me first" waits for your approval in its chat. - Check "Spending". At its daily limit it stops acting for the day, and says so once in its chat. - A run at a gate does not wake it. That is your process working. ## Reporting a bug Send two files with the report: | File | Where to find it | | --- | --- | | The server's log | `~/.orbital/logs/server/orbital.log`. In the desktop app, **Help > Show Logs** opens its folder. | | The export of the run that went wrong | Open the run, then choose **Download run JSON** from its **Run actions** menu (⋯). | Leave out the macOS system log. It holds the operating system's messages and none of Orbital's server or engine activity. ## Related - [Install Orbital](/get-started/install/): install the app or the command and sign in to a harness - [Harnesses and models](/harnesses/): how a step picks its harness and what each status means - [Switch a run's harness](/harnesses/switch-harness/): carry a paused run on with another harness - [Pause, stop or resolve a run](/runs/control-a-run/): resume, retry or end a run that stopped - [Error handling](/reference/error-handling/): failure routes, fallbacks and repair budgets - [Connect a coding agent](/mcp/connect/): register a client and choose its tool groups - [Make a trigger rule](/triggers/make-a-rule/): set up a rule and try it before you save - [Delivery lead](/delivery-lead/): what the delivery lead watches and what it may do - [Settings](/settings/): where each switch named on this page lives --- # Changelog > What changed in each Orbital release, newest first What each Orbital release made available to users, newest first: what it added, improved and fixed. A change is listed in the release that lets a user of a release build reach it, so work still being built behind a development-only flag, or in a part of Orbital that is not yet deployed, is left out until a release ships it. Changes that only affect how Orbital is built or tested are left out, and so are nightly builds. ## v0.3.12 ### New - **Action needed.** Workflows are written in TOML, workflow format version 3, and DOT workflows no longer load. Convert the DOT files in a project's `.orbital/` folder or in `~/.orbital/workflows/` with Convert to TOML on the Workflows page, or with `orbital migrate`. Each `.toml` file is written beside its `.dot` file, with a note for each setting that was dropped. Orbital converts your saved custom workflows and your edits to built-in workflows by itself when it starts. - In format 3 a step lists its own routes, styles replace roles, classes and the model stylesheet, and a retry budget is declared once. Action steps can commit, push and open pull requests. - A guide to format 3 opens once after the upgrade and compares DOT and TOML side by side. Settings → About shows the workflow format. - Settings → General → Serve has a Public address. The links Orbital hands out, such as the run link from MCP `start_run` and `orbital run start`, use it. `orbital settings set public-address` and MCP `change_setting` set it too. - A background task's card in a run thread has an Output section, closed by default, that shows what the task prints while it works. - Pull request delivery asks a decision model whether new comments are a real review or automated noise. Noise goes back to waiting for a review. With no decision model, the run addresses the comments. - `pursue-goal` asks a decision model whether the change touches the frontend, and reviews it in the running app when it does. With no decision model it goes straight to validate. It delivers through the shared pull request delivery workflow. - A workflow preset in Settings can set the harness, model and effort, and several harnesses' own settings, at once. The map and studio show the harness, model and effort each step uses and where each comes from. The new-run preset picker offers Follow Settings. ### Improved - **Action needed.** Tool access is removed. Settings has no Tool access page, roles and steps carry no tool limits, and run steps get every tool. If a step relied on a tool limit, say in its prompt what it must not change. - A paused run gives its slot back, so the queue starts the next run. On Resume or Retry it queues again. - A deciding fan-out's decision shows as a card with the branches it picked. - When a decision model refuses in a way Orbital does not recognise, the Settings Test and the run's decide step show what the provider sent. - The MCP workflow tools list a workflow that does not load with its problem, so one broken file no longer hides the rest. - Durations of a day or more show in days and hours, such as `9d 23h`. - File headers in Changes show the full file name first, with its folder beside it, and stand apart from folded lines. - The docs teach workflow format 3, with new Workflow file, Step kinds and Styles pages and guides to actions and repeating a workflow. The docs sidebar folds its groups, so every heading shows at once, and the docs have a Changelog. ### Fixed - After a restart, a decide step or deciding fan-out uses the decision it recorded instead of asking the model again, and no event is lost or written twice. - Revising the goal of a queued run, or sending it a message, answers at once instead of timing out. The run starts with the new goal or the message. - A gate left by Jump, Retry, Restart or Abort no longer shows as waiting. - The Changes diff, the Files viewer and the file trees follow the app's theme, so they no longer stay light in dark mode. - The project name on the start page is readable in every theme. - Completed rows in the runs sidebar have the same corners as the Working card. - Settings → About → Licences lists only the code Orbital ships, not its build tools. ## v0.3.11 ### New - A workflow can declare a decide step. It asks a decision model structured questions and follows the answer, or takes its default path when no model answers. - A fan-out can decide which of its branches start. The workflow editor adds and edits both. - Settings has a Decision models page with Jev, Clef, Laya and Kev presets. Decision models are configured on the server and reach every run. Decision providers are plugins, and System One HTTP and Cloudflare Workers AI ship first. - Each decision shows as a card in the run thread, with the path it took and why. - Keyboard shortcuts: a command palette, a help view and rebinding in Settings. - Settings → About lists the third-party licences Orbital ships. ### Improved - Stored credentials move into one secret store, protected by a master key that always exists. - The runs list stays responsive while several tabs poll it. - The docs cover decide steps, deciding fan-outs and decision models, and the docs sidebar shows that it scrolls. ### Fixed - Condition pills stay off other edges' lines on the expanded imports map. - The decision model API key hint no longer says where the key is stored. ## v0.3.10 ### New - Use models through OpenRouter. Each harness reports the models it can use, and OpenCode's connected providers list the models Model settings offers. - The start composer picks a preset, not a harness, and the Claude and Codex presets ship with every installation. - Claude Haiku 5.5 joins the model catalogue and titles runs. - A run started with Repeat starts again each time it delivers a pull request. - Trigger rules have MCP tools. - Shipped workflows live in the database and are editable like roles and presets. - Runs are titled after their ticket and show its epic. - Update and restart from About, and a restart that cannot relaunch says so. ### Improved - Trigger rules are more predictable. A re-enabled rule resumes from now, a halted rule resumes on any save, a Start rule starts at most five runs per check and skips an item its own run still works on, and a schedule rule fires at its tick. - A schedule tick missed while Orbital was closed is shown, and the latest one runs once on return. - A rule waiting out a rate limit says so, and a check that cannot read every change says how much it skipped. - Preview says saving does not act on the listed matches, and counts the runs a Start rule would start. - The Runs page loads a page of runs at a time. - Home and project workflows save in place. - Toggling a notification switch shows an example notice, and Orbital says why switched-on notifications cannot show. - The start page, Model settings and the agent question card are polished. - Run branches show without the `orbital/` prefix, and pull request and issue numbers show what they are. - A running skill shows which skill it is. ### Fixed - A Linear checkpoint keeps only the scope its rule watches, and label and assignee conditions test the changed value. - Switching a rule off stops its pending starts, and a start that keeps failing gives up. - A schedule's plain words match when it runs. - A dragged step on the expanded map keeps its edges. ## v0.3.9 ### Fixed - Check for updates, and the automatic update checks, work again in the desktop app. ## v0.3.8 ### New - Pi joins Claude Code, Codex and OpenCode as a harness. - A paused chat carries into a workflow goal. - `pursue-goal` reduces drift after every implement round. - Sending a message at a parked gate continues the run. - A second server on the same Orbital home refuses to start, and the app opens its window on that home's running server. ### Improved - Resume, Retry, Jump and gate passage carry a run on from its last settled state, so a restart loses less. - Each step keeps one Claude process or Codex app server across clean turns. - Every tab following a live run is sent the same encoded event, so busy runs cost less to watch. - Model settings are easier to use. - Start-up reads each unfinished run once before runs walk their workflows. ### Fixed - A turn cut short by more than one restart continues its newest session. - Pausing a queued run takes it out of the queue for good. - A replay after a restart follows the edges the live run chose. - An ended run stays ended when something fails after its end, and an Abort that failed in the running server finishes. - A pending tool permission waits for you like an open question. - A failed pull request look no longer ends a run's walk or watch. - Codex approves Orbital's own MCP tools. - The pull request probe leaves out repositories the run never changed. - `orbital skill install` links where Claude Code reads skills. - The run menu opens in Firefox. ## v0.3.7 ### New - The workflow editor becomes a studio for reading and editing steps. It opens steps over the map, edits expanded imports, searches and creates steps from a route's step picker, and marks new steps and what is left to finish. - A step declares the values it hands over as fields: short text, paragraph, number, yes or no, a choice with a description and step for each option, optional fields and lists of items. The studio previews what the agent is told. - A step lists the values it needs and where each comes from. - One Model settings screen holds roles, presets and harness set-up. Model presets are stored, one is in use, and a running run takes the preset in use at its next turn. - Roles carry a prompt and a description, and the role card and picker say what a role is for. - The run log records where a harness could not carry a setting across. - `list_runs` over MCP pages its answer, filters more and shows recent runs by default. ### Improved - A run's history is written in one transaction per step, so a crash loses less and replay is faster. - Health answers before work that grows with runs, worktrees and projects. - Billing counts every Claude token once, on its model, and says what is missing. The Cost tab shows one model per row and fits without scrolling. - Codex checks its sandbox once per mode and folder. ### Fixed - Orbital leaves alone the processes another running server started. - A queued run answers Move to front truthfully and re-queues after Retry. - A finished fan-out branch leaves no checkpoint. ## v0.3.6 ### New - The sidebar groups runs into Needs you, Working and Queued, and you can move through them by keyboard. - A trigger rule can review new commits pushed to pull requests. - Pause and chat conversations are held, and a durable Resume needs your approval. - Runs prepare, run commands and harnesses in their selected environment, and recover on the environment they recorded. - Run rows show duration, and billing splits token usage into labelled columns. ### Improved - Large runs load faster: a snapshot is encoded once, a prompt reads only the blobs its template names, and repeated history bodies are compressed. - Pause no longer loads the run's workflow, and start-up and pull request refresh decode only the runs they use. - Queue positions show in the sidebar, and run titles and context are easier to read. - Onboarding happens once per server installation. - The release bundle no longer ships its source map. ### Fixed - Updates that stalled on restart recover, and start once the app's main window has loaded. - Archived runs disappear from the list at once. - Paused conversations stay visible, keep refused messages, accept answers and can hand off. - The whole operator backlog reaches a run. - Billing accounts for sub-agent usage. - Ticket prefixes no longer crowd sidebar run titles. - Concurrent OpenCode turns keep their own MCP attachments, and handoffs survive MCP prefix blocks. ## v0.3.5 ### New - Onboarding guides new projects to their first run and lets you skip it. - Binary choices in settings are toggles. - The sidebar has run controls, with Restart in the menu. - Run now is a visible trigger action. - GPT-6.1 Sol joins the Codex catalogue. - Usage analytics, with an opt-out in the app. ### Improved - The start action is called New task, and the project palette opens only for it. - Repeat markers for step visits are subtler. ### Fixed - The server log is restricted, request queries stay out of logs, checkout reads are contained and sibling origins are rejected on live requests. - The desktop app refuses unsafe external link schemes. - Delivery pull requests are attributed to the run's work. - A validated goal that needs no change completes. - Stale start-up assets and project decode retries no longer cause trouble. - Run error diagnostics stay contained, and checkboxes return for list selection. ## v0.3.4 ### Improved - Long runs are far quicker to open and follow. Session transcripts render a bounded window, and live updates, tool groups, Markdown blocks, step projection, Logs search and history indexing all got faster. - Run billing is worked out only when you open the Cost tab. - Run standing reads the same everywhere. - `improve-codebase` stops after one improvement. ### Fixed - Accepted Claude handoffs complete, and MCP waits finish on the run's phase. ## v0.3.3 ### New - Saved triggers start on demand. - The simple built-in workflows return. ### Improved - Disabled workflows collapse below enabled ones, and completed sidebar rows are quieter. - The splash reuses the Orbital icon. ### Fixed - Step transcripts open in Session. ## v0.3.2 ### New - Manage which workflows are available. - Pull request conversation feedback is handled by the review step. - Orbital notifies you when a run waits for input. - Choose an update channel, with nightlies published. - Check for updates from the macOS menu. - Claude Sonnet 5.5 joins the model list. - The run history keeps who or what sent a control, and why. - Failed and blocked runs explain how they ended. ### Improved - The desktop splash shows at once and start-up is faster. - Runs take up less storage and exports are self-contained. - Recent runs are ordered better and their rows are easier to open. - A run waits for new review feedback without republishing. - Operator messages reach every fan-out branch. ### Fixed - Fan-out billing and cached model estimates are priced correctly, and cached usage is kept. - Pull requests a probe found stay visible across branches. - Codex offers only supported approval policies, clears cancelled approvals and keeps MCP failure text and startup diagnostics. - Blocked epic delivery parks for you instead of looping. - A missing build prerequisite no longer ends a run. - Run slots are kept correctly through recovery, removal and method changes. - The desktop app waits for permission before restarting for an update, hides CLI installation when it is installed, and opens server page links in Orbital. ## v0.3.1 ### New - Documentation is reorganised by what you want to do, with screenshots of the real app, a Write prompts guide and Docs, Examples and Reference tabs. - Queue method in Settings: Interleave or Finish first. With Finish first, a run keeps its slot until it ends, and finishes up after a merge without queueing again. - Desktop updates appear in a banner and in About, with the reason when Orbital cannot update. - Claude Code, Codex and OpenCode are equal choices, and `explain-repo` runs on each. - Codex asks for permission in the run. ### Improved - Runs keep working after the project's folder is moved or renamed. - A run's pull request state stays current after its gate and after it ends. - Delivery recovers when probes exhaust their retries, and stale merges at pull request gates are ignored. - The pull request gate says its label once. - The docs explain permissions and tool access on each harness. ### Fixed - Draft pull requests are made ready before auto-merge is requested. - Jumps keep a run's slot. - Codex keeps read-only shell available. - The Supervisor exposes the valid MCP tools. ## v0.3.0 ### New - Claude runs default to Opus 5.5. - Onboarding and desktop start-up offer the `orbital` command and the agent skills. ### Improved - Tooltips are shorter and share one component. - Destructive alert dialogs have an opaque surface with a red top stripe. - Back is at the top of the Settings sidebar. ## v0.2.21 ### New - `pursue-goal` is the default workflow. ### Improved - Each agent skill has one current status in Settings. - An aborted run says who stopped it and why. - The light theme has an off-white page background. - The docs carry the Orbital mark and fix the points of a review against 0.2.19. ## v0.2.20 ### Improved - The turn in flight is told when its run's goal is revised. - A probe step leads with its verdict. ### Fixed - No new step starts once the server has begun shutting down. ## v0.2.19 ### New - Public documentation from overview to reference, and `llms.txt`. - Choose light, dark or match your computer in Settings → General. - Orbital turns on auto-merge itself when a workflow asks for it. - A project limits its own runs, and the scheduler takes turns between projects. - Copy chosen untracked files, such as `.env`, into every new worktree. - Open a sub-agent's card to see its messages, tool calls and final report. - Agents read pull request status and secure a run's work through Orbital's MCP server. - `control_run` moves a queued run to the front of the queue. - Every settings change is recorded with what changed and where it came from. - Orbital runs the agent tools you installed, and ships none. ### Improved - Orbital explains missing or blocked harness programs, and never starts a switched-off harness. - A Claude turn waits for its background work, and refused tool calls show in the run. - The composer footer names the harness and model of the same step. - A gate opens as soon as the run records its pull request merged or closed. - A run waiting at a gate shows as waiting, not paused. - Dropped connections heal quietly, and Orbital shows Connecting only for a lasting outage. - The Projects list and project page are quicker to scan and act on. - The dock's Run tab scrolls as one column, and each section folds under its heading. ### Fixed - Runs recover from a restart, and a step's harness and model resolve together. - Stop and clear works on a run whatever state its workflow is in. - Pasted images become chat attachments. - Skills folders that are symlinks to directories are followed. - Pages work over plain HTTP on a LAN host. - The editor opens a workflow whose import cannot be read. - A restarted turn starts afresh when its harness holds no session to continue. ## v0.2.18 ### New - **Triggers.** Start and steer runs from schedules, GitHub and Linear, with rules a non-technical user can build, and a Triggers page listing every project's rules. Triggers are behind a feature flag in Settings, off by default. - **MCP.** Coding agents drive Orbital through its MCP endpoint, and external agents can inspect runs. Every harness gets a user-wide MCP setup. - **Supervisor.** An experimental standing Supervisor chat acts as delivery lead, woken by what Orbital sees in its runs, behind a Supervisor flag. - **Roles, model settings and tool access** are resolved per step and edited in Settings. Claude, Codex and OpenCode enforce a turn's tool classes. - Workflows `pursue-goal` and `port-project` ship. - Revise a running run's goal. - Chats: every turn reaches Orbital's MCP server, and you can attach files. - Every run gets a readable title, generated with Claude Haiku or Codex Luna, and you can name or rename runs. - Edit a built-in workflow in place and revert it. - Run `orbital run start` to queue runs from the terminal, and install skills with `orbital skill install`. - A run keeps and lists every pull request it delivered. - The run composer accepts a message at every point and says when it lands. - Gates show their next check and total wait, and you can skip them. ### Improved - A run carries on from its last finished activity after the server dies, and every unfinished workflow execution resumes after a crash. - Pause, Resume, Retry, Jump and Abort work on every run. - The queue keeps its order across a restart, and a cap change applies at once. - Human gates park runs without holding a slot, and stalled turns are retried. - Runs are priced per turn by the model the harness reported. - The run page lists only the actions you can take now, and its Workflow tab leads with the drawing. - Web pages are usable on a phone. - The sidebar folds completed runs under a Completed section, and failed and blocked runs stay in Recent runs until resolved. - A themable desktop design system with compact controls. - Each project folder has a check command the review-evidence step runs. - Orbital runs are renamed from graphs to workflows throughout. ### Fixed - Orbital stops every harness process a dead server left behind. - A delivered run no longer fails because its pull request branch was rebased. - A run pauses when GitHub never ran a failed check. - A gate keeps checking its pull request after a restart, and Skip opens it. - A sandboxed Codex turn that cannot set up bubblewrap pauses the run. - Orbital reports start-up failures to open clients, and starts over unreadable start-up records. - Copying works on any address, including a LAN host over plain HTTP. - A harness running out of quota ends the turn with a typed reason. ## v0.2.17 ### New - `implement-epic-quickly`, an epic workflow with eight parallel reviewers. - Stopped runs can restart and show as paused, and you can archive a paused run from its menu. - Cmd+N opens the new run palette in the desktop app. - The start page preselects the workflow a project last ran, and the homepage opens the new-session composer. - Resize and collapse the app sidebar and the editor's side panel. - Settings save every edit as it is made, and every setting can be set from Settings. - A download link that always serves the newest Mac app. ### Improved - Waiting runs and quiet turns say what they are waiting on. ### Fixed - A control takes effect on the first click while a run is working. - Fan-out answers no longer grow every prompt after a review loop. - Workflow pills that overflow the start strip are hidden. ## v0.2.16 ### Improved - The Mac app is presented as Orbital, published by Effective Labs Ltd. ### Fixed - The signed Mac app is built with the right entitlements. ## v0.2.15 No user-facing changes. This release fixed the Mac app's packaging. ## v0.2.14 ### New - Orbital ships as a signed macOS app that updates itself. - The start page asks which project work starts in before anything else, and again when New session is pressed. - Skip a wait from its row in the thread. ### Improved - A run's worktrees are cleaned up when it is archived or deleted. - The Changes tab keeps files in one order and opens on the branch scope. - The project palette is centred over a blurred page. - A checkout with no pull request is decided by its content. ### Fixed - A run's thread keeps advancing after a pause and a resume, and a stopped run carries on when its wait is skipped. - Updates ignore a minimum release age when installing. ## v0.2.13 ### New - The start page remembers the project you chose. - Work that waits for you stays under "Pick up where you left off". ### Improved - The Claude and Codex harness SDKs are upgraded. ### Fixed - A bare address in agent Markdown becomes a link. ## v0.2.12 ### New - Update and restart from the browser, and Orbital offers to update when `orbital serve` starts and knows when a newer release exists. - The workflow editor is reworked into a Graphviz-shaped design, and you can rename a saved workflow from it. - A workflow that drives an epic to closure one ticket at a time, and one that simplifies a complex function as one pull request. - A run can watch for its own pull requests. - Claude Opus 5.5 and the new GPT-6 models are offered as suggestions. - Steps hand their outputs over through a handoff tool instead of trailing JSON. ### Improved - A run is left resumable when `orbital serve` is stopped or killed mid-turn, and a run that stopped on a failed step can resume. - Restart `orbital serve` into a freshly installed version. - Branch answers carry past the fan-in and branches show apart. - The prompt an agent receives shows nothing twice. ### Fixed - The Claude harness no longer signs commits and pull requests. - The run page keeps following a run after it stops and is resumed. - Stopped visits recover with their saved continuation. ## v0.2.11 ### New - Chats: start a conversation with a coding agent beside your runs, each with a worktree of its own. - Start workflows from inside a chat, in the chat's checkout. ### Improved - Runs and chats read as one thread, served a window at a time. ## v0.2.10 ### New - Run project commands and open links from a run. - Run steps merge into the Run dock tab. ### Fixed - OpenCode returns operator answers in order for question batches. - The whole agent response is stored as the step's last response. - A run's owner that departed is recovered before its lease expires. - A transcript row ends at the harness's own message boundary. - The probe searches the forge by the branch's upstream name. ## v0.2.9 ### New - Review a run's changes, and explore its worktree and files, from the workspace dock. - Choose your IDE, and open run folders in it from the run header. - Retry stopped work on another harness. - Manage a project's custom actions from its details, with commands and links validated. - Choose models with reference prices, through a searchable harness and model picker. The model catalogue is expanded and runs retain the models you selected. - A New run button sits at the top of the sidebar. - The run workspace navigation and context views are redesigned. ### Improved - Pull requests are found regardless of branch naming, remembered across failures and restarts, and tracked from detached and renamed checkouts. - Wait actions and run controls stay available when waits or workflows change. - Agent output stays together across tool activity. ### Fixed - Questions keep their full text and pending answers, can be answered inline from every tab, and failed sends retry. - Claude and Codex return complete answers to every question and no longer deliver stale replies. - Custom action saves no longer overwrite another editor or lose drafts when a save fails. - Paused step state resets when a run resumes, and the page returns to the latest steps in long runs. - Pending retries stay isolated between runs. ## v0.2.8 ### Fixed - Claude provider failures are preserved through SDK shutdown. ## v0.2.7 ### Improved - The Orbital logo, and onboarding in Storybook. ## v0.2.6 ### New - Archive a settled run. - Adjustable step pane widths. - The new run form prefills the goal from the workflow's description. - `orbital serve` binds a stable default port, so an installed browser app survives restarts. ### Improved - Standing badges show the current step, and paused steps show without spinners. - Branches chosen inside attached worktrees are tracked. ### Fixed - Unpushed commits are counted from the pull request head. - The detail pane stays in step with streaming rows. ## v0.2.5 ### Fixed - Optional checks are excluded from CI routing. ## v0.2.4 ### New - Documentation site guides you from installation to graph delivery, with three header topics. ### Fixed - Skipping a wait works inside a fan-out branch, is a durable state change, and survives a lapsed lease. - A pull request that merges under the probe is reported. - A control is decided against the ledger its offer was read from. ## v0.2.3 ### New - The run view shows the working branch's pull requests. ## v0.2.2 ### Improved - Project create, read, update and delete is improved. ### Fixed - The output format is clearer and step scroll position is preserved. ## v0.2.1 ### New - Reviewed tickets, ticket hierarchies, complete goals without tracker tickets and codebase improvements are delivered through one shared pull request loop. Performance delivery requires parallel evidence reviews. - Manage the server GitHub token from Settings, and GitHub credentials survive restarts. - Workflow design is available in installed releases. - Export the whole log of a run as JSON. - Imports leave through named exits. - The pull request probe reports auto-merge. - Continuous codebase improvement is restored. ### Improved - Run lists load from their summary rows alone. - Delivery reassesses after five failed repair rounds, bounds ineffective repairs and finishes unreviewed repairs before merging. - Review catches scoped defects before publication, and publication is verified before a merge is requested. - Only the six autonomous delivery workflows ship. - Step colours stay stable across visits. ### Fixed - Resume, Retry and Jump reach a live held run, and Retry starts a fresh visit. - An interrupt is recorded as Interrupted, keeping the session. - A run whose snapshot and control ledger disagree is reconciled. - Blocked tickets are told apart from completed work. ## v0.2.0 ### New - agent-graph is renamed Orbital, with a new mark in the app and its icons. ### Fixed - You can steer a running turn without interrupting it. - Worktrees checked out directly under `worktrees/` are migrated. ## v0.1.7 Contains the same changes as v0.1.6. ## v0.1.6 ### New - Run inputs are populated with their defaults. ### Fixed - Runs that ended as abandoned before Abort existed are listed. ## v0.1.5 ### Fixed - The step thread stays where you scrolled it. ## v0.1.4 ### New - The `implement-epic` workflow. - Skip a wait from the run actions menu. - Concurrency is capped by runs in flight, not agent turns. ### Improved - The README explains install, build from source and every mise command. ### Fixed - A recent run opens on its steps. - Links to other websites open in a new tab. - An aborted run ends everywhere and names what it stands at. ## v0.1.3 No user-facing changes. This release fixed how the package is published. --- # Implement a feature > Turn a short goal or a ticket into a reviewed, merged pull request In this example you have Orbital build a small feature and deliver it as a merged pull request. You start one shipped workflow with the feature in plain words, or with the address of the ticket that describes it. About five minutes to set up. The run then works on its own until the pull request merges. ## Before you start Both workflows ship with Orbital, so there is nothing to download. You need: - a project whose primary folder is a Git repository with a GitHub remote you can push to, because the run works in a [worktree](/projects/worktrees-and-delivery/) and delivers a pull request; - a GitHub token Orbital can use: a `gh auth login`, or a token in **Settings > GitHub**; - a harness that is installed and signed in; For `implement-ticket` with a Linear ticket, the agent reads it with the Linear tools your harness has, such as Linear's MCP server. :::caution Orbital turns on auto-merge for the pull request. Where your branch rules require no review, the pull request merges as soon as its required checks pass. With no required checks, it merges as soon as it opens. Protect the branch before you start, or start your own copy of the workflow with auto-merge removed from its delivery. [Turn auto-merge off](/projects/worktrees-and-delivery/#turn-auto-merge-off) shows how. ::: The steps run with Full access. `pursue-goal` also runs with `permissions = "full"`, so its agent is never asked before it changes files or runs commands. `implement-ticket` uses `auto-accept`. [Permissions](/styles/#permissions) explains what each one asks you. ## 1. Choose a workflow | Use | When | | --- | --- | | `pursue-goal` | You can say what you want in a few sentences, and there is no ticket. | | `implement-ticket` | The work is written up in a GitHub or Linear issue. | Both work in a worktree, review their own work and verification, open a pull request, and repair it until it merges. ## 2. Start the run Follow the part for the workflow you chose. ### Deliver a goal with pursue-goal **App** 1. Choose **New task**, pick the project and choose the `pursue-goal` workflow. ![The Start page asking what to do in a project, with a goal box, a preset picker, a workflow picker and a Chat or Workflow switch.](/screenshots/start-page.webp?v=7454bfb8bf) *Pick the workflow, then write the goal in the box.* 2. Write the goal. Say what should change, how you will know it works, and any rule the result must follow. For example: ```text Add a "Copy link" button to each invoice row. Clicking it copies the invoice's public address. Add a unit test for the address format. Follow the conventions in CONTRIBUTING.md. ``` 3. Choose **Start run**. **Command line** Run `orbital run start` with your project's id and the goal. [The orbital command](/reference/command/) lists every flag. ```sh orbital run start --workflow pursue-goal --project proj_r3tQMqBPBB5p \ --goal "Add a Copy link button to each invoice row. Clicking it copies the invoice's public address. Add a unit test for the address format." ``` ![pursue-goal: create_worktree, implement, validate, then delivery, cleanup and the terminals.](/shipped/pursue-goal.svg?v=5072c9d847) What the run does: | Step | What it does | | --- | --- | | **create_worktree** | Makes a worktree on a new branch. | | **implement** | Changes the code and commits. | | **validate** | Checks the work against every point of the goal, using the change and verification. If something is missing, it sends feedback back to **implement**. This loop runs at most 20 times. | | **delivery** | Opens the pull request and waits for it to merge, repairing CI, conflicts and review comments. | | **cleanup** | Removes the worktree once nothing in it would be lost. | Implementation goes straight to validation. A rejected validation returns its feedback to the next implementation round; a pass starts pull request delivery. There is no separate checks or prerequisite gate. ### Deliver a ticket with implement-ticket **App** 1. Choose **New task**, pick the project and choose the `implement-ticket` workflow. 2. Fill in **Ticket URL**, such as `https://github.com/acme/app/issues/628`. 3. Write a goal, or leave the ticket to speak for itself. 4. Choose **Start run**. **Command line** Give the ticket's address as the `ticket_url` input. ```sh orbital run start --workflow implement-ticket --project proj_r3tQMqBPBB5p \ --input ticket_url=https://github.com/acme/app/issues/628 ``` ![implement-ticket: scope, then the deliver-ticket sub workflow.](/shipped/implement-ticket.svg?v=3dcf81f887) The first step reads the ticket and writes the scope that every later step works from. Then the run plans, implements, and has the change reviewed against the ticket and verification. A rejected review revises the plan and the implementation, and the review runs again. Once the review passes, the run delivers the pull request the same way as `pursue-goal`. It completes the ticket after the merge. ## 3. Follow the run | Where | What it shows | | --- | --- | | The **Session** tab | Each step and the agent's work. | | The dock's **Changes** tab | The diff so far. | | The dock's **Pull requests** section | The pull request, once it is open. | ![A run page on the Session tab showing the steps one by one, with Run facts, Pull requests and Steps in the dock on the right.](/screenshots/run-thread.webp?v=f4f203d47e) *The Session tab shows each step; the dock lists the run's pull requests.* To steer the run, write in the composer. To change what the run should do, send it a message with the new requirement. See [Message a run](/runs/message-a-run/). ## 4. Review the pull request While the pull request waits for review, the run waits at a gate, "The pull request is waiting for review or merge". It takes no run slot. Review the pull request as you normally would. You do not need to merge it: once your branch rules allow, GitHub merges it, because Orbital turned on auto-merge. Orbital looks at the pull request once a minute and carries on by itself. ![A gate step saying the pull request is waiting for review or merge, with the waiting time, the next check and a Skip button.](/screenshots/gate-wait.webp?v=fdf72f63d4) *The run waits here without taking a run slot.* ## What you get The run ends Succeeded when the pull request merges. It ends Failed when someone closes the pull request without merging. See [standings](/runs/#standings). ![The dock's Pull requests list with open and merged pull requests and their branches.](/screenshots/pull-requests-panel.webp?v=612ac483b1) *A merged pull request shows as merged in the dock.* Once the publish step ends, Orbital turns on auto-merge for the pull request itself, with your GitHub token. The agent never runs `gh` for it, so this works on every harness and permission setting. When the repository does not allow auto-merge, a rule blocks it, or the token lacks permission, the run's history says so and the run carries on. Then merge that pull request yourself. [Turn auto-merge off](/projects/worktrees-and-delivery/#turn-auto-merge-off) explains how to stop Orbital asking. ## Related - [Worktrees and delivery](/projects/worktrees-and-delivery/): how the run makes a worktree, opens the pull request and merges it - [Shipped workflows](/reference/shipped-workflows/): every step of `pursue-goal` and `implement-ticket` - [Lead delivery](/delivery-lead/lead-delivery/): have an agent start and follow such runs for you - [Deliver an epic](/examples/deliver-an-epic/): deliver many tickets in one run --- # Deliver an epic > Deliver an epic's tickets in dependency order, one merged pull request each In this example you deliver a whole epic with one run. You start the shipped `implement-epic` workflow with the epic's address. The run picks the next ticket that nothing blocks, delivers it as one merged pull request, and picks again until every ticket is done. Then it closes the epic. About five minutes to start, once the tickets are ready. The run itself can take days. ![implement-epic: select picks a ticket, ticket delivers it, and the loop repeats until close_epic.](/shipped/implement-epic.svg?v=65cdc65e9b) ## Before you start The workflow ships with Orbital, so there is nothing to download. You need the same as for [one feature](/examples/feature-pull-request/#before-you-start): a project on a Git repository, because each ticket is built in a worktree, a GitHub token and a signed-in harness. :::caution Orbital turns on auto-merge for each pull request. Where your branch rules require no review, each pull request merges as soon as its required checks pass. The run then moves on to the next ticket without waiting for you. Protect the branch before you start, or start your own copy of `implement-epic` with auto-merge removed from its delivery. [Turn auto-merge off](/projects/worktrees-and-delivery/#turn-auto-merge-off) shows how. ::: The steps have Full access and run with `permissions = "auto-accept"`. [Permissions](/styles/#permissions) explains what that asks you. ## 1. Prepare the tickets The epic needs tickets the agent can read: | Tracker | What the epic needs | | --- | --- | | GitHub | Sub-issues of the epic, with "blocked by" links where order matters. | | Linear | Sub-issues, read with the Linear tools your harness has. | Each ticket should be one piece of work that one pull request can deliver, with a clear "done when". The run follows the tickets, so refine them first. [Lead delivery](/delivery-lead/lead-delivery/#the-loop) explains how. ## 2. Start the run **App** 1. Choose **New task**, pick the project and choose `implement-epic`. ![The Start page asking what to do in a project, with a goal box, a preset picker, a workflow picker and a Chat or Workflow switch.](/screenshots/start-page.webp?v=7454bfb8bf) *Choose the workflow here before you fill in the epic.* 2. Fill in **Ticket URL** with the epic's address. 3. Write a goal with any rule every ticket must follow, or leave the epic to speak for itself. 4. Choose **Start run**. **Command line** Give the epic's address as the `ticket_url` input. [The orbital command](/reference/command/) lists every flag. ```sh orbital run start --workflow implement-epic --project proj_r3tQMqBPBB5p \ --input ticket_url=https://github.com/acme/app/issues/600 ``` The run then works through the epic: | Step | What it does | | --- | --- | | **select** | Reads the epic and its tickets and picks the next leaf ticket that is open and not blocked. | | **ticket** | Delivers that ticket exactly as [`implement-ticket`](/examples/feature-pull-request/#deliver-a-ticket-with-implement-ticket) does: worktree, plan, implement, review, pull request, merge, complete the ticket. | | Back to **select** | The run starts again with a clean slate for the next ticket. | | Wait | When every remaining ticket is blocked, the run waits thirty minutes and looks again. | | **close_epic** | When no ticket is left, closes the epic, and the run ends Succeeded. | A pull request closed without merging does not stop the run. The run moves on to the next ticket, and you decide what to do with the closed one. ## 3. Review each pull request Each ticket's pull request appears in the dock's **Pull requests** section. The run takes no run slot while it waits at each pull request's review gate. Review each pull request as you normally would. Once your branch rules allow, GitHub merges it, because Orbital turned on auto-merge. ![The dock's Pull requests list with open and merged pull requests and their branches.](/screenshots/pull-requests-panel.webp?v=612ac483b1) *Each ticket adds its own pull request to the list.* An epic run can take days. Check in on it, or let [the delivery lead](/delivery-lead/) or a coding agent with the [`orbital-delivery-lead` skill](/mcp/skills/#orbital-delivery-lead) watch it for you. ## What you get A merged pull request for each ticket the run delivered, with that ticket completed, and the epic closed. The run ends Succeeded when no ticket is left. ## With a review panel `implement-epic-with-panel` delivers an epic the same way, but reviews each change differently. Eight reviewers look at the change in parallel, each for one kind of problem: the change's lifecycle, workflows, integrations, the frontend, tests, complexity, new comments and coding standards. An implementer fixes what they find, and one more reviewer checks the fix. There are at most two fix rounds. Use it when you want a broader review of each ticket and can spend more agent time on it. ![implement-epic-with-panel: parallel reviewers, triage, fix and a second review for each ticket.](/shipped/implement-epic-with-panel.svg?v=4009a72683) ## Related - [Shipped workflows](/reference/shipped-workflows/#implement-epic): every step of `implement-epic` and its panel variant - [Outer loop over an epic](/examples/workflows/epic/): a smaller epic workflow you can read file by file - [Implement a feature](/examples/feature-pull-request/): how one ticket is delivered - [Delivery lead](/delivery-lead/): let an agent watch a long run for you --- # Review pull requests > Start a review run each time someone opens a pull request In this example you have an agent review every new pull request. You add the `review-pull-request` workflow to your library, then make a [trigger](/triggers/) rule that starts it when a pull request opens. The run reads the pull request and leaves one review comment. About ten minutes to set up. After that, each review run starts within about a minute of a pull request opening. ```toml title="review-pull-request.toml" version = 3 entry = "steps.review" description = "Review one pull request and leave the review as comments, without changing, approving or merging it" style = "reviewer" [steps.review] prompt_file = "prompts/review-pull-request/review.md" next = "steps.done" [steps.done] kind = "terminal" ``` ![review has one step, then done.](/review-pull-request.svg?v=86d090e8cf) ## Before you start You need: - the Triggers feature turned on: **Settings > Experimental features > Triggers**; - a GitHub token that can read the repository and comment on its pull requests, from `gh auth login` or **Settings > GitHub**; - the `gh` command on the machine that runs Orbital; - a project whose primary folder is the repository; - the workflow: [download the archive](/review-pull-request.zip). ## 1. Add the workflow 1. Unpack the archive into your workflow library, `~/.orbital/workflows/`. Keep every relative path. 2. Check it: ```sh orbital validate ~/.orbital/workflows/review-pull-request.toml ``` Every project now offers `review-pull-request`, and it shows on the **Workflows** page. ![The Workflows page listing workflows with their source, Clone and Export actions, and Import and New workflow buttons.](/screenshots/workflows-page.webp?v=991e5fde13) *Workflows in your library show here for every project.* The archive holds the workflow above and its prompt: ```md title="prompts/review-pull-request/review.md" Review this pull request: {{ inputs.goal }} Read the pull request with the GitHub command line, `gh`: its description, the tickets it names, its diff, its checks and the review comments already on it. Read the repository's instructions files, such as AGENTS.md or CLAUDE.md, and follow its review conventions. Look for defects, missed requirements, missing tests and risks. For each finding, give the file and line, what is wrong, why it matters and the evidence. Do not report style a formatter would fix. Leave one review on the pull request with `gh pr review --comment`, holding every finding. Never approve, request changes on, merge or close the pull request, and do not push commits or change any file. If you cannot read the pull request, say so and stop. End with a short summary of the review you left, with the pull request's address. ``` ## 2. Understand what the agent can do The workflow's top-level `style = "reviewer"` gives the step the shipped `reviewer` style: the Strict reviewer persona at high effort. The step runs commands, because it needs `gh` to read the pull request and post the review. The workflow makes no worktree. Its prompt tells it never to approve, merge or change anything. The pull request's title, description and diff come from whoever opened it. The agent reads them, so text in them can try to steer it. The prompt tells the agent not to change or push anything, but a prompt is only a request: the step can still edit files and run any command its harness allows. [Permissions](/styles/#permissions) explains what each harness asks before it acts. So the rule in the next step should also start runs only for pull requests from people you trust. ## 3. Make the rule 1. Open **Triggers** in the sidebar and choose the ready-made rule "Review every new pull request". It fills in: - source: GitHub, for pull requests that are opened; - action: Starts a run; - goal: `Review {{ url }}: {{ title }}`. ![The New rule dialog set to GitHub pull requests, with the event A pull request is opened.](/screenshots/trigger-rule-github.webp?v=7d933426ed) *The ready-made rule listens for opened pull requests.* 2. Choose the project and the `review-pull-request` workflow. 3. Limit it to people you trust. Under **Only if**, add "Author's team is any of" and pick a team of your organisation, written as `organisation/team`, such as `acme/engineering`. The rule then starts runs only for pull requests that team's members open, not for one from a stranger on a public repository. Add more conditions if you need them. For example, "Target branch is any of `main`", or leave out draft pull requests. 4. Choose **Preview**. Orbital shows what the rule would have done over the last seven days. Saving turns the rule on. :::caution Once saved, the rule starts runs on its own, with no one choosing **Start run**. Each opened pull request that passes **Only if** starts one. ::: ## 4. Open a pull request When someone opens a pull request, the rule starts a run within about a minute. The run's goal holds the pull request's address and title. The rules list shows when each rule last checked and what it did, such as "Started a run". ![The Triggers page listing rules with their source, an on or off switch, what each does and its recent firings.](/screenshots/triggers-page.webp?v=5fe1ad8f4b) *Each rule's line shows its last check and what it did.* ## What you get The review appears on the pull request as a comment. The run's last message in the **Session** tab summarises it. ## Keep it under control Each opened pull request starts one run, and each run takes a run slot while it works. Keep **Concurrent runs** in mind, and narrow the rule with conditions on a busy repository. See [The run queue](/runs/queue/). A rule can also stop, pause or message the runs that a trigger started for the same pull request. For example, a second rule on "closed" with the action **Stops runs** ends a review that is no longer needed. ## Related - [Triggers](/triggers/): what a rule watches and what it can do to runs - [Make a trigger rule](/triggers/make-a-rule/): build a rule of your own step by step - [Settings](/reference/settings/#experimental-features): turn on the Triggers feature - [The run queue](/runs/queue/): how many runs work at once --- # Improve a codebase > Deliver small improvements one at a time, or one focused refactor, without a ticket In this example you have Orbital improve a codebase without a ticket. Start the shipped `improve-codebase` workflow for one worthwhile improvement, or `reduce-complexity` for one focused refactor. About five minutes to start. Both workflows stop after one improvement, or immediately when nothing worthwhile is found. ![improve-codebase: discover a candidate, implement and deliver it, then stop; exit immediately when there is nothing to do.](/shipped/improve-codebase.svg?v=2321bf1a6d) ## Before you start :::caution Both workflows turn on auto-merge. Where your branch rules require no review, each pull request merges as soon as its required checks pass. See [turn auto-merge off](/projects/worktrees-and-delivery/#turn-auto-merge-off). ::: ## 1. Choose a workflow | Use | When | | --- | --- | | `improve-codebase` | You want one worthwhile improvement supported by evidence. | | `reduce-complexity` | You want one refactor of the most complex code. | ## 2. Start the run Follow the part for the workflow you chose. ### Run improve-codebase 1. Choose **New task**, pick the project and choose `improve-codebase`. ![The Start page asking what to do in a project, with a goal box, a preset picker, a workflow picker and a Chat or Workflow switch.](/screenshots/start-page.webp?v=7454bfb8bf) *Choose the workflow, then point it somewhere in the goal box.* 2. Point it somewhere, or leave the default goal, "Find and deliver one worthwhile improvement anywhere in the codebase." To narrow it, write your own, such as `Improve test coverage of the billing module`. 3. Choose **Start run**. The run finds one worthwhile improvement, delivers it as a merged pull request, and stops after cleanup: | Step | What it does | | --- | --- | | **discover** | Looks for one worthwhile improvement and says why. | | Deliver | If it finds one, the run makes a worktree, implements the improvement with a review loop, and delivers the pull request to merge. | | Clean up | The run cleans up and ends. | | No candidate | If it finds nothing worth doing, the run ends immediately without creating a worktree. | ### Run reduce-complexity 1. Choose **New task**, pick the project and choose `reduce-complexity`. 2. Optionally write a goal, such as `Only look at the src/api folder`. 3. Choose **Start run**. ![reduce-complexity: pick a target, refactor it in a worktree, deliver it.](/shipped/reduce-complexity.svg?v=7ec74b66eb) The pick step measures complexity and picks the function most worth simplifying. The run then refactors it in a worktree, delivers one pull request and stops. ## What you get `improve-codebase` delivers at most one improvement per run. When discovery finds nothing worthwhile, it succeeds without creating a worktree or pull request. `reduce-complexity` leaves one pull request with the refactor, except in two cases: | Outcome | When | | --- | --- | | Succeeded, without a worktree | Nothing is worth simplifying. | | Failed, without a pull request | The refactor was not safe. | ## Run it on a schedule With the Triggers feature on, the ready-made rule "Improve the codebase every weekday morning" starts a run at 9:00 on weekdays. Its goal is "Find and make one small improvement to the codebase". Choose `improve-codebase`, `reduce-complexity` or your own workflow for it. Each scheduled run of `improve-codebase` considers one improvement and ends. See [triggers](/triggers/). ![The New rule dialog set to a schedule of every weekday at 09:00, with the next run times listed.](/screenshots/trigger-rule-schedule.webp?v=5f91dd32fb) *A schedule rule lists its next runs before you save it.* ## Related - [Shipped workflows](/reference/shipped-workflows/): every step of `improve-codebase` and `reduce-complexity` - [Worktrees and delivery](/projects/worktrees-and-delivery/): how each improvement reaches a merged pull request - [Make a trigger rule](/triggers/make-a-rule/): start runs on a schedule --- # Triage issues > Propose a label, priority and next step for each open issue, without changing the tracker In this example you triage a backlog. The `triage-issues` workflow reads the issues you name and answers with one table that gives each issue a proposed label, a priority and a next step. It changes nothing in the tracker. You decide what to apply. About five minutes to set up. ```toml title="triage-issues.toml" version = 3 entry = "steps.triage" description = "Read an issue tracker's open issues and propose a label, a priority and a next step for each, changing nothing" style = "researcher" [inputs.goal] default = "Triage the open issues opened in the last seven days." [steps.triage] prompt_file = "prompts/triage-issues/triage.md" next = "steps.done" [steps.done] kind = "terminal" ``` ![triage has one step, then done.](/triage-issues.svg?v=9444b8fdb4) ## Before you start You need: - a signed-in harness with the connected service for your tracker. The agent reads the tracker with the connected services your harness has. For GitHub Issues or Linear, set up that service's MCP server in your harness first, for example Linear's MCP server in Claude Code; - any project. The workflow does not read the project's files, and the project does not need to be a Git repository, unless you run it with Codex, which needs one. See [plain folders and Git](/projects/#plain-folders-and-git); - the workflow: [download the archive](/triage-issues.zip). The workflow's top-level `style = "researcher"` gives the step the shipped `researcher` style. The prompt tells the agent not to change any issue or file, and the step changes nothing else. :::note The prompt is the only limit on what the agent does. Its harness and connected services can still change files or issues, so read the prompt before you start the run, and keep the [permissions](/styles/#permissions) of the style in mind. ::: ## 1. Add the workflow 1. Unpack the archive into your workflow library, `~/.orbital/workflows/`. Keep every relative path. 2. Check it: ```sh orbital validate ~/.orbital/workflows/triage-issues.toml ``` ![The Workflows page listing workflows with their source, Clone and Export actions, and Import and New workflow buttons.](/screenshots/workflows-page.webp?v=991e5fde13) *Once unpacked, the workflow shows on the Workflows page.* The archive holds the workflow above and its prompt: ```md title="prompts/triage-issues/triage.md" Triage the issues this goal names: {{ inputs.goal }} Read the issues with the tools your tracker offers, such as its connected service or its web pages. Read each issue's title, description and comments. Do not label, assign, comment on, close or otherwise change any issue, and do not change any file. For each issue, propose: 1. a label: bug, feature, question, documentation or duplicate; 2. a priority: urgent, soon or later, with one sentence on why; 3. a next step: ask the reporter a named question, reproduce it, link the duplicate, or ready to build. Answer with one table: issue, title, label, priority, next step. Under the table, list the issues you could not read and why. The table is the run's final answer, so end with it and nothing else. ``` ## 2. Start the run 1. Choose **New task**, pick any project and choose the `triage-issues` workflow. 2. Write which issues to triage, or leave the default: "Triage the open issues opened in the last seven days." For example: ```text Triage the open issues in the ENG team in Linear that have no label. ``` 3. Choose **Start run**. ## 3. Read the table The answer is the step's reply, the last message in the **Session** tab. It is one table with the issue, its title, the proposed label, priority and next step. Under the table, the agent lists any issue it could not read and why. ![A run page with an agent step selected, showing its prompt, the agent's answer and recorded values, with Run facts and a Steps list in the dock.](/screenshots/run-hero.webp?v=4fa21a573c) *The agent's answer holds the triage table.* ## 4. Apply what you agree with The run changes nothing in the tracker. Apply the labels, priorities and next steps you agree with in your tracker yourself. ## Make it yours - Change the labels and priorities in `triage.md` to the ones your team uses. - Run it every morning with a Schedule [trigger](/triggers/). - Give the step another style, or your own, with `style` at the top of `triage-issues.toml`. [Styles and personas](/styles/) explains what a style sets. ## Related - [Styles and personas](/styles/): what a style sets for a step - [Make a trigger rule](/triggers/make-a-rule/): run the triage every morning - [Write a research report](/examples/research-report/): another workflow that changes no files --- # Research report > Research a question on the web and write a short report with sources In this example you turn a question into a short written report with sources. One step of the `research-report` workflow researches the question on the web and lists what it found. A second step writes the report from those findings alone. About five minutes to set up. ```toml title="research-report.toml" version = 3 entry = "steps.research" description = "Research a question on the web and write a short report with sources" style = "researcher" [steps.research] prompt_file = "prompts/research-report/research.md" outputs.findings = "text" next = "steps.write" [steps.write] prompt_file = "prompts/research-report/write.md" next = "steps.done" [steps.done] kind = "terminal" ``` ![research passes its findings to write, then done ends the run.](/research-report.svg?v=f9f6c28b0d) ## Before you start You need a signed-in harness and the workflow: [download the archive](/research-report.zip). The workflow touches no code. Both steps take the shipped `researcher` style from the workflow's top-level `style`, and both prompts tell the agent not to change, create or delete any file. On Codex you can make that a hard limit with a style that sets `codex.sandbox_mode = "read-only"`; [Styles and personas](/styles/) shows how. ## 1. Add the workflow 1. Unpack the archive into your workflow library, `~/.orbital/workflows/`. Keep every relative path. 2. Check it: ```sh orbital validate ~/.orbital/workflows/research-report.toml ``` Every project now offers `research-report`. The archive holds the workflow above and its two prompts: ```md title="prompts/research-report/research.md" Research this question: {{ inputs.goal }} Search the web and read at least three independent sources. Prefer primary sources, such as official documentation, standards, papers or the organisation's own pages, over summaries of them. Do not change, create or delete any file. Return `findings`: a list of facts. Give each fact its source's title and address. Mark each fact as agreed by several sources, stated by one source, or disputed. Note what you could not find out. ``` ```md title="prompts/research-report/write.md" Write a short report that answers this question: {{ inputs.goal }} Use only these findings: {{ context.findings }} Write in plain language, in short sentences. Put the answer first, in two or three sentences. Then give the details under short headings. Say where the sources disagree and what nobody could confirm. End with a numbered list of sources, each with its title and address. Do not change, create or delete any file. The report is the run's final answer, so end with it and nothing else. ``` ## 2. Make a place for it to work Every run belongs to a [project](/projects/), and a step starts in the project's primary folder. Any project works, because the workflow reads no project files. The project does not need to be a Git repository, unless you run it with Codex, which needs one. See [plain folders and Git](/projects/#plain-folders-and-git). If you have no project yet, make one: 1. Make a folder such as `~/research`. 2. In Orbital, open **Projects**, choose **New project** and name it `Research`. 3. Choose **Add folder** and pick `~/research`. It shows as a Plain folder. ## 3. Start the run 1. Choose **New task**, pick any project and choose the `research-report` workflow. 2. Write the question as the goal. For example: ```text What are the main differences between the EU AI Act's obligations for providers and for deployers of high-risk AI systems? ``` 3. Leave the preset picker as it is, or choose a preset, and choose **Start run**. ![The Start page asking what to do in a project, with a goal box, a preset picker, a workflow picker and a Chat or Workflow switch.](/screenshots/start-page.webp?v=7454bfb8bf) *The preset picker sits beside the workflow picker.* ## 4. Read the report The **research** step's findings appear in the **Context** tab as `findings`. Each fact comes with its source and whether sources agree. ![A run's Context tab with a table of context keys, their values and the step that set each one.](/screenshots/run-context.webp?v=3e79bd8607) *Each value shows the step that set it.* The report is the **write** step's reply: the last message in the **Session** tab, above "Run ended: success". It gives the answer first, then the details, then a numbered list of sources. :::caution Check the sources yourself before you rely on the report. The agent can misread a page, and the web changes. ::: ## Make it yours - Ask for a different shape of report by editing `write.md`, for example a one-page brief for a manager. - Add a review step with the `reviewer` style that checks each fact in the report against its source. [Add branches](/workflows/add-branches/) shows how to send a rejected report back. - Start it every Monday with a Schedule [trigger](/triggers/). ## Related - [Pass context between steps](/workflows/pass-context/): how `findings` reaches the write step - [Add branches](/workflows/add-branches/): send a rejected report back for another try - [Triage issues](/examples/triage-issues/): another workflow that changes no files --- # One-shot implementation > The smallest workflow that delivers real work, from worktree to pull request In this example you run the smallest workflow that delivers real work. It takes an isolated checkout, implements once, reviews once, publishes a pull request and puts the checkout away. There is no repair loop and no waiting on the pull request. About five minutes to set up. ```toml title="one-shot.toml" version = 3 entry = "steps.checkout" description = "Implement one change in an isolated checkout, review it once and publish it" [inputs.work] label = "Work" hint = "The change to make, in plain words or as a ticket URL" [styles.quick] effort = "low" [steps.checkout] kind = "action" do = "worktree.create" next = "steps.implement" [steps.implement] style = "implementer" prompt = "Implement the change described by {{ inputs.work }}. Follow the conventions already in the repository. Do not commit or push." outputs.implementation_summary = "text" next = "steps.review" [steps.review] style = "reviewer" prompt = "Review the change in the working folder. The implementer reported: {{ context.implementation_summary }}. Choose pass when the change is complete and correct, or reject when it is not. Do not change files." outputs.verdict = { kind = "choice", options = ["pass", "reject"] } routes = [ { if = "verdict == 'pass'", to = "steps.publish.commit" }, { if = "verdict == 'reject'", to = "steps.rejected" }, ] [steps.publish] style = "quick" [steps.publish.commit] kind = "action" do = "git.commit" message = { write = "A commit message that says why the change exists" } 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" title = { write = "A short title naming the change" } body = { write = "Why the change exists, what it changes and how it was checked" } next = "steps.cleanup" [steps.cleanup] kind = "action" do = "worktree.remove" routes = [{ if = "git.worktree.clean", to = "steps.published" }] next = "steps.retained" [steps.published] kind = "terminal" [steps.rejected] kind = "terminal" outcome = "failed" label = "Checkout kept for inspection" [steps.retained] kind = "terminal" outcome = "failed" label = "Checkout retained to preserve local work" ``` ![checkout → implement → review. A passing review commits, pushes and opens a pull request, then cleans up; a rejected review keeps the checkout.](/one-shot.svg?v=5abb76da02) Use it when the change is small enough that a rejected review means you want to look at it yourself. ## Before you start You need: - a project whose primary folder is a Git repository with a GitHub remote you can push to, because the run works in a worktree and opens a pull request; - Git credentials that can push, and a GitHub token Orbital can use: a `gh auth login`, or a token in **Settings > GitHub**; - a signed-in harness; - the workflow: [download the archive](/one-shot.zip), or just [one-shot.toml](/examples/one-shot/one-shot.toml). ## 1. Add the workflow 1. Unpack the archive into your workflow library, `~/.orbital/workflows/`. Keep every relative path. 2. Check it: ```sh orbital validate ~/.orbital/workflows/one-shot.toml ``` ![The workflow editor showing a workflow as steps joined by arrows, with an Inspector panel on the right and a Problems bar.](/screenshots/workflow-editor.webp?v=85a9c6275a) *The editor draws the workflow's steps; the Problems bar lists what validation rejects.* ## 2. Read how it works `checkout` is an `action` step that does `worktree.create`. It makes one isolated checkout per repository folder on a generated branch, so the run never touches your working copy. `cleanup` is the matching `worktree.remove`. `implement` and `review` take the shipped `implementer` and `reviewer` styles. `review` declares a `verdict` choice output with the options `pass` and `reject`, and its routes answer both, so it needs no `next`. A rejection ends the run at a failed terminal and skips cleanup on purpose. Removal would refuse anyway with uncommitted work in the checkout, and the point of a rejection is that you want to read the diff. No agent publishes the change. The `publish` group holds three actions: `publish.commit` commits every change in the checkout, `publish.push` pushes the branch and `publish.open` opens the pull request. The commit message, the title and the body are each a `write`, a short model session with no tools that reads the run's context and returns only that text. The group's `style = "quick"` reaches the actions inside it, so each write runs at low effort. A write appears in the run history and does not count toward `max_visits`. [Push and open pull requests without an agent](/workflows/use-actions/) describes every action. Cleanup routes on `git.worktree.clean`. Removal measures every checkout first and removes none if any would lose dirty files or unpushed work. So the fallback ends at a failed terminal that names what happened, rather than pretending the run succeeded. ## 3. Start the run Choose **New task** and pick the project on the repository. Choose the `one-shot` workflow, set work to a description or ticket URL, and write the goal. ## What you get | Review | Result | | --- | --- | | Passes | A pull request is opened, and the run ends Succeeded with the checkout removed. | | Rejects | The run ends Failed with the checkout intact for you to inspect. | | Passes, but removal is refused | The run ends Failed and keeps the checkout. | ## Related - [Implementer with a local step](/examples/workflows/local-step/): add a local check before the end - [Ticket to merge](/examples/workflows/ticket/): follow the pull request through to merge - [Worktrees and delivery](/projects/worktrees-and-delivery/): how the `worktree.create` and `worktree.remove` actions make and remove checkouts --- # Implementer with a local step > An implementer checked by a command step, routing on what the machine observed In this example you put a shell command between the implementing agent and the end of the run, so the run routes on what the machine observed rather than on what the agent said. An agent that reports success is not evidence of success. About five minutes to set up. ```toml title="local-step.toml" version = 3 entry = "steps.implement" description = "Implement a change and prove it with a local command before finishing" max_visits = 40 [inputs.work] label = "Work" hint = "The change to make, in plain words or as a ticket URL" [budgets.local_check] rounds = 5 [steps.implement] prompt = "Implement the change described by {{ inputs.work }}. Change the files the work needs. Do not commit." outputs.implementation_summary = "text" next = "steps.observe" [steps.observe] kind = "command" repeat_safety = "idempotent" timeout = "2m" facts.change.present = "bool" facts.change.summary = "text" script = ''' if git status --porcelain | grep -q .; then echo '{"change.present":true,"change.summary":"the working folder has uncommitted changes"}' else echo '{"change.present":false,"change.summary":"the working folder is unchanged"}' fi ''' routes = [ { if = "outcome == 'failed'", to = "steps.broken" }, { if = "change.present", to = "steps.observed", resets = "budgets.local_check" }, { if = "change.present == false", to = "steps.repair", spends = "budgets.local_check" }, { if = "change.present == false", to = "steps.unchanged" }, ] next = "steps.broken" [steps.repair] prompt = "A local command observed that {{ context.change.summary }}. The reported work was: {{ context.implementation_summary }}. Change the files the work actually needs." outputs.repair_summary = "text" next = "steps.observe" [steps.observed] kind = "terminal" [steps.unchanged] kind = "terminal" outcome = "failed" label = "No change after five repairs" [steps.broken] kind = "terminal" outcome = "failed" label = "The local command itself failed" ``` ![implement → observe. The command routes an unchanged folder into repair and a changed one into a successful terminal.](/local-step.svg?v=251b65e802) ## Before you start You need: - a signed-in harness; - a project whose primary folder is a Git repository. It needs no remote. It must be a Git repository because the check asks `git status` whether the folder changed; - the workflow: [download the archive](/local-step.zip), or just [local-step.toml](/examples/local-step/local-step.toml). :::caution The workflow makes no worktree, so the agent changes files in the project's folder itself. Use a practice repository. ::: ## 1. Add the workflow 1. Unpack the archive into your workflow library, `~/.orbital/workflows/`. Keep every relative path. 2. Check it: ```sh orbital validate ~/.orbital/workflows/local-step.toml ``` ![The workflow editor showing a workflow as steps joined by arrows, with an Inspector panel on the right and a Problems bar.](/screenshots/workflow-editor.webp?v=85a9c6275a) *The editor draws the workflow's steps; the Problems bar lists what validation rejects.* ## 2. Read how it works `observe` is a `command` step with a `script`. The script runs once through `bash -c` in the primary working folder, inherits the server environment, and prints one JSON object as its final stdout line. `facts.change.present = "bool"` and `facts.change.summary = "text"` declare exactly the keys that object contains, and Orbital rejects a final line that does not match. The check here asks git whether the working folder has uncommitted changes. Replace it with your project's own check, such as its test or lint command, and declare whatever facts that command can report. Keep the script read-only when it only inspects something, and give it a `timeout` shorter than the default ten minutes when the command is quick. Four routes leave `observe`, tried in order, and `next` catches anything else: | Route | What it does | | --- | --- | | `outcome == 'failed'` | Catches the script itself failing on a timeout, a non-zero exit or a malformed final line. | | `change.present` | Succeeds and resets the `local_check` budget. | | `change.present == false`, spending `budgets.local_check` | Starts a repair round. Once the five rounds are spent, this route is skipped. | | `change.present == false` | Ends the run Failed once the budget is empty. | | `next` | Required, because a command step's facts are not a choice whose routes answer every option. | ## 3. Make a practice repository Run `git init ~/orbital-practice` and add that folder as a project. ## 4. Start the run Choose **New task** and pick the project. Choose the `local-step` workflow, set work to what you want changed, and write the goal. ## What you get An agent that changes a file reaches the successful terminal on the first observation. An agent that changes nothing is sent to repair and observed again, up to five rounds, before the run ends Failed. A script that cannot run at all ends the run Failed at a separate terminal, so you can tell the two causes apart in the transcript. ## Related - [Context](/reference/context/): how to declare the facts a command reports - [Error handling](/reference/error-handling/#bounded-repair): how budgets bound a loop - [One-shot implementation](/examples/workflows/one-shot-implementation/): the smallest workflow that delivers a pull request --- # Outer loop over an epic > An outer loop that delivers an epic's tickets through a sub workflow In this example you work through all of an epic's sub tickets in one run. The outer loop selects the next unfinished sub ticket, hands it to an imported delivery workflow, and comes back to select again. It stops when the select step reports that nothing remains. About five minutes to set up. The run can take a long time, one delivery per sub ticket. ```toml title="epic.toml" version = 3 entry = "steps.select" description = "Work through an epic one sub ticket at a time" max_visits = 200 [inputs.work] kind = "epic" label = "Epic" hint = "The epic whose sub tickets this run delivers" [steps.select] prompt = """ Read the epic at {{ inputs.work.url }} and every sub ticket under it. Choose the next unfinished sub ticket and report it as the current work. Choose ticket while one remains, or none once every sub ticket is finished. """ outputs.current_work = "work" outputs.remaining = { kind = "choice", options = ["ticket", "none"] } routes = [ { if = "remaining == 'ticket'", to = "steps.ticket" }, { if = "remaining == 'none'", to = "steps.finished" }, ] [steps.ticket] import = "imports/ticket.toml" exits.delivered = { to = "steps.select", loop_restart = true } exits.rejected = "steps.stalled" [steps.finished] kind = "terminal" [steps.stalled] kind = "terminal" outcome = "failed" label = "A sub ticket could not be delivered" ``` ![select routes a remaining ticket into the imported sub workflow and returns; no remaining ticket ends the run, and a sub ticket the reviewer will not pass ends it failed.](/epic.svg?v=1609c64e01) ## Before you start You need: - a signed-in harness that can read your tracker; - a project whose primary folder holds the code the sub tickets change. The example makes no worktree, so the agent changes files in that folder itself. A plain folder works, except with Codex. See [plain folders and Git](/projects/#plain-folders-and-git); - the workflow: [download the archive](/epic.zip). It holds `epic.toml` and `imports/ticket.toml`. ## 1. Add the workflow 1. Unpack the archive into your workflow library, `~/.orbital/workflows/`. Keep every relative path, so `imports/ticket.toml` sits beside `epic.toml`. 2. Check it: ```sh orbital validate ~/.orbital/workflows/epic.toml ``` ![The workflow editor showing a workflow with an imported sub workflow drawn as one step.](/screenshots/workflow-editor-imports.webp?v=3f00eb3b61) *The imported sub workflow shows as one step in its parent.* ## 2. Read how it works The `work` input has the `epic` kind, so Orbital looks the epic up when the run starts and refuses an address it cannot read. The select prompt reads that address as `{{ inputs.work.url }}`. `select` declares two outputs. `remaining` is a choice of `ticket` or `none`, and its two routes answer both, so the step needs no `next`. `current_work` matters more. The `work` kind accepts a validated work scope rather than free text. A select step that returns a vague description, or a ticket URL that is its own parent, fails validation instead of sending the implementing agent somewhere unhelpful. Any prompt downstream renders that scope with `{{ current_work }}`. `ticket` is an import step. `import = "imports/ticket.toml"` brings the other file's steps into this workflow as a group, so they run as `ticket.implement` and `ticket.review`. Each terminal of the imported file is an exit named by its id, and the import step's `exits` say where each one goes: `delivered` returns to the select step, and `rejected` goes to `stalled`, which ends the run Failed. An exit without a mapping would end the run with that terminal's outcome. The `delivered` exit sets `loop_restart = true`. That clears the previous sub ticket's context, the history of its steps and its agent sessions. It keeps the original inputs, the run identity, visit counts and budgets. Without it, the tenth sub ticket would carry nine sub tickets of history into every prompt. Bounding the inner review loop needs a budget rather than a visit cap. `max_visits` counts every visit to a step for the life of the run, and a restarted loop keeps those counts. So a cap of four on `ticket.implement` would bound the whole epic at four sub tickets rather than four attempts at one of them. The imported file declares `[budgets.sub_ticket]` instead, and three routes leave `review`. A reject spends a round and goes back to `implement`. Once the five rounds are spent, that route is skipped and the next reject route ends at `rejected`. A pass resets the budget, so the next sub ticket starts fresh. The workflow's `max_visits` remains the outer guard on total agent turns. ```toml title="imports/ticket.toml" version = 3 entry = "steps.implement" description = "Implement and locally review one sub ticket" [budgets.sub_ticket] rounds = 5 [steps.implement] prompt = "Implement the current work described below. Satisfy only its requirements. {{ current_work }}" outputs.implementation_summary = "text" next = "steps.review" [steps.review] prompt = "Review the implementation against the current work described below. Choose pass when it satisfies the work, or reject when it does not. {{ current_work }}" outputs.verdict = { kind = "choice", options = ["pass", "reject"] } routes = [ { if = "verdict == 'pass'", to = "steps.delivered", resets = "budgets.sub_ticket" }, { if = "verdict == 'reject'", to = "steps.implement", spends = "budgets.sub_ticket" }, { if = "verdict == 'reject'", to = "steps.rejected" }, ] [steps.delivered] kind = "terminal" [steps.rejected] kind = "terminal" outcome = "failed" label = "Five reviews rejected the same sub ticket" ``` [Download epic.toml](/examples/epic/epic.toml) or [ticket.toml](/examples/epic/imports/ticket.toml) on its own. Replace the sub workflow with your own delivery workflow. The [ticket workflow](/examples/workflows/ticket/) is the realistic one: it publishes a pull request, watches its state and merges. ## 3. Start the run Choose **New task** and pick the project that holds the code the sub tickets change. Choose the `epic` workflow, set work to the epic's URL and write the goal. :::tip Watch the first two iterations before you leave the run alone. ::: ## What you get Each iteration selects one sub ticket, implements and reviews it, then returns to the select step with a cleared context. A select step that reports none ends the run Succeeded. When the reviewer keeps rejecting the same sub ticket after five repairs, the budget is spent and the run ends Failed at `stalled`, labelled "A sub ticket could not be delivered". ## Related - [Reuse a workflow](/workflows/reuse-a-workflow/): how a sub workflow's terminals become exits - [Add a loop](/workflows/add-a-loop/): the other ways to bound a loop - [Ticket to merge](/examples/workflows/ticket/): a realistic delivery workflow to import instead - [Deliver an epic](/examples/deliver-an-epic/): the shipped epic workflow --- # Ticket to merge > A complete workflow that takes a ticket to a merged pull request, like the shipped delivery sub workflows In this example you run the complete delivery workflow. It implements a ticket, publishes a pull request, watches it, repairs what CI and reviewers find, and merges. It imports two sub workflows and ships twelve prompt files. About ten minutes to set up, including reading the prompts. The run then works until the pull request merges or closes. ```toml title="ticket.toml" version = 3 entry = "steps.create_worktree" description = "Deliver one ticket through reviewed implementation and a merged PR" [inputs.work] label = "Work" hint = "The ticket to deliver, as a URL, with anything the run should know" [steps.create_worktree] kind = "action" do = "worktree.create" next = "steps.implementation" [steps.implementation] import = "imports/implementation.toml" inputs.work = "{{ inputs.work }}" exits.approved = "steps.delivery" [steps.delivery] import = "imports/pr-delivery.toml" inputs.work = "{{ inputs.work }}" exits.merged = "steps.complete_ticket" exits.closed = "steps.cleanup.closed" [steps.complete_ticket] prompt_file = "prompts/complete-ticket.md" outputs.completion_summary = "text" next = "steps.cleanup.merged" [steps.cleanup.merged] kind = "action" do = "worktree.remove" routes = [{ if = "git.worktree.clean", to = "steps.complete" }] next = "steps.worktree_retained" [steps.cleanup.closed] kind = "action" do = "worktree.remove" routes = [{ if = "git.worktree.clean", to = "steps.closed" }] next = "steps.cleanup_pending" [steps.complete] kind = "terminal" [steps.closed] kind = "terminal" outcome = "failed" label = "PR closed without merging" [steps.worktree_retained] kind = "terminal" label = "Delivered; worktree retained for the unpushed commits git.worktree.status names" [steps.cleanup_pending] kind = "terminal" outcome = "failed" label = "Worktree retained to preserve local work" ``` ![create_worktree → implementation → delivery → complete_ticket → cleanup. Delivery loops through PR observations and repairs until merged or closed. Cleanup retains unsafe worktrees.](/ticket.svg?v=403db0edd1) ## Before you start You need: - a project whose primary folder is the Git repository the ticket changes, with a GitHub remote you can push to, because the run works in a worktree and opens a pull request. An empty practice repository is not enough for this workflow; - a GitHub token and a signed-in harness. The agent needs access to the repository and the tracker, and permission to publish and repair the pull request; - the workflow: [download the archive](/ticket.zip). Read all prompts before running. They are listed under [complete source](#complete-source). :::caution Like the shipped workflows, the publish step sets `auto_merge = "squash"`, so Orbital turns on auto-merge for the pull request. Where your branch rules require no review, the pull request merges as soon as its required checks pass. Delete that key from `imports/pr-delivery.toml` to merge by hand. See [turn auto-merge off](/projects/worktrees-and-delivery/#turn-auto-merge-off). ::: ## 1. Add the workflow 1. Unpack the archive into your workflow library, `~/.orbital/workflows/`. Keep every relative path. 2. Check it: ```sh orbital validate ~/.orbital/workflows/ticket.toml ``` ![The workflow editor showing a pull-request workflow whose routes loop back to earlier steps.](/screenshots/workflow-editor-loop.webp?v=4dacb0ba6a) *Routes loop back until the pull request merges or closes.* ## 2. Read how it works `ticket.toml` makes the worktree with the `worktree.create` action, then imports two files as steps. `implementation` brings in `imports/implementation.toml`, and `delivery` brings in `imports/pr-delivery.toml`. Each import step passes the run's `work` input on with `inputs.work`, and its `exits` say where each terminal of the imported file leads: `approved` goes on to delivery, `merged` to `complete_ticket`, and `closed` to cleanup. Imported steps run under dotted ids, such as `delivery.pr.check`, and that is how the run history names them. In `pr-delivery.toml`, the `publish` agent pushes the branch and opens the pull request, and its `auto_merge` asks GitHub to merge it once its outputs are accepted. The `pr.check` probe observes `github.pr` and `github.threads`, and its routes read `github.pr.state`: `MERGED` and `CLOSED` leave through an exit, while `CONFLICTS`, `CI_FAILED` and `CHANGES_REQUIRED` each go to their own repair. When none of them holds, `next` goes to the `pr.wait` gate. Each repair reports whether it changed anything. A change goes to `fix.review` before it is published again, and no change goes straight back to `publish`. The two `worktree.remove` actions sit in a `cleanup` group, one for a merged pull request and one for a closed one. Each routes on `git.worktree.clean`, and its `next` keeps the worktree when removing it would lose work. ## 3. Start the run Choose **New task**, pick the project and choose the `ticket` workflow. Set work to the ticket URL and write the delivery goal. ## What you get Implementation plans, implements and reviews until approved. Delivery publishes the pull request, reads its state, handles CI, conflicts and reviews, and waits at a human gate while the pull request waits for review or merge. The waiting run shows an eye and "Waiting: The pull request is waiting for review or merge", takes no run slot, and carries on by itself when the pull request changes. An approved pull request that is not merged yet returns to the gate. | The pull request | Result | | --- | --- | | Merges | The run completes the ticket, cleans up and ends Succeeded. | | Merges, but the worktree still holds commits the remote never saw | The run ends Succeeded and keeps the worktree. `git.worktree.status` names those commits. | | Closes without merging | The run cleans up and ends Failed, labelled "PR closed without merging". | | Closes, and cleanup would lose work | The run ends Failed and keeps the worktree. | ## The review loop has no budget `imports/implementation.toml` loops `local_review → revise.plan → revise.implement → local_review` until the review passes. It has no budget, because it mirrors the shipped `implementation` sub workflow, which has none. The workflow's `max_visits` bounds the loop instead. It defaults to 200 agent turns for the whole run, and when they are spent the run ends Failed. For a tighter bound, declare a budget in `imports/implementation.toml`, spend it on the reject route, reset it on the pass route, and add a second reject route to a failed terminal: ```toml [budgets.review] rounds = 5 [steps.local_review] routes = [ { if = "review_verdict == 'pass'", to = "steps.approved", resets = "budgets.review" }, { if = "review_verdict == 'reject'", to = "steps.revise.plan", spends = "budgets.review" }, { if = "review_verdict == 'reject'", to = "steps.rejected" }, ] [steps.rejected] kind = "terminal" outcome = "failed" label = "Five revisions did not pass review" ``` After five rejected rounds the spending route is skipped, and the next reject takes the route to `rejected`. That terminal is a new exit of the imported file. `ticket.toml` does not map it, so reaching it ends the run Failed. [Error handling](/reference/error-handling/#bounded-repair) describes the rules, and [outer loop over an epic](/examples/workflows/epic/) uses such a budget. ## Complete source Every TOML file below is a complete workflow, and the archive holds the same files. The Markdown files are prompts the workflows name, not separate workflows. `ticket.toml` is at the top of this page. [Download ticket.toml](/examples/ticket/ticket.toml) on its own. ### imports/implementation.toml ```toml title="imports/implementation.toml" version = 3 entry = "steps.plan" description = "Plan, implement and locally review one work scope" [inputs.work] label = "Work" [steps.plan] prompt_file = "../prompts/implementation/plan.md" outputs.implementation_plan = "text" next = "steps.implement" [steps.implement] prompt_file = "../prompts/implementation/implement.md" outputs.implementation_summary = "text" next = "steps.local_review" [steps.local_review] prompt_file = "../prompts/implementation/review.md" outputs.review_verdict = { kind = "choice", options = ["pass", "reject"] } outputs.review_findings = { kind = "json", required_if = "review_verdict == 'reject'" } routes = [ { if = "review_verdict == 'pass'", to = "steps.approved" }, { if = "review_verdict == 'reject'", to = "steps.revise.plan" }, ] [steps.revise.plan] prompt_file = "../prompts/implementation/plan-review.md" outputs.implementation_plan = "text" next = "steps.revise.implement" [steps.revise.implement] prompt_file = "../prompts/implementation/implement-review.md" outputs.implementation_summary = "text" next = "steps.local_review" [steps.approved] kind = "terminal" ``` [Download implementation.toml](/examples/ticket/imports/implementation.toml) ### imports/pr-delivery.toml ```toml title="imports/pr-delivery.toml" version = 3 entry = "steps.publish" description = "Publish a change, then wait on its review and publish each repair until the PR is merged or closed" [inputs.work] label = "Work" [steps.publish] prompt_file = "../prompts/pr-delivery/publish.md" outputs.publication_summary = "text" auto_merge = "squash" next = "steps.pr.check" [steps.pr.check] kind = "probe" observe = ["github.pr", "github.threads"] routes = [ { if = "outcome == 'failed'", to = "steps.closed" }, { if = "github.pr.state == 'MERGED'", to = "steps.merged" }, { if = "github.pr.state == 'CLOSED'", to = "steps.closed" }, { if = "github.pr.state == 'CONFLICTS'", to = "steps.fix.conflicts" }, { if = "github.pr.state == 'CI_FAILED'", to = "steps.fix.ci" }, { if = "github.pr.state == 'CHANGES_REQUIRED'", to = "steps.fix.reviews" }, ] next = "steps.pr.wait" [steps.pr.wait] kind = "gate" label = "The pull request is waiting for review or merge" next = "steps.pr.check" [steps.fix.reviews] prompt_file = "../prompts/pr-delivery/address-reviews.md" outputs.repair_summary = "text" outputs.repair_result = { kind = "choice", options = ["changed", "unchanged"] } routes = [ { if = "repair_result == 'changed'", to = "steps.fix.review" }, { if = "repair_result == 'unchanged'", to = "steps.publish" }, ] [steps.fix.ci] prompt_file = "../prompts/pr-delivery/fix-ci.md" outputs.repair_summary = "text" outputs.repair_result = { kind = "choice", options = ["changed", "unchanged"] } routes = [ { if = "repair_result == 'changed'", to = "steps.fix.review" }, { if = "repair_result == 'unchanged'", to = "steps.publish" }, ] [steps.fix.conflicts] prompt_file = "../prompts/pr-delivery/resolve-conflicts.md" outputs.repair_summary = "text" outputs.repair_result = { kind = "choice", options = ["changed", "unchanged"] } routes = [ { if = "repair_result == 'changed'", to = "steps.fix.review" }, { if = "repair_result == 'unchanged'", to = "steps.publish" }, ] [steps.fix.review] prompt_file = "../prompts/pr-delivery/review-repairs.md" outputs.review_verdict = { kind = "choice", options = ["pass", "reject"] } outputs.review_findings = { kind = "json", required_if = "review_verdict == 'reject'" } routes = [ { if = "review_verdict == 'pass'", to = "steps.publish" }, { if = "review_verdict == 'reject'", to = "steps.fix.revise" }, ] [steps.fix.revise] prompt_file = "../prompts/pr-delivery/revise-repairs.md" outputs.repair_summary = "text" next = "steps.publish" [steps.merged] kind = "terminal" [steps.closed] kind = "terminal" outcome = "failed" ``` [Download pr-delivery.toml](/examples/ticket/imports/pr-delivery.toml) ### prompts/complete-ticket.md ```markdown title="prompts/complete-ticket.md" Discover and use relevant installed skills for completing tickets and communicating delivery results. Ticket: {{ context.work }} Publication summary: {{ context.publication_summary }} Confirm from the remote that the PR delivering this work has merged. Verify that the merged change satisfies the ticket and goal. Identify any ticket-related changes that remain unpublished before marking the work complete. Mark the supplied ticket done using its tracker’s workflow. If the ticket is already complete, verify the recorded delivery evidence and avoid duplicate updates. Leave parent, sibling and unrelated tickets unchanged. This step may update the supplied ticket's completion record. Leave repository files, commits, branches and pull requests unchanged. Return `completion_summary` with the ticket’s confirmed status, merged PR link and delivery evidence. A follow-up step cleans up the worktree. ``` [Download complete-ticket.md](/examples/ticket/prompts/complete-ticket.md) ### prompts/implementation/implement-review.md ```markdown title="prompts/implementation/implement-review.md" Discover and use relevant installed skills for implementation and verification. Work scope: {{ context.work }} Revised implementation plan: {{ context.implementation_plan }} Previous implementation summary: {{ context.implementation_summary }} Review findings: {{ context.review_findings | dump }} Inspect the current changes and applicable repository instructions. Apply the revised plan and address every blocking finding. Preserve correct work and remove changes the plan identifies as outside scope. Verify each correction and run the relevant checks for the complete change. Close all remaining defects, unmet requirements or verification gaps in the work scope and the plan. Address all required changes from the plan and review. Create or update local commits using the repository’s conventions. This step may change repository files, run checks and create local commits. Later steps own local review and publication. The caller owns any tracker updates. Return an updated `implementation_summary` that replaces the previous one. Describe the change as it stands now, then how this round corrected and verified each finding above, then anything still unresolved, stated once. Leave out earlier rounds, their findings and any dispute already settled: later steps read only this summary and the code, so history in it only makes every later prompt longer. ``` [Download implement-review.md](/examples/ticket/prompts/implementation/implement-review.md) ### prompts/implementation/implement.md ```markdown title="prompts/implementation/implement.md" Discover and use relevant installed skills for implementation and verification. Work scope: {{ context.work }} Implementation plan: {{ context.implementation_plan }} Implement the planned change in the prepared worktree. If the work scope names an open pull request, check out its branch first and build on it, so publication updates that pull request instead of opening another. Follow applicable repository instructions and preserve unrelated work. Satisfy every requirement in the work scope and goal. If the code reveals an error in the plan, make the necessary adjustment within that scope and explain it. Run the relevant checks. Record what each check establishes and any failures or verification gaps. Create or update local commits using the repository’s conventions. This step may change repository files, run checks and create local commits. Later steps own publication. The caller owns any tracker updates. Return `implementation_summary` with the changes, verification evidence, deviations from the plan and any unmet requirements. ``` [Download implement.md](/examples/ticket/prompts/implementation/implement.md) ### prompts/implementation/plan-review.md ```markdown title="prompts/implementation/plan-review.md" Discover and use relevant installed skills for planning and communicating the plan. Work scope: {{ context.work }} Previous implementation plan: {{ context.implementation_plan }} Implementation summary: {{ context.implementation_summary }} Review findings: {{ context.review_findings | dump }} Read the work scope, applicable repository instructions and current changes. Investigate each blocking finding against the code and available evidence. Revise the implementation plan to address every blocking finding. Specify each correction and how it will be verified. Preserve work that already satisfies the requirements and identify any changes that must be removed or revised. Keep the revised plan complete enough for implementation. Cover the work scope and goal without adding unrelated work. State assumptions and material risks. Return the revised plan as `implementation_plan`. Leave repository files and tracker records unchanged. Later steps will implement and publish the changes. The caller owns any tracker updates. ``` [Download plan-review.md](/examples/ticket/prompts/implementation/plan-review.md) ### prompts/implementation/plan.md ```markdown title="prompts/implementation/plan.md" Discover and use relevant installed skills for planning and communicating the plan. Work scope: {{ context.work }} Read the supplied work scope, relevant linked context and applicable repository instructions. Inspect the affected code and tests. For ticket-based work, read the identified ticket. Plan the changes needed to satisfy the work scope. Use the goal to guide decisions without expanding that scope. Choose the smallest implementation that meets every requirement. Explain the required behaviour, affected components, implementation order and how each acceptance criterion will be verified. Ground decisions in the inspected code. Return the plan as `implementation_plan`. Leave repository files and tracker records unchanged. Later steps will implement and publish the changes. ``` [Download plan.md](/examples/ticket/prompts/implementation/plan.md) ### prompts/implementation/review.md ```markdown title="prompts/implementation/review.md" Discover and use relevant installed skills for code review and communicating findings. Work scope: {{ context.work }} Implementation plan: {{ context.implementation_plan }} Implementation summary: {{ context.implementation_summary }} Review the current change against the work scope, goal, plan and applicable repository instructions. Inspect the complete branch diff against its target, including uncommitted changes, affected code and relevant tests. Review only requirements and defects that can be verified through automated checks or code inspection. For each rejection, identify the requirement, concrete evidence, required correction and machine-checkable completion condition. Use the work scope, plan and summary as context. Verify their claims against the code and available evidence. Reject unmet requirements, correctness or security defects, violations of mandatory repository rules, missing verification of required behaviour and changes made which are outside the work scope. Treat preferences as non-blocking unless repository instructions require them. Human reviews, manual acceptance, visual approval and user sign-off are non-blocking. Report them as outstanding without requiring their completion to pass. Compare each finding with previous reviews and attempted corrections. If the same finding returns without new evidence or a materially different actionable correction, recognise that the review is looping. Do not reject again for that finding. Report the unresolved concern and explain why another repair pass would not advance it. For each blocking finding, identify the requirement or defect, supporting evidence and the correction needed. This step is read-only. Leave files, commits, branches, pull requests and tracker records unchanged. Return `review_verdict` as `reject` only when actionable, machine-checkable blocking findings remain. Otherwise return `pass`. Keep outstanding human or manual checks and concerns excluded. Return `review_findings` as an array containing those findings, or an empty array when the review passes. ``` [Download review.md](/examples/ticket/prompts/implementation/review.md) ### prompts/pr-delivery/address-reviews.md ```markdown title="prompts/pr-delivery/address-reviews.md" Discover and use relevant installed skills for addressing pull request feedback, verification and communicating with reviewers. Work scope: {{ context.work }} Publication summary: {{ context.publication_summary }} The review threads still open, one repository at a time: {% for name, folder in run.folders %}{% if context[name].github.threads.open %}### {{ name }} {{ context[name].github.threads.open }} {% endif %}{% endfor %} Inspect the current PR, its published head, review decisions and unresolved threads. Read the affected code and applicable repository instructions. Address feedback within the work scope and goal. Make the required local changes, verify them and create or update local commits using the repository’s conventions. Preserve unrelated work. For feedback already satisfied by the published change, reply with supporting evidence and resolve the thread. For feedback requiring a new local change, keep the thread unresolved until that correction is published and verified. Check existing replies before posting to avoid duplicates. Explain with evidence when a requested change is unnecessary or outside scope. If the PR has merged or closed, report that state without modifying it. This step may change repository files, run checks, create local commits, reply to reviews and resolve addressed threads. Later steps own local review and publication. The caller owns any tracker updates. Return `repair_summary` with the feedback addressed, changes, verification evidence, published corrections confirmed, and threads awaiting publication or further resolution. Return `repair_result` as `changed` when this step changed the local branch or its files, otherwise `unchanged`. A changed repair is reviewed against its reason before it is published. An unchanged one goes straight to publication. ``` [Download address-reviews.md](/examples/ticket/prompts/pr-delivery/address-reviews.md) ### prompts/pr-delivery/fix-ci.md ```markdown title="prompts/pr-delivery/fix-ci.md" Discover and use relevant installed skills for diagnosing CI failures, implementing corrections and verification. Work scope: {{ context.work }} Publication summary: {{ context.publication_summary }} Get the PR's required CI checks green. Inspect the current PR, published head and failing checks. Read their logs, affected code and applicable repository instructions. Confirm the failures belong to the current published change. Identify the cause of each failure. Use focused local checks where possible and compare against the target branch when needed to distinguish defects in this change from existing failures or external problems. Correct failures within the work scope and goal. Preserve unrelated work. Rerun a failed remote check when the evidence supports a transient failure; do not rerun checks repeatedly without investigating. Verify the corrections and create or update local commits using the repository’s conventions. If the PR has merged or closed, report that state without modifying it. This step may change repository files, run checks, rerun remote CI and create local commits. Later steps own local review and publication. The caller owns any tracker updates. Return `repair_summary` with the failed checks, diagnosis, corrections, verification results and anything still unresolved. Return `repair_result` as `changed` when this step changed the local branch or its files, otherwise `unchanged`. A changed repair is reviewed against its reason before it is published. An unchanged one goes straight to publication. ``` [Download fix-ci.md](/examples/ticket/prompts/pr-delivery/fix-ci.md) ### prompts/pr-delivery/publish.md ```markdown title="prompts/pr-delivery/publish.md" Discover and use relevant installed skills for publishing pull requests and communicating the change. Work scope: {{ context.work }} Inspect the prepared worktree, current branch and any associated pull request. Push the intended local commits. Update the existing open PR, or create one against the repository’s intended target branch. Describe the complete change, its relationship to the work scope and the verification performed. When the commit messages record review decisions, state each decision and its reason in the description. Keep an existing `Open items` section, and remove an item only when the published change resolves it. Use ticket references only when the work scope identifies an existing ticket. When it does, write `Closes #N` in the description if the change completes that ticket, and `Part of #N` if it only advances it. If the associated PR is already merged and the branch contains follow up changes, open a new PR. Confirm the PR URL from the remote. If publication fails, inspect the current remote state before retrying. Preserve local work and avoid creating duplicate PRs. This step owns publication. Leave implementation files and tracker records unchanged. Later steps handle PR feedback, CI and conflicts. The caller owns any tracker updates. Return `publication_summary` with the PR URL, confirmed remote state and published commits. ``` [Download publish.md](/examples/ticket/prompts/pr-delivery/publish.md) ### prompts/pr-delivery/resolve-conflicts.md ```markdown title="prompts/pr-delivery/resolve-conflicts.md" Discover and use relevant installed skills for resolving merge conflicts and verifying the resulting change. Work scope: {{ context.work }} Publication summary: {{ context.publication_summary }} Make the PR mergeable by resolving its conflicts with the target branch. Inspect the current PR, its target branch, published head and prepared worktree. Read applicable repository instructions. If the PR has merged or closed, report that state without modifying it. Fetch the latest target branch and integrate it using the repository’s conventions. Prefer rebase. Resolve conflicts by understanding both changes. Preserve the required behaviour of the work scope, relevant target-branch changes and unrelated work. Verify that the local branch integrates cleanly with the fetched target. Run the relevant checks and create or update local commits using the repository’s conventions. If the conflict has already disappeared, report the evidence without introducing unnecessary changes. This step may fetch remote changes, merge or rebase locally, resolve conflicts, run checks and create local commits. Later steps own local review and publication. The caller owns any tracker updates. Return `repair_summary` with the target revision, conflicts resolved, resulting changes, verification evidence and anything still unresolved. Return `repair_result` as `changed` when this step changed the local branch or its files, otherwise `unchanged`. A changed repair is reviewed against its reason before it is published. An unchanged one goes straight to publication. ``` [Download resolve-conflicts.md](/examples/ticket/prompts/pr-delivery/resolve-conflicts.md) ### prompts/pr-delivery/review-repairs.md ```markdown title="prompts/pr-delivery/review-repairs.md" Discover and use relevant installed skills for code review and communicating findings. Publication summary: {{ context.publication_summary }} Repair summary: {{ context.repair_summary }} The published head each repair started from, and the reason for the repair, one repository at a time: {% for name, folder in run.folders %}### {{ name }} Started from: {{ context[name].github.pr.head }} Reason: {{ context[name].github.pr.state }} {% if context[name].github.pr.state == "CHANGES_REQUIRED" %} The review threads the repair answers: {{ context[name].github.threads.open }} {% endif %} {% endfor %} A `CI_FAILED` repair answers the failing checks on the pull request. A `CONFLICTS` repair answers the merge conflict with the target branch. A `CHANGES_REQUIRED` repair answers the review threads above. A repository whose reason is `WAITING` needed no repair. Review only what the repair step changed. Inspect the diff from the head it started from to the current local head, including uncommitted changes. When the repair rebased onto the target branch, compare the change before and after the rebase, for example with `git range-diff`, and review only the resolution. Read the failing checks, the conflict or the review threads to judge the repair against its reason. The complete change passed review before it was published. Do not review it again against the ticket, and do not search for defects the repair did not introduce. Verify the corrections and verification claims in the repair summary against the code and available evidence. Reject a repair only when it is wrong, incomplete for its reason, breaks a mandatory repository rule, or changes things outside its reason. Treat preferences as non-blocking unless repository instructions require them. This step is read-only. Leave files, commits, branches, pull requests and tracker records unchanged. Return `review_verdict` as `reject` when blocking findings remain, otherwise `pass`. Return `review_findings` as an array of blocking findings, each with `requirement`, `evidence` and `correction`. A pass returns an empty array. A pass publishes the repair. A reject goes to one revision step, which fixes what it can, records the rest as open items in the pull request description and publishes without another review. ``` [Download review-repairs.md](/examples/ticket/prompts/pr-delivery/review-repairs.md) ### prompts/pr-delivery/revise-repairs.md ```markdown title="prompts/pr-delivery/revise-repairs.md" Discover and use relevant installed skills for implementation and verification. Work scope: {{ context.work }} Publication summary: {{ context.publication_summary }} Previous repair summary: {{ context.repair_summary }} Review findings: {{ context.review_findings | dump }} Inspect the current changes and applicable repository instructions. Address each blocking finding you can fix within this step's permissions. Preserve correct work and remove changes identified as outside the repair's reason. Verify each correction and run the relevant checks for the complete change. Create or update local commits using the repository’s conventions. Some findings cannot be fixed here: they need a tracker change, a decision from a person, or a change this step may not make. Record each one, and each finding you judged wrong, as an open item in the pull request description. Keep the rest of the description, add the items under an `Open items` heading, and state for each the finding and why it was not fixed here. Do not repeat an item already listed. This step may change repository files, run checks, create local commits and edit the pull request description. Leave the PR's commits, reviews and tracker records otherwise unchanged. Return an updated `repair_summary` that replaces the previous one. Describe the repair as it stands now, then how this step corrected and verified each finding above, then the open items it recorded, stated once. Leave out earlier rounds and any dispute already settled: later steps read only this summary and the code, so history in it only makes every later prompt longer. The next step publishes these changes without another review and continues monitoring the PR. ``` [Download revise-repairs.md](/examples/ticket/prompts/pr-delivery/revise-repairs.md) ## Related This is the workflow the shorter examples build up to. - [One-shot implementation](/examples/workflows/one-shot-implementation/): compare it to see what waiting on a pull request adds - [Outer loop over an epic](/examples/workflows/epic/): run this kind of delivery once per sub ticket - [Add gates and waits](/workflows/gates-and-waits/): how the run waits on the pull request - [Worktrees and delivery](/projects/worktrees-and-delivery/): how the pull request is published and merged --- # 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 =`. 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.` 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. A [map](/reference/step-kinds/#map) holds the steps of the branch it runs once per item. ### Maps A `map` step names a list in context with `over` and runs its nested steps once per item, in parallel. The first nested step starts each item's branch, which reads `{{ item }}` and `{{ index }}`; a nested step with no `next` or `routes` ends it. `max_items` is required. ```toml [steps.audit] kind = "map" over = "outputs.plan.rules" max_parallel = 4 max_items = 50 next = "steps.summarise" [steps.audit.check] prompt = "Audit billing rule {{ item.id }}: {{ item.text }}. Find its tests and say whether they cover it." outputs.covered = { kind = "choice", options = ["yes", "partly", "no"] } outputs.gaps = "text" ``` Later steps read `outputs.audit.results`, one entry per item in item order with its `item`, `index`, `outcome` and `outputs`, and the counts `outputs.audit.succeeded` and `outputs.audit.failed`. When some items fail, the map's `outcome` is `partial`. `fail_fast`, `reuse = "unchanged"` with its `inputs`, restarts and an empty list are described under [Map](/reference/step-kinds/#map). 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`, `map` 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). | | `maintains` | table | No | None | On an agent, command or decide step: the [artifact it keeps between runs](#maintains). | 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`. ### `maintains` A step that maintains an artifact keeps one file current from run to run. Orbital keeps every version of it per project, workflow and step, outside any worktree. ```toml [steps.readiness] prompt = "Bring the release brief up to date. It reads:\n\n{{ maintained.current }}" maintains = { artifact = "release-readiness.md", inputs = ["goal", "repo:CHANGELOG.md"], fresh_for = "1d" } ``` | Key | Type | Required | Default | Notes | | --- | --- | --- | --- | --- | | `artifact` | file name | Yes | None | Letters, digits, `.`, `_` and `-`. | | `inputs` | list | No | None | The step's [maintained inputs](#maintained-inputs): `goal`, `repo:`, `output:.` or `{ command = "..." }`. | | `fresh_for` | duration, such as `12h` or `1d` | No | None | How long a version stays fresh. Without it, a version stays fresh until a later one is recorded. A stale version starts nothing; to refresh on a schedule, add a [trigger rule](/triggers/). | Before the step runs, Orbital writes the current version, if there is one, to `.orbital/maintained/` in the step's working directory, which git is told to ignore. The step's templates read the text as `{{ maintained.current }}` and its path as `{{ maintained.path }}`, and an agent's prompt says where the file is. When the step completes, Orbital records what the file holds as the next version, with the run, the step visit, a fingerprint of the step's definition, the time, the step's Input Fingerprint and the outputs the visit wrote. A step that fails, or leaves no file, records nothing, and the previous version stands. #### Maintained inputs A maintaining step's `inputs` name what its work depends on. Before each visit, Orbital reads them and makes an Input Fingerprint of what they read as and of the step's definition. | Input | What Orbital reads | | --- | --- | | `goal` | The run's goal, which holds a trigger's payload. | | `repo:` | What git holds at `` at HEAD in the step's working directory, and any uncommitted change under it. | | `output:.` | The value in context at ``, an output, fact or decision answer that `` declares. Loading refuses a step or name the workflow does not have. | | `{ command = "..." }` | What the command prints, run in the step's working directory. | `trigger` reads as `goal`, and `context.` reads the value at ``; both still load. The step definition in the fingerprint is the step as written, the prompt its `prompt_file` holds at the visit, and the harness, model, settings and instructions its turn would take, styles included. When the fingerprint matches the one the artifact's current version was recorded with, and that version is still fresh, the visit is an Unchanged step. It takes no turn and checks none of its `ensures`. Orbital writes the current version back to `.orbital/maintained/`, and writes into context the outputs the visit that made the version wrote: an agent's outputs and `response.`, a script's facts, or a decision's answers. The step then succeeds, and its routes read those values. Orbital does not repeat that visit's commits, merges or commands, and does not restore `last_response`. The run's history reads "Unchanged since" and names the run that made the version, with a link to it. Any change makes the step run as before: a changed input, an edited prompt or prompt file, another harness, model or style, or a version past its `fresh_for`. A step without `inputs` always runs. A command that exits other than 0, or runs longer than 60 seconds, counts as changed, and the run's history says why. A version recorded before Orbital kept Input Fingerprints never matches. The project page lists each maintained artifact with its current version and history, and the MCP tool [`read_artifact`](/reference/mcp-tools/#read_artifact) reads any version. ## Postconditions An agent, command, action, decide or fan-in step can declare `ensures`: the conditions that must hold after it. | Key | Type | Required | Default | Notes | | --- | --- | --- | --- | --- | | `ensures` | list of conditions and step references | No | None | Each entry is plain language, or `{ step = "steps." }` naming a step that checks the work. | | `on_unmet` | step reference | No | The step itself | Where the run goes when an entry fails, instead of back into the step. | ```toml [steps.implement] prompt = "Implement the billing rules.\n\n{{ unmet }}" max_visits = 3 ensures = [ "Every new billing rule has a test named after its rule ID", { step = "steps.unit_tests" }, { step = "steps.rounding_review" }, ] next = "steps.done" ``` The checks run once the step's handoff is accepted and before its routes are followed, side by side where they are independent. - **A plain-language entry** is judged by the [decision model](/workflows/decide-with-a-model/) settings name. It sees the step's outputs, its transcript summarised (the raw transcript stays in the run journal), its last response and its diff against the commit the visit started on. It answers whether the condition holds and, when the model gives one, why. Without a decision model, the entry fails. - **A `command` step** passes when its script exits 0, and its output is the reason. A check command needs no `facts`. - **A `decide` step** passes when every question is answered yes. - **An agent step** passes when its `choice` output with the options `pass` and `fail` is `pass`. It needs that output. A check is read-only towards the work it checks. An agent check runs with read tools only. Nothing a check writes reaches the run's context, so the checked step's outputs stay as it left them. Before the checks run, Orbital takes a snapshot of each repository folder: its commit, its index and its files, untracked ones included. Afterwards it puts every folder back to that snapshot, so whatever a check wrote, deleted or committed is undone. If a folder can't be put back, every check step entry fails. When every entry holds, the step follows its routes. When any fails, the step is entered again, which counts towards its `max_visits`, and `{{ unmet }}` lists each failed entry with its reason. A prompt that does not render `{{ unmet }}` is shown it under "Unmet postconditions". With `on_unmet`, the run goes to that step instead, which can read `{{ unmet }}` too. When the step's visits run out, its own `max_visits` or else the workflow's, the run is Paused with the reason "Postconditions unmet", naming each unmet entry. The thread shows a checklist for each visit: every entry, whether it held, why, and, for an agent check, a link to the check step's own record. Command and decide checks have no record of their own; their output is the reason. The run journal records one `RuleChecked` event per entry per visit. A step inside a fan-out branch can't declare `ensures`: its branch shares the files with its siblings, and putting them back after the checks would undo the siblings' work. Declare it on the fan-in instead. Like a route, `on_unmet` can't enter a branch or a fan-in from outside its fork. A step referenced by `ensures` runs only as a check: it needs no routes of its own, and validation does not report it as unreachable. ## 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.` | 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.`. 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.` | template | For each required input of the imported file | None | Rendered each time an imported step reads the input. | | `exits.` | 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.` passes the imported workflow its inputs, and an imported step reads each as `{{ inputs. }}`. 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. | | `delivery_lead` | The prompt the [delivery lead](/delivery-lead/) 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 `.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, map, gate and thread rules broken, as [Step kinds](/reference/step-kinds/) gives them, including a map whose `over` does not name a list, a map without `max_items`, or a later step reading a `results` field the branch's last step does not declare | 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 | | | An `ensures` entry naming a missing step, a `terminal`, `gate`, `fan_out`, `wait`, `action` or probe step, an agent step without a `pass`/`fail` choice output, or the step itself | | | `ensures` on a step inside a fan-out branch | | | `on_unmet` entering a fan-out branch or a fan-in from outside its fork | | 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. --- # 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](/workflows/), and its kind decides what it does. A step with no `kind` is an agent. ```toml title="review.toml" [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](#agent) | none | Sends a prompt to a harness and waits for the turn to finish. | | [Probe](#probe) | `probe` | Reads built-in observations, such as the pull request's state. | | [Action](#action) | `action` | Does a built-in change, such as pushing a branch or opening a pull request. | | [Command](#command) | `command` | Runs a script of your own and records the facts it prints. | | [Decide](#decide) | `decide` | Asks a decision model a question and follows its answer. | | [Wait](#wait) | `wait` | Pauses for a set duration. | | [Gate](#gate) | `gate` | Stops the run until a person lets it continue. | | [Fan-out](#fan-out) | `fan_out` | Runs its child steps as parallel branches. | | [Fan-in](#fan-in) | `fan_in` | Joins the parallel branches. | | [Map](#map) | `map` | Runs its nested steps once per item of a list, in parallel, and collects the results. | | [Terminal](#terminal) | `terminal` | Ends the run with an outcome. | Two more tables under `steps` are not kinds of their own: an [import step](#import) puts another workflow in its place, and a [group](#group) gathers steps under one name. Every step takes `kind`, `label`, `max_visits`, `next` and `routes`, as [Keys every step takes](/reference/workflow-file/#keys-every-step-takes) describes. Unknown keys are errors. The editor labels each step on the canvas with its kind. ![The workflow editor canvas shows steps labelled by kind: a Probes step, Agent steps, a Wait of five minutes, a Worktree create step and an Ending with success.](/screenshots/workflow-editor.webp?v=85a9c6275a) *Each step shows its kind above its id.* ## Steps that do work ### Agent Sends a prompt to a harness and waits for the turn to finish. ```toml [steps.] prompt = "