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. Keep the in-file name: equal to
the filename: Shelbi resolves workflows by filename and only warns on a
mismatch, so a mismatched name: shows up but won't load by that name (two
files declaring the same name: fail the load).
shelbi init writes two workflows, task (the project's
default_workflow) and
subtask. A built-in default workflow is used only when workflows/ has no
workflow files at all, and a lingering default reference resolves to task
when no default.yaml exists. See
shipped workflows
and shelbi workflow.
Editing from Settings
You don't have to edit workflow files by hand. Settings ▸ Workflows, in the
desktop app and in the TUI (press , in the sidebar, then 4), lists the
project's workflows and its status catalog.
From there you can create, duplicate, rename, and delete a workflow, pick the
project's default, and open one in an editor: a form plus a graph builder for
its statuses (owner, agent, tags), initial_status, transitions (actions,
target, url), git, required_params, and review:. A transition's
run, ready, and ready_timeout show as a
Run script action, always last in its Actions since it runs after the git
actions: pick it from Add action to enter its commands one per row, in order,
with an optional ready check and timeout. Removing it removes all three keys.
Saves go through a patch that keeps comments and unknown keys, and Edit YAML
opens the file itself. If a file changed on disk since the page read it,
nothing is written.
Example
The shipped task workflow, as shelbi init writes it (the scaffold also
carries commented-out initial_status and zen blocks):
# ~/.shelbi/projects/myapp/workflows/task.yaml
name: task
description: 'Branch work with a review gate: the default track shipped with every project.'
# 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: review, tags: [review] }
- { id: done, owner: user }
- { id: canceled, owner: user }
transitions:
- { from: in-progress, to: review, actions: [push_branch, open_pr] }
- { from: review, to: done, actions: [push_branch, merge] }
- { from: in-progress, to: canceled, actions: [close_pr, delete_branch] }
- { from: review, to: canceled, actions: [close_pr, delete_branch] }
git:
base_branch: main
branch: '{{github_user}}/{{id}}'
merge_strategy: squashTop-level fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | yes | — | Workflow id, referenced from task frontmatter. Keep it equal to the filename (<name>.yaml): workflows are looked up by filename, and a mismatch only warns. |
statuses | list of Status | yes | — | Statuses this workflow uses (at least one). Board columns render in statuses.yaml order; the first entry here is the default initial_status. |
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. |
review | Review | no | — (diff-only review) | Recipe the Review agent runs to serve this workflow's branch. |
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 | see description | — | Which agent runs this status under automation. Required when owner: agent (the load fails without it). Omitted on a user status, it 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 task is loaded
there. tags: [review] on a handoff status is the primitive behind
review workspaces. The literal review tag
is also special-cased: review-tagged slots are kept out of developer dispatch,
and the hub's review auto-loader fills them.
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. Each action
is also a command you can run by hand: see shelbi action.
| 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. |
url | string | no | — | Reviewable URL for a server this edge's run: starts ($PORT / $SLOT substituted). Used by Open Browser when the workflow has no review.url. |
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]Work with no branch
Some issues deliver something outside the repo, like a plan written to a
document store. Give them a workflow whose edges never push, open a PR, merge,
or restack. close_pr and delete_branch only clean up, so they're fine to
keep.
# workflows/plan.yaml
name: plan
statuses:
- { id: backlog, owner: user }
- { id: todo, owner: agent, agent: orchestrator }
- { id: in-progress, owner: agent, agent: developer }
- { id: review, owner: user }
- { id: done, owner: user }
- { id: canceled, owner: user }
transitions:
- { from: in-progress, to: canceled, actions: [delete_branch] }A worker on this workflow writes the ready marker with no commits ahead of the
base. The card moves to review with no PR and no branch on origin, and moving it
to done merges nothing. If the worker does commit, those commits are still
pushed at handoff. A workflow with any push_branch, open_pr, merge, or
restack edge, or no transitions: block at all, still needs a branch.
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}} # resolves to task/<parent-id>The rendered base is the literal task/<parent-id>. It matches the parent's
branch only when the parent's branch has that name: the shipped task workflow
names branches {{github_user}}/{{id}}, so either give the parent an explicit
branch: task/<id> or change one of the two templates to agree.
Leave 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 uses the project's base_branch and branch
naming. A workflow that sets branch or branch_prefix replaces the project's
branch naming wholesale (see
branch resolution). 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}}'. Mutually exclusive with branch_prefix. |
branch_prefix | string | no | project's branch_prefix | Shorthand alternative to branch: generates <branch_prefix>/<task-id> when a task omits branch:. Mutually exclusive with branch — declaring both in one git: block fails the load. |
merge_strategy | squash | merge | rebase | no | squash | Accepted, but not consulted today: the merge action always uses the project's git.merge_strategy. |
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 task.
| 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.