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.

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

FieldTypeRequiredDefaultDescription
namestringyesWorkflow id. Must match the filename (<name>.yaml); referenced from task frontmatter.
statuseslist of StatusyesOrdered statuses this workflow uses. At least one required. Order is the column order in the TUI.
descriptionstringnoFree-form description surfaced in shelbi workflow list and the picker.
initial_statusstringnofirst statusStable id a new task lands in. Must reference a status declared here.
transitionslist of TransitionnoSide-effect declarations for specific edges. Omitting it means all moves are pure status changes.
required_paramslist of stringnoTask 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.
gitGitnoinherit projectPer-workflow override of the project's git: block.
zenZennoinherit projectPer-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.

FieldTypeRequiredDefaultDescription
idstringyesStable id, declared in statuses.yaml. Conventionally lowercase kebab-case.
owneruser | agentyesWho moves the task next when automation is off. agent makes it eligible for auto-dispatch; user waits.
agentstringnoderived from categoryWhich agent runs this status under automation. Defaults to developer for active and orchestrator for ready when omitted. None means no automation path.
tagslist of stringno[]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.

FieldTypeRequiredDefaultDescription
fromstringyesStatus id the task moves out of. Must be a declared status.
tostringyesStatus id the task moves into. Must be a declared status.
actionslist of actionno[]Ordered git side-effects to run on this edge. Failures short-circuit the rest.
targetstringnoresolved base_branchOverrides where merge / open_pr land for this edge only. Supports {{var}} placeholders resolved from task params.
runlist of stringno[]Ordered shell commands to run on this edge, in the task's worktree. Compose with — and run after — this edge's actions.
readystringnoShell command polled until it exits 0 (or ready_timeout elapses) after run. Confirms a launched server is up.
ready_timeoutduration (seconds)no90How 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:

ActionEffect
push_branchPush the task's branch to origin.
open_prOpen a PR for the task's branch.
mergeMerge the task's branch into its target. Trips Zen's high-confidence bar.
close_prClose any open PR without merging.
delete_branchDelete the local and remote branch.
restackRebase 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.

  • 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:

VariableValue
$SLOTThe workspace's numeric slot — e.g. derive a per-slot port with $((3000 + $SLOT)).
$SHELBI_TASKThe task id.
$SHELBI_BRANCHThe task's git branch.
$SHELBI_WORKTREEAbsolute path to the worktree the commands run in.
$SHELBI_MACHINEThe 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))" || true

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

FieldTypeRequiredDefaultDescription
workdirstringnoworktree rootDirectory to run the recipe in, relative to the worktree root.
setupstringnoOne-shot install/build command; must exit 0 before serving.
servestringyesCommand that starts the dev server, bound to the slot's port via $SLOT.
readystringnoReadiness probe polled until it exits 0.
urlstringnoReviewable 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:$SLOT

review: 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 branch

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 inherits base_branch, branch, and merge_strategy from the project. Field values may contain {{var}} placeholders resolved against task params at load time.

FieldTypeRequiredDefaultDescription
base_branchstringnoproject's base_branchBranch this workflow's tasks are based on and merged into.
branchstringnoproject's branchFull 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_strategysquash | merge | rebasenoproject's merge_strategyHow this workflow's branches integrate back.
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: 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.

FieldTypeRequiredDefaultDescription
checks.locallist of stringnoproject's zen.checks.localReplaces the project's local check list for this workflow's tasks.
ci_timeoutduration (seconds)noproject's zen.ci_timeoutOverrides how long Zen waits on CI.
danger_pathsextend / override / bare listnoproject's zen.danger_pathsOverrides 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 entirely

See also

  • Workflows — owners, categories, the transition model, and worked examples.
  • Statuses — the statuses.yaml catalog that supplies each status's name and category.
  • shelbi workflow — list, show, scaffold, and edit workflow files.