Workflow
View as markdownA 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.
Where it lives
<config-root>/workflows/<name>.yaml<config-root> is ~/.shelbi/projects/<name>/ in the default
global mode 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.
Example
# ~/.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 | 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 | 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 must appear here, or the workflow is rejected at load. See Required params. |
git | Git | no | inherit project | Per-workflow override of the project's git: block. |
zen | Zen | no | inherit project | Per-workflow override of the project's Zen Mode config. |
statuses
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. Repeating an inline
name: or category: here fails the load. This keeps identity from
drifting across workflows.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | yes | — | Stable id, declared in statuses.yaml. 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 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. |
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 so the orchestrator loads
the task there. tags: [review] on a handoff status is the primitive behind
review workspaces.
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.
| 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 | 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 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. |
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. |
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.
runis 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 gitactions, sharing the same short-circuit contract: the first non-zero exit aborts the edge.readyis a single command polled until it exits 0, afterrunfinishes. Use it to block until a server therunstep launched actually answers.ready_timeout(seconds, default90) caps the wait; a timeout fails the edge.- Teardown is not a separate field: express it as the
runof 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 — 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. |
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))" || truerun 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; an unassigned
task on such an edge is an error, not a silent hub-side run.
review
A workflow-scoped recipe for how the Review agent
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.
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:$SLOTreview: vs. a transition run: serve block
The run / ready 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.
required_params
The task frontmatter fields every task on this workflow must carry. This is
the contract that makes a templated git.base_branch 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:
name: subtask
required_params: [task] # every subtask must set `task:`
git:
base_branch: task/{{task}} # the parent task's branchLeave it out for workflows whose base_branch is a literal like main.
git
Per-workflow override of the project's 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. |
git:
base_branch: develop
branch: 'app/{{id}}'
merge_strategy: squashExplicit 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: 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. |
# 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 entirelySee also
- Workflows — owners, categories, the transition model, and worked examples.
- Statuses — the
statuses.yamlcatalog that supplies each status'snameandcategory. shelbi workflow— list, show, scaffold, and edit workflow files.