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. The workflows still load and remain available for direct or crew references. This is separate from --trust-project-skills, which is default-off and controls project roles, skills, and crews.

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.


Native ROOT path: a child draft ​

In native mode, a synthesized workflow command proposes a native ROOT child. Fresh sessions default to legacy; opt in with --native-root. Saved sessions retain their recorded mode. --no-native-root explicitly selects legacy execution. Opposite-mode saved-session resume is refused, not migrated. No additional --run-controls or /wf opt-in is required within native mode. The first invocation returns root_id and root_operation_id, with dispatchable:false. Repeat the same command and arguments with --root-id <returned-root-id> and --root-operation-id <returned-operation-id> to persist that exact draft. Ask the coordinator to declare its acceptance criteria and explicitly prepare and invoke root_plan_commit. Preparation, draft persistence and commit are separate; a plain command is not immediate execution.

The planner can also use task_create({subject,description,workflow,bindings,criteria,mergeOnSuccess:false}) through its prepared-operation protocol. It retains IDs on retries rather than inventing a new attempt. See Coordinator Planning for the worker/verifier local example and exact /root grammar.

All five existing modes remain available: roster, sequential, per-stage-prompt, coordinator-per-stage and coordinator-prompt, including inferred mode selection. The host uses the existing runner, not a second stage engine. Eligible managed children retain durable stage-boundary controls and resume. Other definitions use an owned opaque existing-runner child with its actual coordinator/stage sessions and descendants. Both use host-resolved provider, role/skill/crew, model, MCP and permission settings, including writable Claude and existing acknowledged bypasses where authorized.

Preparation pins the complete parsed invocation: parameter defaults and group guards, named bindings over --var values, plan directory, stage-visit limit, quiet/verbose setting, initial run controls, bridge setting and existing dispatch choice. Registry/dependency/trust changes are rechecked before crossing. Root-planning ownership records a proposal, not a fake attempt; allocation requires the real committed/admitted dispatch identity.

The existing intake, gates, retry/conditional/fanout/nested workflow, interrupt, checkpoint/rollback and handoff services remain wired. Their existing restrictions still apply: for example, interrupts are coordinator-per-stage behavior, not a new feature for every runner mode. Managed eligibility is a stage-recovery restriction, not a blanket execution ban on these features. Checkpoint-enabled runs can use the existing checkpoint manager in an owned aggregate workspace; that does not make an interrupted opaque run stage-resumable. Existing workflow token/cost probes are not hard billing guarantees.

The runner automatically tags configured iteration checkpoints. Rollback is an existing host/API-driven capability; workflows can also include explicit rollback worker stages, such as fixer-loop. Declaring a rollback block alone does not install an automatic failure trigger. In the native path, the owned wrapper checks the current scope, attempt and owner before each effect, and rollback affects only the retained workspace. Unknown effects are not blindly rolled back or replayed. This adds no CLI command and promises neither exactly-once execution nor OS containment. Opting in to native execution changes neither those boundaries nor independent trust/permission controls.

Child completion reports execution finished only. Missing validated child evidence remains held; even a declared verifier cannot manufacture that evidence. Criteria alone requires authenticated native operator acceptance via /root approve or /root deny; an independent contract uses its declared read-only verifier and any required human gate. Do not describe arbitrary child outputs as automatically accepted.

Use native /root pause|resume|stop|abort with the root ID, operation ID and original expected revision. Controls are durable requests, not immediate settlement: managed pause drains to a safe child boundary and resume continues the same owned child. A live opaque child can observe pause before further dispatch; after interruption it is lookup-or-unknown, never automatic replay. Abort requests owned cancellation; it cannot assert that external work ceased. Inspect root status for pending receipts and actual phase. Repeated child observations do not resume execution.

