Flow Definition
The top-level fields that wrap phases into a taskflow.
A flow definition is a single JSON object that declares a multi-phase workflow: its name, the arguments it accepts, how aggressively it should parallelize, what it is allowed to cost, and the ordered array of phases that form its DAG. Every taskflow — whether saved to disk or passed inline as a one-liner — desugars into the same shape before the runtime validates and executes it.
You might be wondering: do I need all these fields? No. Only phases is structurally required (and name for saved flows). Everything else tunes behavior — concurrency, cost ceilings, caching defaults, strictness. This page introduces each one in the order you will actually need it, then collects the exhaustive reference material at the end.
Quick mental model
A flow is just a container. It holds metadata, a few execution tunables, and a list of phases. The phases do the real work; the container decides how they run.
{
"name": "my-flow",
"phases": [
{ "id": "do", "type": "agent", "task": "...", "final": true }
]
}Everything below is an addition to that shape. The runtime validates the whole thing before spawning a single subagent — invalid flows never cost you a token.
A running example
We will build up a realistic flow — audit a directory's routes for missing auth checks — piece by piece. Each section adds one top-level field and explains why you would reach for it. The complete flow appears at the end of the page.
Name and description
Every saved flow needs a name. It becomes the /tf:<name> command alias and the run-state directory name. A description is optional but worth adding — it shows up in /tf list and run records.
{
"name": "audit-auth",
"description": "Audit routes for missing auth checks",
"phases": [/* ... */]
}A missing or empty name is a hard validation error (Missing or invalid 'name'), checked before any phase validation. Inline shorthand flows that omit name get a synthesized fallback ("task", "parallel", or "chain").
Declare your arguments: args
Most reusable flows take arguments — a directory, a severity threshold, a feature flag. Declare them in args so the runtime can apply defaults and document them. Each arg is an object with an optional default, description, and advisory required flag.
{
"name": "audit",
"args": {
"dir": { "default": "src/routes", "description": "Directory to audit" },
"force": { "default": false },
"severity": { "default": "high", "required": true }
},
"phases": [/* ... */]
}Pass args at invocation time:
/tf run audit dir=src/api force=trueThen interpolate them inside any phase with {args.<name>}:
{
"id": "scan",
"type": "agent",
"task": "Scan {args.dir} for missing auth checks."
}Declaring args is optional. Undeclared args passed at runtime are still accessible via {args.X} — they just won't get a default. Declaring them gives you defaults, documentation, and clearer validation messages.
You might be wondering what happens if a phase references {args.X} but the caller didn't supply X and there's no default. By default the placeholder stays literal and the runtime emits a warning. Under strictInterpolation: true (see Safety nets), that warning becomes a hard error — useful for production flows where a literal {args.X} leaking into a prompt is a bug, not a curiosity.
Control parallelism: concurrency
concurrency is the default cap on how many subagents run at once across the whole flow. The default is 8. map and parallel phases inherit it unless they set their own per-phase concurrency.
{
"name": "fanout",
"concurrency": 16,
"phases": [
{
"id": "scan",
"type": "map",
"over": "{steps.discover.json}",
"task": "...",
"concurrency": 4
}
]
}Above, the per-phase concurrency: 4 overrides the flow-level 16 for that phase only.
Cap the cost: budget
Some flows are open-ended — a deep research loop, a tournament with many variants. A budget lets you set a run-wide ceiling on USD cost and/or token usage. When either is exceeded, remaining phases are skipped and the run ends with status blocked.
{
"name": "capped-run",
"budget": {
"maxUSD": 2.0,
"maxTokens": 2000000
},
"phases": [/* ... */]
}Both fields are optional — set either or both. Already-running subagents are not interrupted mid-call; the ceiling is checked between phases. A budget of 0 is treated as "exceed immediately," useful only for dry structural checks.
Where do agents come from? agentScope
Phases reference agents by name ("agent": "security-reviewer"). The runtime discovers agent definitions (.md files with YAML frontmatter) in one of three scopes:
| Value | Search path | Collision rule |
|---|---|---|
"user" (default) | ~/.pi/agent/agents/*.md | — |
"project" | .pi/agents/*.md | — |
"both" | user, then project | Project wins on name collision |
{
"name": "team-flow",
"agentScope": "both",
"phases": [/* ... */]
}Use "both" when you have shared user-level agents but want a project to override specific ones.
Safety nets: strictInterpolation
By default, taskflow is lenient about non-fatal issues — it warns and continues. Turn on strictInterpolation to promote those warnings to hard errors. This is the right call for saved, production flows: it catches silent placeholder leakage and stale field references before a run wastes tokens.
{
"name": "production-flow",
"strictInterpolation": true,
"phases": [/* ... */]
}strictInterpolation does not control the hard {steps.X} reachability check — that is always a hard error. It only promotes warnings to errors.
What is always a hard error (regardless of this flag):
{steps.X.*}whereXis not reachable viadependsOn/from(the phase would run in parallel with its producer and see the literal placeholder).- A phase referencing its own output (except
loopphases, which legitimately do this inuntil). - Unknown
dependsOn/fromtargets, dependency cycles, duplicate phase ids, more than onefinal: true.
What becomes a hard error only under strictInterpolation: true:
| Warning | Default | Under strict |
|---|---|---|
{args.X} referenced but not provided (and no default) | warning | error |
Invocation cwd does not match args.codebase | warning | error |
Loop-only fields (until, maxIterations, convergence) on a non-loop phase | warning (ignored) | error |
idempotent: false is a no-op on approval / flow | warning | error |
idempotent: false overrides cache.scope: "cross-run" | warning | error |
Both eval and score set on a gate | warning | error |
Reuse work across runs: incremental
By default, every run starts fresh — each phase re-executes even if nothing changed. Set incremental: true to default every phase to cross-run caching (cache.scope: "cross-run"), so re-running the flow reuses unchanged phase results across runs and sessions.
{
"name": "ci-audit",
"incremental": true,
"phases": [
{ "id": "lint", "type": "script", "run": "npm run lint" },
{ "id": "review", "type": "agent", "task": "Review recent changes." }
]
}A few rules govern how incremental interacts with per-phase settings:
- It is equivalent to setting
cache: { scope: "cross-run" }on every phase. - A phase's own
cachesetting takes precedence —cache.scope: "off"or"run-only"is honored. - The cross-run-blocked phase types (
gate,approval,loop,tournament,script) are never cached cross-run, even underincremental— they must produce a fresh result each run. In the example above,lintre-runs every invocation despiteincremental;reviewis cached. idempotent: falsephases are excluded from caching entirely. Underincremental, this produces a warning so you know the phase won't be reused.
A run-time incremental argument (passed at invocation) overrides the flow's declared value, letting you force a fresh or incremental run without editing the flow.
Share context between phases: contextSharing
When several phases read the same files, you can opt them all into the Shared Context Tree with a single flag. contextSharing: true is shorthand for shareContext: true on every phase — each phase's subagent gets ctx_read/ctx_write (a blackboard shared with siblings and ancestors) and ctx_report/ctx_spawn (report upward + queue child tasks). Individual phases can still opt out with shareContext: false.
{
"name": "collab-flow",
"contextSharing": true,
"phases": [
{ "id": "explore", "type": "agent", "task": "..." },
{ "id": "synthesize", "type": "agent", "task": "...", "dependsOn": ["explore"] }
]
}When context sharing is active, phase ids become filesystem node ids and must match [A-Za-z0-9._-]+. Two ids that sanitize to the same node would silently merge their blackboards — this is enforced as a hard error.
The phases array
phases is the one required field — an ordered array of phase definitions (minItems: 1). Each phase has a unique id, a type (default "agent"), and forms DAG edges via dependsOn. Phases with no dependencies run in the first topological layer.
If no phase is marked final: true, the last phase in array order becomes the workflow result. If more than one is marked final, validation fails.
The runtime computes topological layers via Kahn's algorithm: phases in the same layer run concurrently (up to concurrency). A phase starts once all its dependsOn complete — or once one completes, under join: "any".
For every field on every phase type, see the Phase Types page.
Shorthand: skip the DAG for simple work
For one-off delegations you don't need to author a full phases DAG. taskflow accepts three shorthand shapes that the runtime's desugar() expands into a complete flow before validation. A spec is recognized as shorthand when it has no phases array but has one of task, tasks, or chain.
One task
{ "task": "Summarize src/", "agent": "explorer" }Desugars to one agent phase (id "main", final: true). Optional agent, context, and contextLimit carry through.
Parallel tasks
{
"tasks": [
{ "task": "Audit auth in src/api", "agent": "analyst" },
{ "task": "Audit validation in src/api", "agent": "analyst" }
]
}Desugars to a single parallel phase (id "parallel", final: true). Context is shared across all branches: spec-level context plus the union of step-level context arrays are merged and read once.
Sequential chain
{
"chain": [
{ "task": "Find all TODO comments" },
{ "task": "Summarize them: {previous.output}" },
{ "task": "Open issues for: {previous.output}" }
]
}Desugars to sequential agent phases (step1, step2, …), each depending on the prior; the last is final: true. Each step references the prior step's output with {previous.output}.
In chain mode, top-level context / contextLimit are ignored (the runtime warns). Put them on individual steps instead — flow-level context in chain mode is deliberately unsupported.
What carries through from shorthand
All three forms carry these optional top-level fields into the desugared flow: name (defaults to "task", "parallel", or "chain"), description, concurrency, agentScope, args, budget, strictInterpolation.
Shorthand is great for experimentation. Move to the full phases DAG when you need conditional routing, loops, gates, caching, or cross-session resume.
The complete example
Here is the audit-auth flow with everything we discussed applied:
{
"name": "audit-auth",
"description": "Audit routes for missing auth checks",
"version": 2,
"args": {
"dir": { "default": "src/routes", "description": "Directory to audit" },
"severity": { "default": "high", "required": true }
},
"concurrency": 8,
"agentScope": "both",
"strictInterpolation": true,
"incremental": false,
"budget": {
"maxUSD": 2.0,
"maxTokens": 2000000
},
"phases": [
{
"id": "discover",
"type": "agent",
"agent": "scout",
"task": "List every route handler in {args.dir} as JSON.",
"output": "json"
},
{
"id": "audit-each",
"type": "map",
"over": "{steps.discover.json}",
"as": "route",
"agent": "security-reviewer",
"task": "Does {route.path} require auth? Output {\"ok\": true|false}.",
"output": "json",
"dependsOn": ["discover"]
},
{
"id": "report",
"type": "reduce",
"from": ["audit-each"],
"agent": "writer",
"task": "Summarize the audit results: {steps.audit-each.output}",
"final": true,
"dependsOn": ["audit-each"]
}
]
}Field reference
The rest of this page is the exhaustive reference — every top-level field, the ArgSpec shape, and the full validation table. Reach for it when you need a precise answer.
Top-level fields
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
name | string (minLength: 1) | — | Yes (saved flows) | Workflow identifier. Becomes /tf:<name> and the run-state directory name. |
description | string | — | No | Human-readable summary. Surfaced in /tf list and run records. |
version | number | 1 | No | Schema/flow version tag for author tracking. Not enforced. |
args | Record<string, ArgSpec> | — | No | Declared invocation arguments with defaults. See ArgSpec. |
concurrency | number | 8 | No | Default max concurrent subagents. Inherited by map/parallel phases. |
budget | Budget | — | No | Run-wide cost / token ceiling. See budget. |
agentScope | "user" | "project" | "both" | "user" | No | Where to discover agents. |
strictInterpolation | boolean | false | No | Promote validation warnings to hard errors. |
contextSharing | boolean | false | No | Enable the Shared Context Tree for all phases. |
incremental | boolean | false | No | Default every phase to cross-run caching. |
phases | Phase[] (minItems: 1) | — | Yes | Ordered phase definitions. See Phase Types. |
ArgSpec
Each entry in args is an object with these fields:
| Field | Type | Default | Description |
|---|---|---|---|
default | unknown | — | Value used when the arg is not supplied at runtime. Type is not enforced — interpolate carefully. |
description | string | — | Human-readable arg documentation. |
required | boolean | — | Author-declared required flag (advisory; the runtime applies default when missing). |
Validation behavior
Every flow is validated before execution, in two modes:
- Static (
/tf verify, no runtime args): structural checks only — schema shape, DAG integrity, cycle detection, reachability of{steps.X}refs. Zero tokens. - Runtime (at run start, with resolved args): everything above plus arg-presence warnings and cwd/codebase mismatch warnings.
Hard errors (always fail the run)
| Error | Cause |
|---|---|
Missing or invalid 'name' | name missing, empty, or not a string. |
Taskflow must have at least one phase | phases missing or empty. |
Duplicate phase id: X | Two phases share an id. |
Phase 'X': unknown type 'Y' | type not in the 10 allowed values. |
Phase 'X': dependsOn unknown phase 'Y' | A dependsOn or from entry has no matching phase. |
Dependency cycle detected: A -> B -> A | Kahn's algorithm found a cycle. |
Only one phase may be marked 'final' (found N) | More than one final: true phase. |
Phase 'X': task references {steps.Y.*} but 'Y' is not reachable via dependsOn | A {steps.Y} ref where Y isn't a transitive ancestor (unless join: "any"). |
Phase 'X': references its own output via {steps.X.*} | Self-reference (except loop phases). |
Phase 'X': id uses underscores | Phase id contains _ (use hyphens). |
Phase 'X': agent name 'Y' uses underscores | Agent name contains _. |
Phase 'X': agent 'Y' has invalid name format | Agent doesn't match /^[a-z][a-z0-9-]*$/. |
Phase 'X': ids used with context sharing must match [A-Za-z0-9._-]+ | Context-sharing phase id has invalid charset. |
Warnings (promoted to errors under strictInterpolation)
| Warning | Notes |
|---|---|
{args.X} referenced but not provided | No default, no runtime value. Placeholder stays literal. |
Invocation cwd ≠ args.codebase | Agents relying on cwd may inspect the wrong repo. |
| Loop-only field on non-loop phase | until / maxIterations / convergence are silently ignored. |
idempotent: false no-op on approval / flow | These phases run no subagent; the flag has no effect. |
idempotent: false overrides cache.scope: "cross-run" | A side-effecting phase is never cached. |
idempotent: false under incremental | Phase excluded from caching, re-runs every invocation. |
Both eval and score on a gate | eval runs first; usually one suffices. |
Run /tf verify early and often. It catches every hard error above at zero cost, before any subagent is spawned.
isShorthand recognition
A spec is treated as shorthand when it has no phases array and at least one of:
chainis a non-empty arraytasksis a non-empty arraytaskis a string
Otherwise it is validated against TaskflowSchema as a full flow.
Next
Phase Types
Every field on every phase type.
Caching
Cross-run memoization, fingerprints, and TTLs.
Interpolation
Placeholders, conditions, and the when / until expression grammar.
Last updated on
Was this helpful?
Help us improve the docs or ask a question in the community.