Complete reference for authoring flows in pi-flows. Covers all step types with syntax, field references, and practical examples.
Flows are .yaml files with YAML frontmatter followed by step definitions. Each step has a unique id and is connected to other steps via blockedBy declarations to form a directed acyclic graph (DAG).
Location: .pi/flows/flows/<name>.yaml or a package-registered flows directory.
---
name: research-and-build
description: Research the codebase then implement changes
max_concurrent: 2
task_required: true
task_prompt: "What feature should I research and implement?"
---| Field | Required | Description |
|---|---|---|
name |
✓ | Flow identifier — for documentation; the slash command is derived from the file path |
description |
✓ | What the flow does (shown in command list and dashboard) |
max_concurrent |
Maximum agents running in parallel (default: 4) |
|
task_required |
When true, prompts the user for a task if none was provided |
|
task_prompt |
Custom prompt text shown when asking for a task |
Command name = file path, not
name:field. The slash command is derived from the file path:
File path Command .pi/flows/flows/research.yaml/research.pi/flows/flows/my-domain/apply.yaml/my-domain:apply
Steps form a DAG based on blockedBy declarations. pi-flows schedules steps that have no pending dependencies in parallel, respecting max_concurrent.
Dispatches a named agent with a task. This is the most common step type.
Syntax:
steps:
- id: my-step
agent: agent-name
task: Optional task override with ${{template}} variables
blockedBy: [step-a, step-b]
inputs:
input_name: "${{result.step-a.summary}}"
on_complete: next-step
on_error: error-handlerField reference:
| Field | Description |
|---|---|
id |
Required. Unique step identifier |
agent |
Required. Agent name to dispatch |
task |
Task override (template string). If omitted, uses the flow's task |
blockedBy |
Array of step IDs that must complete before this step starts |
inputs |
Named inputs wired from template expressions |
on_complete |
Step ID to route to on success |
on_error |
Step ID to route to on error |
Example — parallel research with fan-in:
name: multi-research
description: Research multiple domains in parallel
steps:
- id: backend-research
agent: researcher
task: Investigate backend patterns for ${{task}}
- id: frontend-research
agent: researcher
task: Investigate frontend patterns for ${{task}}
- id: synthesizer
agent: synthesizer
blockedBy: [backend-research, frontend-research]
inputs:
backend: "${{result.backend-research.summary}}"
frontend: "${{result.frontend-research.summary}}"
task: Synthesize findings from both research passesPause execution and present options to the user. The selected option determines which branch runs.
Syntax:
- id: choose-approach
type: fork
question: "Which approach do you prefer?"
options: [Quick fix, Full refactor]
branches:
Quick fix: quick-fix-step
Full refactor: refactor-step
allowCustom: true
agent: router-agentField reference:
| Field | Required | Description |
|---|---|---|
question |
✓ | Question displayed to the user |
options |
✓ | Comma-separated or YAML list of choices |
branches |
✓ | Map of option text → step ID |
allowCustom |
Appends "Other (describe)" option. Custom freetext routes through the fork's agent. Requires agent. |
|
multiSelect |
Allow selecting multiple options. All selected branches run sequentially. | |
agent |
Agent for autonomous decisions (Ctrl+A) and custom freetext routing. Required when allowCustom is set. |
|
task |
Task description for the decision agent. Defaults to question + options. |
Behavior:
- Single-select: Routes to one branch. Unselected branches are skipped (synthetic "skipped" results stored).
- Multi-select: Multiple branches run in parallel.
- After selection: User is prompted for optional notes (Enter to skip). Fork context is automatically injected into the branch step's system prompt.
Autonomous mode: When Ctrl+A is active, fork steps with an agent: field skip the user prompt and let the agent decide automatically. Forks without agent: always prompt the user.
Branch based on whether a field from a previous step's result is non-empty.
Syntax:
- id: check-research
type: conditional
check: researcher.artifacts
present: process-artifacts
absent: skip-to-buildField reference:
| Field | Required | Description |
|---|---|---|
check |
✓ | stepId or stepId.field to check. Field defaults to artifacts if omitted. |
present |
✓ | Step ID to route to if the field is non-empty |
absent |
✓ | Step ID to route to if the field is empty or the step has no result |
Supported fields:
| Field | Checks |
|---|---|
artifacts |
The <artifacts> block from finish (default) |
summary |
The <summary> from finish |
files |
The files list from finish |
status |
The status field ("complete", "error", etc.) |
Delegate a routing decision to an agent. The agent analyzes the context and calls finish with a branch name.
Syntax:
- id: route-decision
type: agent-decision
agent: router-agent
task: "Review and decide: ${{result.analyzer.summary}}"
branches:
needs-work: fix-step
ready: deploy-stepField reference:
| Field | Required | Description |
|---|---|---|
agent |
✓ | Agent name — must call finish with a valid branch |
task |
✓ | Task for the decision agent. Supports template variables. |
branches |
✓ | Map of branch names → step IDs |
Agent finish call:
finish(summary="Quality is sufficient.", branch="ready")
If the agent returns a branch not in branches, the flow errors.
Iterative verify/fix cycles. The agent decides on each iteration whether to loop back or exit.
Syntax:
- id: verify-loop
type: agent-loop-decision
agent: verifier
task: "Check iteration ${{loop.verify-loop.iteration}}/${{loop.verify-loop.max}}: ${{result.developer.summary}}"
loop_target: developer
exit_target: finalize
max_iterations: 3Field reference:
| Field | Required | Description |
|---|---|---|
agent |
✓ | Decision agent — calls finish(branch:) with loop_target or exit_target |
task |
✓ | Task for the decision agent. Supports loop template variables. |
loop_target |
✓ | Step ID to jump back to for more work |
exit_target |
✓ | Step ID to continue to when satisfied |
max_iterations |
✓ | Safety cap — forces exit when exceeded |
How the agent decides:
# Continue looping:
finish(summary="Tests still failing.", branch="developer")
# Exit the loop:
finish(summary="All tests passing.", branch="finalize")
The branch value must exactly match either loop_target or exit_target.
Full loop example:
steps:
- id: developer
agent: developer
task: Implement ${{task}}
- id: verify-loop
type: agent-loop-decision
agent: verifier
task: >
Check implementation (attempt ${{loop.verify-loop.iteration}}/${{loop.verify-loop.max}}).
Developer output: ${{result.developer.summary}}
loop_target: developer
exit_target: finalize
max_iterations: 3
- id: finalize
agent: summarizer
task: Summarize the completed implementationDelegate execution to another flow file. The sub-flow runs to completion before continuing.
Syntax:
- id: run-tests
type: flow-ref
path: .pi/flows/flows/test-suite.yaml
on_complete: deploy-step
on_error: fix-stepField reference:
| Field | Description |
|---|---|
path |
Required. Path to the sub-flow .yaml file |
on_complete |
Step ID to route to after sub-flow completes |
on_error |
Step ID to route to if sub-flow errors |
Result propagation:
- Sub-flow agent results are flat-merged into the parent's result context
- The flow-ref step ID stores the last agent's result
- Downstream steps can reference sub-flow agents:
${{result.sub-agent-id.summary}}
Template variables are placeholders in task, inputs, and question fields. They are expanded just before an agent is dispatched.
| Variable | Resolves To |
|---|---|
${{task}} |
The task passed when the flow was invoked |
${{input.NAME}} |
A wired input value (from the inputs: block) |
${{result.STEP-ID}} |
Full raw output from a completed step |
${{result.STEP-ID.summary}} |
Summary from finish(summary:) |
${{result.STEP-ID.status}} |
Status: complete, error, or blocked |
${{result.STEP-ID.artifacts}} |
The <artifacts> XML block from finish |
${{result.STEP-ID.files}} |
Comma-separated file paths created/modified |
${{result.STEP-ID.<outputName>}} |
A typed output from the agent's outputs: declaration |
${{loop.STEP-ID.iteration}} |
Current iteration (1-based) in a loop step |
${{loop.STEP-ID.max}} |
Maximum iterations configured for a loop |
Resolution order: Variables are expanded at dispatch time.
${{result.X}}is only valid if stepXhas already completed (guaranteed whenXis inblockedBy).
Missing values resolve to empty string. Referencing an unrun step, undeclared input, or non-existent field becomes
"".
Inputs wire specific data from one step to another as named variables, separate from the task text.
# agents/developer.md
inputs:
- research_output
- ticket_context - id: developer
agent: developer
blockedBy: [researcher]
inputs:
research_output: "${{result.researcher.summary}}"
ticket_context: "${{result.ticket-fetch.artifacts}}"Research context:
${{input.research_output}}
Ticket context:
${{input.ticket_context}}If inputs: is not declared in the agent's frontmatter, the input values are substituted but the system prompt has no ${{input.NAME}} to expand them into.
When an agent needs the content of a file (not just a path), use the file:// prefix:
- id: validate
agent: validator
blockedBy: [researcher]
inputs:
report: file://research/findings.mdDynamic path from previous step:
- id: summarize
agent: summarizer
blockedBy: [writer]
inputs:
report: file://${{result.writer.files}}Rules:
- The producing step MUST be in
blockedByso the file exists at dispatch time - File content is injected verbatim (never template-expanded)
- If the file doesn't exist, the step fails with a clear error
name: research-and-build
description: Research, plan, then implement
steps:
- id: research
agent: researcher
task: Investigate the codebase for ${{task}}
- id: planner
agent: planner
blockedBy: [research]
task: Create an implementation plan based on: ${{result.research.summary}}
- id: implementer
agent: developer
blockedBy: [planner]
inputs:
plan: "${{result.planner.summary}}"
task: Implement the plan. Details: ${{input.plan}}name: flexible-workflow
description: User-driven workflow selection
steps:
- id: choose
type: fork
question: "How thorough should the analysis be?"
options: [Quick scan, Deep analysis]
branches:
Quick scan: quick
Deep analysis: deep
- id: quick
agent: quick-scanner
task: Quick scan of ${{task}}
- id: deep
agent: deep-analyzer
task: Deep analysis of ${{task}}
- id: report
agent: reporter
blockedBy: [quick, deep]
task: Generate report from analysis resultsname: implement-and-verify
description: Implement with verification loop
steps:
- id: implement
agent: developer
task: Implement ${{task}}
- id: verify
agent: verifier
blockedBy: [implement]
task: Verify the implementation
- id: verify-loop
type: agent-loop-decision
agent: flow-decision
task: "Evaluate: ${{result.verify.summary}}"
loop_target: implement
exit_target: done
max_iterations: 3
- id: done
agent: summarizer
task: Summarize the completed work