--run-controls=paused|running only selects the child's initial phase on this path; it does not bypass the root draft/commit barrier. For an initially paused child under a running root, use task-scoped /root resume <rootId> <operationId> <expectedRevision> <taskId>; a root-scoped resume requires the root itself to be paused or pause-requested. Legacy /wf mutations and recovery cannot bypass root ownership. --legacy-workflow-dispatch selects the existing legacy engine route inside native ownership, not legacy root authority. The existing parser rejects combining it with non-dry-run --run-controls. Dry-run stays nonexecuting and does not propose or commit a root.

Stage workers retain their work instead of automatically merging it early. An explicit host-authorized root_integration:{targetRef,targetBase} on the child contract permits accepted aggregate Git delivery for both managed and opaque children; no extra --legacy-workflow-dispatch flag is required. Synthetic commands start with mergeOnSuccess:false; acceptance alone does not add integration intent. Checked-out/current targets can use the existing non-FF merge, while the nonchecked-out strategy remains FF/CAS. Dirty or conflicted targets do not justify discarding user changes or source branches.

Terminal handoff is deferred to the existing HandoffEngine after exact aggregate evidence and acceptance. Only previously authorized targets/options may perform push/PR/handoff operations. Lost acknowledgements remain unknown, not a reason to resend. Source fixtures exercise local production paths; they are not live remote-service or model certification.

Legacy non-ROOT managed stage controls ​

The following describes existing non-root-owned managed runs and embedding callers, including --no-native-root and legacy saved-session resume. In that legacy path, --run-controls=paused|running opts into managed execution. Do not use these commands as a recovery or acceptance backdoor for ROOT children.

CommandMeaning
/wf pause <run-id>Request a pause after the active stage and its owned children settle.
/wf step <run-id>From a safe paused/ready boundary, execute one whole stage visit, then pause.
/wf resume <run-id>Continue from the persisted next stage, not from the beginning.
/wf stop <run-id>Drain the active stage, then stop without starting the next one.
/wf abort <run-id>Request cancellation of owned work and wait internally for settlement before marking aborted.
/wf inspect <run-id>Safe operator projection of the durable snapshot, not a raw state dump.
/wf recover <run-id> retry-stage --ack-side-effectsAcknowledge possible duplicate effects and prepare a whole-stage retry at a paused boundary. Then explicitly step or resume.
/wf recover <run-id> abandonExplicitly acknowledge partial effects and mark only the logical run stopped, even with unknown prior children or changed source. No external work is cancelled or undone.

Controls return after the request is persisted, not after stage drain. pause-requested, stop-requested, aborting, stepping, paused, recovery-required, and terminal phases remain distinct. A worker becoming idle does not mean the run completed. After stepping the final stage, the terminal cursor remains paused; /wf resume <run-id> finalizes without replay. Already-effective requests report changed: false; rejected requests explain why. IDs are mandatory: there is no implicit "current run", bulk control, or silent recovery. Unexpected trailing arguments and recovery flag combinations are rejected. abandon itself is the acknowledgement; do not add the retry flag.

/wf status merges managed records with legacy metrics even when observability.persistMetrics: false. Managed inspect/status/graph show the persisted phase, pinned graph digest, state revision, current/next stage, zero-based visit ordinal and retry attempt, iteration, pending action, last committed selected transition, budget and runtimeError. Inspect does not dump bindings, credentials, owner tokens, tool results, or arbitrary scratchpad. Runtime errors are process-local; durable error counts, the last recorded error and terminal reasons remain available after restart, including while paused.

/wf graph <run-id> [--svg [path]] [--no-color] [--lr] always uses the managed run's pinned definition, even if the registry changed or removed it. /wf graph <workflow-name> still renders the current static definition. Inspection never edits topology. Continuing or preparing a retry revalidates the source, dependencies, trust, permissions and working directory; changes can require launching a new run instead. Logical abandonment does not execute the workflow and does not require unchanged source or execution preflight. All controls and recovery actions remain fenced to the current workspace; inspect and managed-run lists are workspace-scoped.

