Skip to content

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:

  1. Stage runners — the generic runWorkflowStages engine (src/skills/workflowRunner.ts) walks stages sequentially, in parallel, or via conditional edges.
  2. 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-outWorkflow
Define a reusable team of agents with a coordinatorCrew (optionally backed by a workflow)
Define a single agent persona / voiceRole
Package a composable knowledge/capability moduleSkill

2. Anatomy of a .workflow.md

Workflow anatomyA .workflow.md file: YAML frontmatter (stages, command, params) + Markdown body, resolved across three precedence tiers.

markdown
---
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).

TierPathPrecedence
Bundledsrc/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-workflows or AGENTS_FLEET_TRUST_PROJECT_WORKFLOWS=0 to 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 (Workflow interface)

FieldTypeRequiredDescription
namestringUnique identifier (kebab-case).
descriptionstringHuman-readable summary.
stagesWorkflowStage[]NoOrdered stage list. [] = freeform (no stages enforced).
commandstringNoAuto-synthesize a slash command with this name.
command_aliasesstring[]NoAliases for the synthesized command.
command_usagestringNoUsage string shown in /help.
paramsWorkflowParamSpec[]NoTyped parameters (PR-1). Supersedes legacy args:.
argsWorkflowArgSpec[]NoDeprecated. Coerced to params with type: text, positional: true.
paramGroupsWorkflowParamGroup[]NoCross-param constraints (e.g., mutually-exclusive).
runnerstringNoExplicit run mode: sequential, per-stage-prompt, coordinator-per-stage, coordinator-prompt, roster. Auto-detected if omitted.
prompt_templatestringNoMustache+${} template for coordinator-prompt mode.
mode'planning' | 'implementation'NoSafety mode — planning restricts writes to plan-dir.
completionWorkflowCompletionSpecNoCompletion contract (see below).
cyclesbooleanNoOpt-in for backward goto: edges. Default false.
maxStageVisitsnumberNoRuntime cap on per-stage visits (1–500). Default 50 for cyclic workflows.
phaseTimeoutMsnumberNoPer-stage send/wait timeout (ms). Default 30 minutes.
retry{ default: RetryPolicy }NoWorkflow-level default retry applied to stages without their own.
budgetsWorkflowBudgetsNoIteration/time/token/cost caps for cyclic workflows (PR-8).
convergenceWorkflowConvergenceNoConvergence detector for iterative loops (PR-8).
antiLoopWorkflowAntiLoopNoAnti-loop journal — abort if a file is mutated N+ times (PR-8).
requiredRolesstring[]NoRoles that must be installed at load time (PR-12).
requiredSkillsstring[]NoSkills that must be installed at load time (PR-12).
requiredGatesstring[]NoValidation gates that must be installed at load time (PR-12).
crewCrewV2NoInline workflow-bundled crew (PR-12).
versionstring | numberNoAuthor-managed version tag.

completion: sub-object

FieldTypeDescription
requiredArtifacts / required_artifactsstring[]Files that must exist for the workflow to be considered done.
stableChecks / stable_checksnumberConsecutive stable stat reads before an artifact is durable.
pollIntervalMs / poll_interval_msnumberPoll interval (ms) while a stage send is in flight.
monitorbooleanWire onWorkflowComplete for unattended singleShot drivers.
seedPlaceholders / seed_placeholdersArray<{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

FieldTypeDescription
namestringRequired. Unique stage identifier.
agentsstring[]Role names to spawn. Mutually exclusive with subgraph.
agentType / agent_typeAgentTypeExplicit agent type for the stage.
parallelbooleanSpawn agents in parallel.
after'all' | string[]Wait for named stages (or all) before starting.
artifactstringExpected output file (relative to plan dir).
workersRequired / workers_requirednumberMin parallel workers; triggers retry if fewer spawn.
timeoutMs / timeout_msnumberPer-stage drain/poll timeout override (ms).
retryRetryPolicyPer-stage retry (overrides workflow-level retry.default).
nextNextSpecConditional edges — predicate-driven routing (Phase 3A).
subgraphstringDelegate to another workflow (mutually exclusive with agents).
inputsRecord<string, string>Parent→child placeholder map (subgraph only).
outputsstring[]Child artifacts to surface back to parent (subgraph only).
forEachForEachStageFan-out / fan-in (Phase 3B).
interruptInterruptStageHuman-in-the-loop gate (Phase 3C).
postCheckstring | {kind, params}Post-stage validation handler.
postChecksPostCheckRef[]Plural form — array of post-check references.
postCheckSeverity'error' | 'warning'How postCheck failure is treated.
postCheckMaxRetriesnumberMax postCheck invocations.
postCheckRetryInstructionstringInstruction appended on postCheck retries.
artifactRetryInstructionstringInstruction appended when artifact is missing.
workersRetryInstructionstringInstruction appended when insufficient workers spawn.
tasksRetryInstructionstringInstruction appended when no tasks are created.
preQuestions / pre_questionsstring[]Questions asked deterministically before the stage prompt.
validationGateInvocation[]Declarative validation gates (PR-6).
gatesstring[]Shorthand gate names (auto-synthesized to validation:).

Parser quirks

  • Both snake_case and camelCase accepted for most fields (e.g., workers_required and workersRequired both 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

Workflow complexity ladderThe 7 progressive levels — each builds on the previous, from minimal sequential to full autopilot.

Stage execution modelsThe 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.

markdown
---
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] — the summarize stage waits for research to 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.

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

markdown
---
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 /investigate in the REPL.
  • command_aliases: [inv]/inv also 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 --flag needed).

