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. 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: squash

Top-level fields

FieldTypeRequiredDefaultDescription
namestringyes—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.
statuseslist of Statusyes—Statuses this workflow uses (at least one). Board columns render in statuses.yaml order; the first entry here is the default initial_status.
descriptionstringno—Free-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 Transitionno—Side-effect declarations for specific edges. Omitting it means all moves are pure status changes.
required_paramslist of stringno—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.
gitGitnoinherit projectPer-workflow override of the project's git: block.
zenZennoinherit projectPer-workflow override of the project's Zen Mode config.
reviewReviewno— (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.

FieldTypeRequiredDefaultDescription
idstringyes—Stable id, declared in statuses.yaml. Conventionally lowercase kebab-case.
owneruser | agentyes—Who moves the task next when automation is off. agent makes it eligible for auto-dispatch; user waits.
agentstringsee 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.
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 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.

FieldTypeRequiredDefaultDescription
fromstringyes—Status id the task moves out of. Must be a declared status.
tostringyes—Status 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.
readystringno—Shell 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.
urlstringno—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:

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]

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.

  • 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.
setupstringno—One-shot install/build command; must exit 0 before serving.
servestringyes—Command that starts the dev server, bound to the slot's port via $SLOT.
readystringno—Readiness probe polled until it exits 0.
urlstringno—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:$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}}   # 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.

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}}'. Mutually exclusive with branch_prefix.
branch_prefixstringnoproject's branch_prefixShorthand 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_strategysquash | merge | rebasenosquashAccepted, but not consulted today: the merge action always uses the project's git.merge_strategy.
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 task.

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.