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

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