Param types reference (src/skills/types.ts:338–354):

TypeExtra fieldsNotes
textmaxLengthFree-form string.
integermin, maxValidated as integer.
booleanFlag-only (--name / --no-name). Cannot be positional.
enumvalues: string[]Must match one of the listed values.
slugpatternDefaults to ^[a-z0-9][a-z0-9-]*$.
pathmustExistValidated against isPathInsideDir.
git-rangeexpandValidates refs via git rev-parse; expands ${files}.

Level 4 — Conditional edges (next:)

Branch execution based on artifacts, placeholders, or prior-stage results.

markdown
---
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):

PredicateTestsExample
ifArtifactFile exists in planDir{ ifArtifact: "report.md", goto: process }
ifArtifactMissingFile does NOT exist{ ifArtifactMissing: "report.md", goto: retry }
ifPlaceholderPlaceholder value test{ ifPlaceholder: { name: x, equals: "y" }, goto: z }
ifEnvEnvironment variable test{ ifEnv: { name: CI, exists: true }, goto: ci-path }
ifResultPrior-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. Omitting else: falls through to the next stage linearly.
  • Use goto: end to terminate the workflow cleanly.
  • Backward gotos require cycles: true at the workflow level, plus a maxStageVisits cap.

ifPlaceholder operators: equals, contains, matches (regex), exists (boolean).

ifResult fields: satisfied, retried, agentsSpawnedAtLeast, errored.


Level 5 — Fan-out / forEach (map pattern)

forEach fan-outThe 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.

markdown
---
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):

FieldTypeDefaultDescription
itemsItemSourceRequired. Where items come from.
parallelbooleantrueFan-out in parallel.
maxConcurrencynumbermin(8, items.length)Concurrent dispatch cap.
asContextstring'item'Placeholder name bound to each item ( above).
subgraphstringSub-workflow invoked per item (instead of stage agents).
onError'gather-all' | 'fail-fast''gather-all'Failure mode.
retryRetryPolicyPer-item retry.
gatherGatherSpecRequired. How outputs are combined.
forceLargeFanOutbooleanRequired when inline items > 50.

Item sources (src/skills/types.ts:794–799):

KindExampleNotes
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):

KindOptionsOutput
arrayJSON array of per-item results.
concatseparator, headerString concatenation.
mergeconflict: 'last-wins' | 'error'Object merge.

Level 6 — Human-in-the-loop interrupts + gates

Human-in-the-loop interruptsInterrupt stages pause for human input: approve continues, reject aborts, hold re-prompts.

Pause the workflow for human approval before continuing.

markdown
---
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):

FieldTypeDefaultDescription
waitFor'user'Required. Transport (v1 only supports user).
promptstringRequired. Displayed to the user at suspend time.
onResumeRecord<string, InterruptAction>{ approve: 'continue', reject: 'abort' }Decision routing map.
maxRepromptsnumber3Re-prompts before auto-abort.
storeAsstringStore the user's answer as a placeholder for downstream stages.
timeoutMsnumberIdle 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 / approvecontinue
  • no / n / cancel / rejectabort

Resume commands:

  • /approve — shortcut for continue
  • /reject — shortcut for abort
  • /workflows resume <thread-id> <decision> — explicit resume

Mutual exclusions: interrupt stages cannot have agents, artifact, parallel, or retry.


Level 7 — A full autopilot

Autopilot loopThe 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):

yaml
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: true

Key patterns demonstrated:

  • Cyclic loopsvalidate → fix retry loop, review → convergence-check → review convergence loop.
  • Fan-out via forEach — the review stage fans out over 4 angles; fix-findings fans out over a dynamic file.
  • Conditional edges — every stage uses next: to branch based on artifacts.
  • BudgetsmaxWallClockMs caps 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:

PlaceholderSource
${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:

markdown
# Phase 1: Feature Brainstorming — ${feature}

Plan directory: ${planDir}
${context}

When to use sibling prompts vs inline prompt_template:

Use caseApproach
Complex multi-stage pipelines with long promptsSibling *.prompt.md files + runner: per-stage-prompt
Single coordinator prompt that references stage metadataprompt_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 /reload to reload the skill registry in an active session.

Running it

If your workflow declares command::

/my-command arg1 --flag value

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

bash
npx vitest run src/skills/parser.test.ts
npx vitest run src/skills/workflowRunner.test.ts

Or run the full skills test suite:

bash
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

SymptomCauseFix
Workflow silently not loadedMissing name: in frontmatterAdd name: field
Stage skippedBoth subgraph: and agents: on same stageUse one or the other
postCheck ignoredBoth singular postCheck: and plural postChecks:Use only one form
forEach stage droppedInvalid items: or missing gather:Check item source format
Backward goto rejectedMissing cycles: true at workflow levelAdd 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