# Workflow

Field-by-field reference for a workflow YAML — statuses (reference-only), initial_status, transitions and their actions, and per-workflow git/zen overrides.

A workflow file declares the pipeline a task moves through: which
statuses it references, which side-effects fire on which edges, and any
per-workflow git or Zen overrides. For the model behind these fields
(owners, categories, the any-to-any transition policy), see
[the workflows concept](/docs/guides/getting-started/workflows).

## Where it lives

```
<config-root>/workflows/<name>.yaml
```

`<config-root>` is `~/.shelbi/projects/<name>/` in the default
[global mode](/docs/concepts/config-modes) and `<repo>/.shelbi/` in
in-repo mode. The file's basename is its id; the in-file `name:` field
must match. Every project has a built-in virtual `default` workflow that
lives in code until `shelbi workflow new` or `edit` writes a file. See
[`shelbi workflow`](/docs/cli/workflow).

## Example

```yaml
# ~/.shelbi/projects/myapp/workflows/default.yaml
name: default
description: standard one-track flow

# Reference-only statuses: name + category come from statuses.yaml.
statuses:
  - { id: backlog,     owner: user,  agent: orchestrator }
  - { id: todo,        owner: agent, agent: orchestrator }
  - { id: in-progress, owner: agent, agent: developer }
  - { id: review,      owner: user,  agent: orchestrator }
  - { id: done,        owner: user }

transitions:
  - { from: in-progress, to: review, actions: [push_branch, open_pr] }
  - { from: review,      to: done,   actions: [merge, delete_branch] }
```

