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

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