Building Workflows — Simple → Advanced
New here? This is the progressive, tutorial-style guide. For the exhaustive reference (all fields, all bundled workflows, runner internals), see
workflows-reference.md.
1. What a workflow is & when to build one
A workflow is a declarative, multi-step pipeline authored as a *.workflow.md file. It has YAML frontmatter (an ordered list of stages) plus a Markdown body. The runtime loads it into SkillRegistry as a Workflow artifact (src/skills/types.ts:421) and resolves it by name.
Workflows are passive data — they don't execute themselves. Two kinds of clients consume them:
- Stage runners — the generic
runWorkflowStagesengine (src/skills/workflowRunner.ts) walks stages sequentially, in parallel, or via conditional edges. - Freeform crews — inject the workflow body into the coordinator's system prompt as guidance.
When to build a workflow (vs a crew / role / skill)
| You want to… | Use |
|---|---|
| Orchestrate multiple steps with ordering, branching, fan-out | Workflow |
| Define a reusable team of agents with a coordinator | Crew (optionally backed by a workflow) |
| Define a single agent persona / voice | Role |
| Package a composable knowledge/capability module | Skill |
2. Anatomy of a .workflow.md
A .workflow.md file: YAML frontmatter (stages, command, params) + Markdown body, resolved across three precedence tiers.
---
name: my-workflow
description: What this workflow does
stages:
- name: first-step
agents: [explorer]
- name: second-step
after: [first-step]
---
## My Workflow
Body text — injected into the coordinator's system prompt
when the workflow runs in freeform/crew mode.File naming & location tiers
Files must end in *.workflow.md. The loader skips backups (*.v<N>.workflow.md) and shadow files (*.shadow.workflow.md).
| Tier | Path | Precedence |
|---|---|---|
| Bundled | src/skills/bundled/workflows/ (dev) / dist/skills/bundled/workflows/ (packaged) | Lowest |
| User | ~/.fleet/workflows/ | Middle |
| Project | <cwd>/.fleet/workflows/ | Highest (wins via upsertArtifact) |
Project-tier trust. Project workflows are trusted by default (PR-1). Use
--no-trust-project-workflowsorAGENTS_FLEET_TRUST_PROJECT_WORKFLOWS=0to disable project-tier command synthesis when running in a cloned repo whose workflow definitions you haven't reviewed.
How command: synthesizes a slash command
When a workflow declares command: my-cmd, the discovery layer (src/commands/discoverWorkflowCommands.ts) registers /my-cmd as a REPL slash command. Users invoke it like any built-in command.
3. The schema, field by field
Workflow frontmatter (top-level)
Source:
src/skills/types.ts:421–531(Workflowinterface)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Unique identifier (kebab-case). |
description | string | ✅ | Human-readable summary. |
stages | WorkflowStage[] | No | Ordered stage list. [] = freeform (no stages enforced). |
command | string | No | Auto-synthesize a slash command with this name. |
command_aliases | string[] | No | Aliases for the synthesized command. |
command_usage | string | No | Usage string shown in /help. |
params | WorkflowParamSpec[] | No | Typed parameters (PR-1). Supersedes legacy args:. |
args | WorkflowArgSpec[] | No | Deprecated. Coerced to params with type: text, positional: true. |
paramGroups | WorkflowParamGroup[] | No | Cross-param constraints (e.g., mutually-exclusive). |
runner | string | No | Explicit run mode: sequential, per-stage-prompt, coordinator-per-stage, coordinator-prompt, roster. Auto-detected if omitted. |
prompt_template | string | No | Mustache+${} template for coordinator-prompt mode. |
mode | 'planning' | 'implementation' | No | Safety mode — planning restricts writes to plan-dir. |
completion | WorkflowCompletionSpec | No | Completion contract (see below). |
cycles | boolean | No | Opt-in for backward goto: edges. Default false. |
maxStageVisits | number | No | Runtime cap on per-stage visits (1–500). Default 50 for cyclic workflows. |
phaseTimeoutMs | number | No | Per-stage send/wait timeout (ms). Default 30 minutes. |
retry | { default: RetryPolicy } | No | Workflow-level default retry applied to stages without their own. |
budgets | WorkflowBudgets | No | Iteration/time/token/cost caps for cyclic workflows (PR-8). |
convergence | WorkflowConvergence | No | Convergence detector for iterative loops (PR-8). |
antiLoop | WorkflowAntiLoop | No | Anti-loop journal — abort if a file is mutated N+ times (PR-8). |
requiredRoles | string[] | No | Roles that must be installed at load time (PR-12). |
requiredSkills | string[] | No | Skills that must be installed at load time (PR-12). |
requiredGates | string[] | No | Validation gates that must be installed at load time (PR-12). |
crew | CrewV2 | No | Inline workflow-bundled crew (PR-12). |
version | string | number | No | Author-managed version tag. |
completion: sub-object
| Field | Type | Description |
|---|---|---|
requiredArtifacts / required_artifacts | string[] | Files that must exist for the workflow to be considered done. |
stableChecks / stable_checks | number | Consecutive stable stat reads before an artifact is durable. |
pollIntervalMs / poll_interval_ms | number | Poll interval (ms) while a stage send is in flight. |
monitor | boolean | Wire onWorkflowComplete for unattended singleShot drivers. |
seedPlaceholders / seed_placeholders | Array<{path, kind}> | Pre-handoff placeholder seeds (closed enum: init-placeholder). |
WorkflowStage fields
Source:
src/skills/types.ts:133–273+ declaration-merged augmentations at lines 684, 952
| Field | Type | Description |
|---|---|---|
name | string | Required. Unique stage identifier. |
agents | string[] | Role names to spawn. Mutually exclusive with subgraph. |
agentType / agent_type | AgentType | Explicit agent type for the stage. |
parallel | boolean | Spawn agents in parallel. |
after | 'all' | string[] | Wait for named stages (or all) before starting. |
artifact | string | Expected output file (relative to plan dir). |
workersRequired / workers_required | number | Min parallel workers; triggers retry if fewer spawn. |
timeoutMs / timeout_ms | number | Per-stage drain/poll timeout override (ms). |
retry | RetryPolicy | Per-stage retry (overrides workflow-level retry.default). |
next | NextSpec | Conditional edges — predicate-driven routing (Phase 3A). |
subgraph | string | Delegate to another workflow (mutually exclusive with agents). |
inputs | Record<string, string> | Parent→child placeholder map (subgraph only). |
outputs | string[] | Child artifacts to surface back to parent (subgraph only). |
forEach | ForEachStage | Fan-out / fan-in (Phase 3B). |
interrupt | InterruptStage | Human-in-the-loop gate (Phase 3C). |
postCheck | string | {kind, params} | Post-stage validation handler. |
postChecks | PostCheckRef[] | Plural form — array of post-check references. |
postCheckSeverity | 'error' | 'warning' | How postCheck failure is treated. |
postCheckMaxRetries | number | Max postCheck invocations. |
postCheckRetryInstruction | string | Instruction appended on postCheck retries. |
artifactRetryInstruction | string | Instruction appended when artifact is missing. |
workersRetryInstruction | string | Instruction appended when insufficient workers spawn. |
tasksRetryInstruction | string | Instruction appended when no tasks are created. |
preQuestions / pre_questions | string[] | Questions asked deterministically before the stage prompt. |
validation | GateInvocation[] | Declarative validation gates (PR-6). |
gates | string[] | Shorthand gate names (auto-synthesized to validation:). |
Parser quirks
- Both
snake_caseandcamelCaseaccepted for most fields (e.g.,workers_requiredandworkersRequiredboth work). - Inline arrays use
[a, b, c]syntax on a single line. - Inline objects use
{ key: value, key2: value2 }syntax. - No multi-line YAML scalars — the hand-rolled parser does not support
|or>block scalars in the way a full YAML parser would. Use single-line values or the|-syntax within list items. stages: []means freeform (no stages enforced).- Numeric strings are auto-coerced (e.g.,
workers_required: "4"→4).
4. Build it: simple → advanced
The 7 progressive levels — each builds on the previous, from minimal sequential to full autopilot.
The three core execution patterns: sequential chaining, parallel fan-out with synthesis, and conditional edge routing.
Level 1 — Minimal sequential workflow
The smallest workflow that runs: two stages, one after the other.
---
name: hello-pipeline
description: Minimal 2-stage sequential workflow
runner: coordinator-per-stage
stages:
- name: research
artifact: research.md
- name: summarize
after: [research]
artifact: SUMMARY.md
completion:
requiredArtifacts: [SUMMARY.md]
version: 1
---
## Hello Pipeline
Stage 1 produces `research.md` (the coordinator generates it). Stage 2 reads
the research and writes a summary.Key concepts:
after: [research]— thesummarizestage waits forresearchto complete.artifact:— the file the stage is expected to produce.completion.requiredArtifacts— the runner considers the workflow done when these files exist.runner: coordinator-per-stage— the coordinator receives one prompt per stage (the most common mode for author-driven workflows).
Level 2 — Parallel fan + synthesize
Spawn multiple agents in parallel, then synthesize results.
---
name: multi-review
description: Parallel review with synthesis
runner: coordinator-per-stage
stages:
- name: review
agents: [security-reviewer, correctness-reviewer, performance-reviewer]
parallel: true
- name: synthesize
after: [review]
artifact: REVIEW.md
completion:
requiredArtifacts: [REVIEW.md]
version: 1
---
## Multi-agent Review
The coordinator spawns all three reviewers in parallel during the `review`
stage. After all complete, the `synthesize` stage merges their findings.New concepts:
agents: [...]— role names to spawn as workers for this stage.parallel: true— all agents run concurrently.after: [review]— the synthesize stage waits for the parallel stage.
This pattern mirrors the bundled
code-review.workflow.md(src/skills/bundled/workflows/code-review.workflow.md).
Level 3 — A synthesized slash command
Add command:, params:, and usage to make your workflow invokable as /my-cmd.
---
name: investigate
description: Investigate a topic with structured output
command: investigate
command_aliases: [inv]
command_usage: "/investigate <topic> [--depth shallow|deep]"
params:
- name: topic
type: text
positional: true
required: true
description: The topic to investigate
- name: depth
type: enum
values: [shallow, deep]
default: shallow
description: Investigation depth
runner: coordinator-per-stage
stages:
- name: research
artifact: research.md
- name: report
after: [research]
artifact: REPORT.md
completion:
requiredArtifacts: [REPORT.md]
version: 1
---
## Investigation Workflow
Investigate ${topic} at ${depth} depth. Write findings to research.md,
then synthesize into REPORT.md.New concepts:
command: investigate— registers/investigatein the REPL.command_aliases: [inv]—/invalso works.params:— typed parameters. Types:text,integer,boolean,enum,slug,path,git-range.${topic}and${depth}— placeholders expanded in stage prompts and the workflow body.positional: true— bare argument (no--flagneeded).
Param types reference (src/skills/types.ts:338–354):
| Type | Extra fields | Notes |
|---|---|---|
text | maxLength | Free-form string. |
integer | min, max | Validated as integer. |
boolean | — | Flag-only (--name / --no-name). Cannot be positional. |
enum | values: string[] | Must match one of the listed values. |
slug | pattern | Defaults to ^[a-z0-9][a-z0-9-]*$. |
path | mustExist | Validated against isPathInsideDir. |
git-range | expand | Validates refs via git rev-parse; expands ${files}. |
Level 4 — Conditional edges (next:)
Branch execution based on artifacts, placeholders, or prior-stage results.
---
name: smart-triage
description: Investigate then branch based on severity
runner: coordinator-per-stage
stages:
- name: investigate
artifact: triage-report.md
next: { edges: [{ ifResult: { stage: investigate, errored: true }, goto: end }] }
- name: classify
after: [investigate]
next: { edges: [{ ifArtifact: "triage-report.md", goto: route }], else: skip }
- name: route
after: [classify]
next:
edges:
- { ifPlaceholder: { name: severity, equals: high }, goto: fix }
- { ifPlaceholder: { name: severity, equals: medium }, goto: review }
else: skip
- name: fix
agents: [fleet-coder]
next: { edges: [], else: end }
- name: review
agents: [fleet-reviewer]
next: { edges: [], else: end }
- name: skip
completion:
requiredArtifacts: [triage-report.md]
version: 1
---
## Smart Triage
Conditional branching based on investigation results.Adapted from the bundled
triage.workflow.md.
New concepts — the next: block:
The next: field on a stage replaces the default "advance to the next stage" behaviour with predicate-driven routing.
Five predicate heads (src/skills/types.ts:661–666):
| Predicate | Tests | Example |
|---|---|---|
ifArtifact | File exists in planDir | { ifArtifact: "report.md", goto: process } |
ifArtifactMissing | File does NOT exist | { ifArtifactMissing: "report.md", goto: retry } |
ifPlaceholder | Placeholder value test | { ifPlaceholder: { name: x, equals: "y" }, goto: z } |
ifEnv | Environment variable test | { ifEnv: { name: CI, exists: true }, goto: ci-path } |
ifResult | Prior-stage outcome | { ifResult: { stage: build, errored: true }, goto: fix } |
Evaluation rules:
- Edges are evaluated in declaration order — first match wins.
else:is the fallback when no edge matches. Omittingelse:falls through to the next stage linearly.- Use
goto: endto terminate the workflow cleanly. - Backward gotos require
cycles: trueat the workflow level, plus amaxStageVisitscap.
ifPlaceholder operators: equals, contains, matches (regex), exists (boolean).
ifResult fields: satisfied, retried, agentsSpawnedAtLeast, errored.
Level 5 — Fan-out / forEach (map pattern)
The map pattern: an item source fans out to N parallel workers, then gather joins results into a single artifact.
Spawn one worker per item from a dynamic list.
---
name: per-file-audit
description: Fan-out per-file reviews
runner: coordinator-per-stage
stages:
- name: list-files
artifact: files.txt
- name: audit
after: [list-files]
forEach:
items: { artifact: files.txt, format: lines }
parallel: true
maxConcurrency: 5
asContext: file
onError: gather-all
gather: { strategy: { kind: concat, separator: "\n\n---\n\n" }, artifact: audit-results.md }
- name: report
after: [audit]
artifact: REPORT.md
completion:
requiredArtifacts: [REPORT.md]
version: 1
---
## Per-file Audit
Fan-out review: one worker per file, gathered into a single digest.Adapted from the bundled
per-file-review.workflow.md.
forEach sub-object (src/skills/types.ts:822–843):
| Field | Type | Default | Description |
|---|---|---|---|
items | ItemSource | — | Required. Where items come from. |
parallel | boolean | true | Fan-out in parallel. |
maxConcurrency | number | min(8, items.length) | Concurrent dispatch cap. |
asContext | string | 'item' | Placeholder name bound to each item ( above). |
subgraph | string | — | Sub-workflow invoked per item (instead of stage agents). |
onError | 'gather-all' | 'fail-fast' | 'gather-all' | Failure mode. |
retry | RetryPolicy | — | Per-item retry. |
gather | GatherSpec | — | Required. How outputs are combined. |
forceLargeFanOut | boolean | — | Required when inline items > 50. |
Item sources (src/skills/types.ts:794–799):
| Kind | Example | Notes |
|---|---|---|
inline | { values: [a, b, c] } | Literal list in YAML. |
placeholder | { ref: myList } | Split runtime placeholder by newline/JSON. |
artifact | { artifact: files.txt, format: lines } | Read file from planDir. |
command | { cmd: "git diff --name-only", format: lines } | Requires AGENTS_FLEET_FOREACH_COMMAND=1. |
intake | { intake: true } | Uses workflow-level intake adapter. |
Gather strategies (src/skills/types.ts:802–805):
| Kind | Options | Output |
|---|---|---|
array | — | JSON array of per-item results. |
concat | separator, header | String concatenation. |
merge | conflict: 'last-wins' | 'error' | Object merge. |
Level 6 — Human-in-the-loop interrupts + gates
Interrupt stages pause for human input: approve continues, reject aborts, hold re-prompts.
Pause the workflow for human approval before continuing.
---
name: deploy-gate
description: Build → human approval → deploy
runner: coordinator-per-stage
stages:
- name: build
artifact: BUILD_REPORT.md
- name: await-approval
interrupt:
waitFor: user
prompt: |
Build report ready at ${planDir}/BUILD_REPORT.md.
Approve deploy? (reply: approve | reject | hold)
onResume:
approve: continue
reject: abort
hold: repeat
maxReprompts: 2
storeAs: approverDecision
unattendedPolicy: fail-loud
- name: deploy
after: [await-approval]
artifact: DEPLOY_REPORT.md
completion:
requiredArtifacts: [DEPLOY_REPORT.md]
version: 1
---
## Deploy Gate
Build → human approval → deploy. The workflow suspends at the gate
and resumes only on explicit user action.Adapted from the bundled
approval-gate.workflow.md.
interrupt: sub-object (src/skills/types.ts:905–936):
| Field | Type | Default | Description |
|---|---|---|---|
waitFor | 'user' | — | Required. Transport (v1 only supports user). |
prompt | string | — | Required. Displayed to the user at suspend time. |
onResume | Record<string, InterruptAction> | { approve: 'continue', reject: 'abort' } | Decision routing map. |
maxReprompts | number | 3 | Re-prompts before auto-abort. |
storeAs | string | — | Store the user's answer as a placeholder for downstream stages. |
timeoutMs | number | — | Idle timeout before auto-abort. |
unattendedPolicy | 'fail-loud' | 'auto-approve' | 'fail-loud' | Behaviour under --unattended. |
Actions: continue (advance), abort (stop workflow), repeat (re-prompt).
Built-in synonyms (case-insensitive, applied before onResume map):
yes/y/ok/approve→continueno/n/cancel/reject→abort
Resume commands:
/approve— shortcut forcontinue/reject— shortcut forabort/workflows resume <thread-id> <decision>— explicit resume
Mutual exclusions: interrupt stages cannot have agents, artifact, parallel, or retry.
Level 7 — A full autopilot
The bug-autopilot cycle: collect → fix → validate (retry loop) → review → convergence (loop) → open-pr → merge.
The bundled bug-autopilot.workflow.md demonstrates a production-grade 16-stage cyclic workflow. Here's the shape (not the full 130-line file):
name: bug-autopilot
command: bug-autopilot
runner: coordinator-per-stage
cycles: true # enables backward goto edges
maxStageVisits: 60 # hard cap on per-stage visits
phaseTimeoutMs: 3600000 # 1 hour per stage
params:
- name: bug
type: text
positional: true
required: true
- name: scope
type: enum
values: [diff, full]
default: diff
budgets:
maxWallClockMs: 14400000 # 4-hour wall clock budget
maxTokens: 200000000
stages:
- name: collect # investigate
- name: reproduce # create repro, branch on ifArtifact
- name: fix # implement fix
- name: validate # run tests — loop back to fix if fail
- name: review # fan-out ×4 reviewers via forEach
- name: triage # classify findings
- name: fix-findings # fan-out fix per finding
- name: convergence-check # decide: converged → open-pr, else → review (LOOP)
- name: open-pr # commit, push, create PR
- name: await-review # poll GitHub for approval
- name: merge # squash-merge
- name: done # SUMMARY.md (terminal)
completion:
requiredArtifacts: [SUMMARY.md]
monitor: trueKey patterns demonstrated:
- Cyclic loops —
validate → fixretry loop,review → convergence-check → reviewconvergence loop. - Fan-out via
forEach— thereviewstage fans out over 4 angles;fix-findingsfans out over a dynamic file. - Conditional edges — every stage uses
next:to branch based on artifacts. - Budgets —
maxWallClockMscaps total runtime. monitor: true— enables unattended completion signalling.
See the full file:
src/skills/bundled/workflows/bug-autopilot.workflow.md.
5. Per-stage prompt templates
For workflows using the per-stage-prompt runner mode (like /feature), each stage's prompt lives in a sibling directory:
.fleet/workflows/
my-pipeline.workflow.md
my-pipeline/
research.prompt.md ← prompt for stage "research"
design.prompt.md ← prompt for stage "design"
tasks.prompt.md ← prompt for stage "tasks"Convention (src/skills/workflowPrompts.ts:13–17): a workflow at <dir>/<name>.workflow.md looks for prompts at <dir>/<name>/<stage>.prompt.md.
Placeholder substitution
Templates use ${var} syntax (and is also recognized). Standard placeholders available:
| Placeholder | Source |
|---|---|
${feature}, ${topic}, etc. | User-supplied params |
${planDir} | Runtime plan directory path |
${cwd} | Working directory |
${context} | Project context (when available) |
Example from feature-pipeline/brainstorm.prompt.md:
# Phase 1: Feature Brainstorming — ${feature}
Plan directory: ${planDir}
${context}When to use sibling prompts vs inline prompt_template:
| Use case | Approach |
|---|---|
| Complex multi-stage pipelines with long prompts | Sibling *.prompt.md files + runner: per-stage-prompt |
| Single coordinator prompt that references stage metadata | prompt_template: in frontmatter + runner: coordinator-prompt |
| Simple coordinator-driven stages (most common) | runner: coordinator-per-stage — no separate prompt files needed |
6. Install, run, and validate
Where to put your workflow
For personal use: ~/.fleet/workflows/my-workflow.workflow.md
For a team/project: <project>/.fleet/workflows/my-workflow.workflow.md
Making the registry pick it up
The registry loads workflows at startup. After adding a new file:
- Restart the session, or
- Use
/reloadto reload the skill registry in an active session.
Running it
If your workflow declares command::
/my-command arg1 --flag valueIf it doesn't declare a command, activate it via a crew that references it, or invoke it programmatically.
Validation — sanity-check parsing
Run the bundled parser + smoke tests to confirm your workflow parses correctly:
npx vitest run src/skills/parser.test.ts
npx vitest run src/skills/workflowRunner.test.tsOr run the full skills test suite:
npx vitest run src/skills/The parser tests cover all bundled workflows — if yours follows the same patterns and the tests pass with your file in the appropriate tier, it parses correctly.
7. Troubleshooting & gotchas
YAML-subset limitations
The parser uses a hand-rolled YAML subset (src/skills/yamlInline.ts), not a full YAML parser. Key limitations:
- No multi-line block scalars at the top level —
|and>don't work for top-level frontmatter keys. They DO work inside list items (stage definitions). - All top-level keys must be single-line — arrays use
[a, b]inline syntax. - Inline objects use
{ key: value }— nested objects beyond 2 levels may not parse. - Strings don't need quotes unless they contain special characters.
Reserved command names
The following names are reserved and cannot be used as command: values: built-in slash commands (feature, code-review, init, start, status, help, etc.). The discovery layer rejects them with a console warning.
Common parse errors
| Symptom | Cause | Fix |
|---|---|---|
| Workflow silently not loaded | Missing name: in frontmatter | Add name: field |
| Stage skipped | Both subgraph: and agents: on same stage | Use one or the other |
postCheck ignored | Both singular postCheck: and plural postChecks: | Use only one form |
| forEach stage dropped | Invalid items: or missing gather: | Check item source format |
| Backward goto rejected | Missing cycles: true at workflow level | Add cycles: true |
| Command not synthesized | _paramsError (reserved param name, invalid group member) | Check param names |
stages: [] — freeform behaviour
An empty stages array means the workflow operates in freeform mode. The body is injected into the coordinator's system prompt, but no stage walker runs. Used by crews for unstructured operation.
Project-tier trust
Project-tier workflows (<cwd>/.fleet/workflows/) are trusted by default. Disable with --no-trust-project-workflows when:
- Running in a cloned repo you haven't reviewed
- CI environments where you want explicit control
8. Cross-references
- Workflows Reference — exhaustive field reference, all bundled workflows, runner internals
- Crews Reference — how crews select and override workflows via the
workflows:map - Composition Guide — how roles, skills, and workflows compose together
- Orchestration Tutorial — end-to-end walkthrough including custom workflows
- Loops & Autopilots — goal-driven loops and the autopilot commands
- Commands Manual — all slash commands including synthesized workflow commands