## Top-level fields

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | string | yes | — | Workflow id. Must match the filename (`<name>.yaml`); referenced from task frontmatter. |
| `statuses` | list of [Status](#statuses) | yes | — | Ordered statuses this workflow uses. At least one required. Order is the column order in the TUI. |
| `description` | string | no | — | Free-form description surfaced in `shelbi workflow list` and the picker. |
| `initial_status` | string | no | first status | Stable `id` a new task lands in. Must reference a status declared here. |
| `transitions` | list of [Transition](#transitions) | no | — | Side-effect declarations for specific edges. Omitting it means all moves are pure status changes. |
| `required_params` | list of string | no | — | Task frontmatter fields every task on this workflow must carry. Every `{{var}}` in [`git.base_branch`](#git) must appear here, or the workflow is rejected at load. See [Required params](#required-params). |
| `git` | [Git](#git) | no | inherit project | Per-workflow override of the project's `git:` block. |
| `zen` | [Zen](#zen) | no | inherit project | Per-workflow override of the project's Zen Mode config. |

## statuses

<Callout type="tip" title="Reference-only form">

A workflow status entry carries **only** `id`, `owner`, and optional
`agent`. The display `name` and `category` are declared once in
[`statuses.yaml`](/docs/configuration/statuses). Repeating an inline
`name:` or `category:` here fails the load. This keeps identity from
drifting across workflows.

</Callout>

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | string | yes | — | Stable id, declared in [`statuses.yaml`](/docs/configuration/statuses). Conventionally lowercase kebab-case. |
| `owner` | `user` \| `agent` | yes | — | Who moves the task next when automation is off. `agent` makes it eligible for auto-dispatch; `user` waits. |
| `agent` | string | no | derived from category | Which agent runs this status under automation. Defaults to `developer` for `active` and `orchestrator` for `ready` when omitted. `None` means no automation path. |
| `tags` | list of string | no | `[]` | Required workspace tags. The task routes to a free workspace whose [effective tags](/docs/configuration/project#workspaces) are a **superset** of this set (set-AND). Empty means any free workspace — the default. Accepts a scalar `tag:` alias and a bare string as shorthand. Elided when empty. |

<Callout type="note" title="Tags route work to the right slot">

A status's `tags` are how a workflow says "this step needs a workspace with
these capabilities." Pair them with a workspace/machine that carries the same
[`tags`](/docs/configuration/project#workspaces) so the orchestrator loads
the task there. `tags: [review]` on a handoff status is the primitive behind
[review workspaces](/docs/concepts/review-workspaces).

</Callout>

## transitions

Each entry declares the hub-side side-effects that fire when a task
crosses one edge. Transitions do **not** restrict which moves are legal.
Moves are any-to-any; unlisted edges are pure status changes. See
[transitions](/docs/guides/getting-started/workflows#transitions).

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `from` | string | yes | — | Status `id` the task moves out of. Must be a declared status. |
| `to` | string | yes | — | Status `id` the task moves into. Must be a declared status. |
| `actions` | list of [action](#actions) | no | `[]` | Ordered git side-effects to run on this edge. Failures short-circuit the rest. |
| `target` | string | no | resolved `base_branch` | Overrides where `merge` / `open_pr` land for this edge only. Supports `{{var}}` placeholders resolved from task params. |
| `run` | list of string | no | `[]` | Ordered [shell commands](#run-ready-and-teardown) to run on this edge, in the task's worktree. Compose with — and run *after* — this edge's `actions`. |
| `ready` | string | no | — | Shell command polled until it exits 0 (or `ready_timeout` elapses) after `run`. Confirms a launched server is up. |
| `ready_timeout` | duration (seconds) | no | `90` | How long to poll `ready` before failing the edge. Ignored when `ready` is unset. |

### actions

The six hub-side action primitives an edge's `actions` list may contain:

| Action | Effect |
| --- | --- |
| `push_branch` | Push the task's branch to origin. |
| `open_pr` | Open a PR for the task's branch. |
| `merge` | Merge the task's branch into its target. Trips [Zen's high-confidence bar](/docs/concepts/zen-mode#the-high-confidence-bar). |
| `close_pr` | Close any open PR without merging. |
| `delete_branch` | Delete the local and remote branch. |
| `restack` | Rebase the task's branch onto its parent's current branch. |

```yaml
transitions:
  - from: in-progress
    to: review
    target: develop            # this edge merges into develop, not base_branch
    actions: [push_branch, merge]
```

### run, ready, and teardown

Beyond the six git actions, an edge can run arbitrary shell commands. This is
what lets a status *do* something on entry — install dependencies, boot a dev
server, warm a cache — and undo it on exit.

- **`run`** is a list of commands executed in order, in the task's worktree,
  on the assigned workspace's machine (host-routed over SSH for remote
  machines). They run **after** this edge's git `actions`, sharing the same
  short-circuit contract: the first non-zero exit aborts the edge.
- **`ready`** is a single command polled until it exits 0, after `run`
  finishes. Use it to block until a server the `run` step launched actually
  answers. `ready_timeout` (seconds, default `90`) caps the wait; a timeout
  fails the edge.
- **Teardown** is not a separate field: express it as the `run` of the *exit*
  transition (the edge that moves the task out of the status).

Each command runs synchronously, so a long-lived server must background
itself — the edge is considered entered the moment the launcher returns, and
`ready` is what confirms the server is up.

Every command has these variables exported into its environment:

| Variable | Value |
| --- | --- |
| `$SLOT` | The workspace's numeric [`slot`](/docs/configuration/project#workspaces) — e.g. derive a per-slot port with `$((3000 + $SLOT))`. |
| `$SHELBI_TASK` | The task id. |
| `$SHELBI_BRANCH` | The task's git branch. |
| `$SHELBI_WORKTREE` | Absolute path to the worktree the commands run in. |
| `$SHELBI_MACHINE` | The machine the workspace runs on. |

```yaml
transitions:
  # Entering review: boot a dev server on a per-slot port, wait for it.
  - from: in-progress
    to: review
    run:
      - npm install
      - npm run dev -- --port $((3000 + $SLOT)) &
    ready: curl -fsS http://localhost:$((3000 + $SLOT)) > /dev/null
    ready_timeout: 120
  # Leaving review: tear the server down.
  - from: review
    to: done
    actions: [merge, delete_branch]
    run:
      - pkill -f "port $((3000 + $SLOT))" || true
```

<Callout type="note" title="run needs an assigned workspace">

A transition that declares `run`/`ready` must fire on a task that's assigned
to a workspace — that's what tells Shelbi *where* to run the commands. Route
the status to a workspace with the status's [`tags`](#statuses); an unassigned
task on such an edge is an error, not a silent hub-side run.

</Callout>

## review

A workflow-scoped recipe for how the [Review agent](/docs/concepts/review-workspaces)
boots this branch so a human can run it. This is the recommended way to stand
up a review server: Shelbi resolves the recipe's `$SLOT` / `$PORT` against the
review workspace's slot and injects it into the Review agent's prompt, and the
agent runs it verbatim, health-checking it and handing back a URL. Because the
recipe lives on the workflow, a monorepo's `app` / `site` / `docs` workflows
each serve their own subdirectory on the review slot's port without colliding.

When a workflow declares no `review:` block, the Review agent does a
**diff-only** review: it does not auto-detect a framework or boot a
default-port server.

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `workdir` | string | no | worktree root | Directory to run the recipe in, relative to the worktree root. |
| `setup` | string | no | — | One-shot install/build command; must exit 0 before serving. |
| `serve` | string | yes | — | Command that starts the dev server, bound to the slot's port via `$SLOT`. |
| `ready` | string | no | — | Readiness probe polled until it exits 0. |
| `url` | string | no | — | Reviewable URL handed to the human; also gates the review interface's "Open Browser" action. |

Every field may reference the review slot's port as `$SLOT` or `$PORT` (both
`$X` and `${X}` spellings); Shelbi substitutes the resolved port before the
recipe reaches the agent.

```yaml
review:
  workdir: site
  setup: npm install --no-audit --no-fund
  serve: npm run dev -- -p $SLOT
  ready: curl -sf http://localhost:$SLOT
  url: http://localhost:$SLOT
```

<Callout type="note" title="review: vs. a transition run: serve block">

The [`run` / `ready`](#run-ready-and-teardown) block on a review-entering
transition also boots a server, but it runs **hub-side** and declaratively.
Prefer `review:` for a serve recipe: the Review agent executes it, so it can
health-check, summarize failures, and apply a human's tweak. Keep transition
`run:` for hub-side side-effects that aren't the review server itself.

</Callout>

## required_params

The task frontmatter fields every task on this workflow must carry. This is
the contract that makes a templated [`git.base_branch`](#git) safe: because
`base_branch` resolves only against a task's own frontmatter params (unlike
`branch`, it gets no `{{id}}` or `{{github_user}}` context), a task that omits
the field produces an unresolved base branch at dispatch.

Shelbi validates the invariant when the workflow loads: every `{{var}}` in
`git.base_branch` must appear in `required_params`, or the load fails naming
the variable and the workflow. The shipped `subtask` workflow declares it:

```yaml
name: subtask
required_params: [task]        # every subtask must set `task:`
git:
  base_branch: task/{{task}}   # the parent task's branch
```

Leave it out for workflows whose `base_branch` is a literal like `main`.

## git

Per-workflow override of the project's [`git:`](/docs/configuration/project#git)
block. When omitted, the workflow inherits `base_branch`, `branch`, and
`merge_strategy` from the project. Field values may contain `{{var}}`
placeholders resolved against task params at load time.

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `base_branch` | string | no | project's `base_branch` | Branch this workflow's tasks are based on and merged into. |
| `branch` | string | no | project's `branch` | Full branch-name template for generated task branches in this workflow, rendered with `{{var}}` substitution when a task omits `branch:`. The shipped `task` workflow uses `branch: '{{github_user}}/{{id}}'`. |
| `merge_strategy` | `squash` \| `merge` \| `rebase` | no | project's `merge_strategy` | How this workflow's branches integrate back. |

```yaml
git:
  base_branch: develop
  branch: 'app/{{id}}'
  merge_strategy: squash
```

Explicit task `branch:` frontmatter still wins over a workflow `branch`
template. If no task, workflow, or project `branch` is configured, Shelbi names
the generated branch after your authenticated GitHub username, falling back to
`user` when it cannot determine one.

## zen

Per-workflow override of the project's
[`zen:`](/docs/configuration/project#zen) block. Each subfield is
independently optional: override just `checks`, just `ci_timeout`, just
`danger_paths`, or any combination. Unset subfields fall back to the
project. The canonical use is a `research:` workflow opting out of
code-style checks without affecting `default`.

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `checks.local` | list of string | no | project's `zen.checks.local` | Replaces the project's local check list for this workflow's tasks. |
| `ci_timeout` | duration (seconds) | no | project's `zen.ci_timeout` | Overrides how long Zen waits on CI. |
| `danger_paths` | `extend` / `override` / bare list | no | project's `zen.danger_paths` | Overrides the danger-path globs. Same shape as the project field. |

```yaml
# workflows/research.yaml — no code checks, no auto-merge risk
name: research
statuses:
  - { id: todo, owner: agent, agent: developer }
  - { id: done, owner: user }
zen:
  checks:
    local: []            # skip the project's cargo/npm checks entirely
```

## See also

- [Workflows](/docs/guides/getting-started/workflows) — owners, categories, the
  transition model, and worked examples.
- [Statuses](/docs/configuration/statuses) — the `statuses.yaml` catalog
  that supplies each status's `name` and `category`.
- [`shelbi workflow`](/docs/cli/workflow) — list, show, scaffold, and
  edit workflow files.