Safety and supported scope ​

V1 supports only coordinator-per-stage, including ordinary forEach, retries, and conditional cycles. It rejects convergence, antiLoop, checkpoints, rollback, quarantine, subgraphs (including stage.forEach.subgraph), interrupts and terminal handoffs before persistence, filesystem effects or spawning. Intake must already be resolved into literal work items by an embedding caller; ordinary managed CLI launch does not pre-resolve intake. Managed dry-run and bootstrap placeholder seeds are rejected. The owned-stage bridge must be enabled; controls cannot be combined with --legacy-workflow-dispatch.

The host has no cumulative run-attributed token/cost attestation, so declared token and cost budgets are rejected up front rather than treated as zero usage. Original wallclock, iteration and stage-visit limits persist across controls; paused and offline time still count against wallclock. Neither SDK model/tool internals nor /loop are controlled by this interface.

A stage is the atomic control unit, including its fan-out, retries and recursive children. Crashing midstage requires explicit recovery; retry reexecutes the entire stage, not a tool or just its failed children, and can duplicate prior effects. Retry requires verifiable settlement of prior owned work, current source/permission/bridge preflight and the explicit acknowledgement. After restart, unknown provider/child state refuses retry even with the acknowledgement; PID death alone is insufficient.

Abandon stops only the logical run, even if prior children have unknown outcomes or source/trust/permissions have changed. It never replays, cancels unknown external work, rolls back partial effects, or claims underlying work has stopped. Prior child IDs and that uncertainty remain in durable errors. Both recovery actions require the current workspace, exclusive ownership and no live/scheduled host; a live or unverifiable owner cannot be taken over. Abandon is not a force-unlock or external-work cleanup mechanism.

Managed child worker worktree merges are currently forced off (mergeOnSuccess: false). Integrate worker changes explicitly after reviewing them; there is no managed CLI flag to enable automatic integration, and an embedding caller requesting mergeOnSuccess: true is rejected before launch. This does not prohibit a workflow's own tool-driven Git operations: review the workflow and its options before executing a stage.

These are local commands in the existing registry, including through the opt-in --control-socket transport; they never go through the coordinator/model prompt queue. /workflows retains its legacy interrupt/approval namespace, session /resume and global /pause-dispatch are unchanged.


3. The schema, field by field ​

Workflow frontmatter (top-level) ​

Source: src/skills/types.ts:421–531 (Workflow interface)

FieldTypeRequiredDescription
namestring✅Unique identifier (kebab-case).
descriptionstring✅Human-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.
boolean—Flag-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
itemsItemSource—Required. Where items come from.
parallelbooleantrueFan-out in parallel.
maxConcurrencynumbermin(8, items.length)Concurrent dispatch cap.
asContextstring'item'Placeholder name bound to each item ( above).
subgraphstring—Sub-workflow invoked per item (instead of stage agents).
onError'gather-all' | 'fail-fast''gather-all'Failure mode.
retryRetryPolicy—Per-item retry.
gatherGatherSpec—Required. How outputs are combined.
forceLargeFanOutboolean—Required 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
array—JSON 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).
promptstring—Required. Displayed to the user at suspend time.
onResumeRecord<string, InterruptAction>{ approve: 'continue', reject: 'abort' }Decision routing map.
maxRepromptsnumber3Re-prompts before auto-abort.
storeAsstring—Store the user's answer as a placeholder for downstream stages.
timeoutMsnumber—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 → continue
  • no / n / cancel / reject → abort

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 loops — validate → 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.
  • Budgets — maxWallClockMs 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 /wf reload or the coordinator's reload_registry tool in an active session.

When authoring through coordinator tools, pass the complete Markdown document to save_workflow. It lints the content, writes it verbatim on success, and does not update the in-memory registry until you reload.

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/) load by default, and their commands are synthesized by default. Disable command synthesis 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 ​