taskflow

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.

The minimal shape
{
  "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.

Naming a flow
{
  "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.

Declaring args with defaults
{
  "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:

Run with args
/tf run audit dir=src/api force=true

Then interpolate them inside any phase with {args.<name>}:

Interpolating an arg
{
  "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.

Flow-level vs phase-level 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.

Setting a budget
{
  "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:

ValueSearch pathCollision rule
"user" (default)~/.pi/agent/agents/*.md
"project".pi/agents/*.md
"both"user, then projectProject wins on name collision
Using both scopes
{
  "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.

Strict mode on
{
  "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.*} where X is not reachable via dependsOn/from (the phase would run in parallel with its producer and see the literal placeholder).
  • A phase referencing its own output (except loop phases, which legitimately do this in until).
  • Unknown dependsOn / from targets, dependency cycles, duplicate phase ids, more than one final: true.

What becomes a hard error only under strictInterpolation: true:

WarningDefaultUnder strict
{args.X} referenced but not provided (and no default)warningerror
Invocation cwd does not match args.codebasewarningerror
Loop-only fields (until, maxIterations, convergence) on a non-loop phasewarning (ignored)error
idempotent: false is a no-op on approval / flowwarningerror
idempotent: false overrides cache.scope: "cross-run"warningerror
Both eval and score set on a gatewarningerror

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.

Incremental flow
{
  "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 cache setting takes precedencecache.scope: "off" or "run-only" is honored.
  • The cross-run-blocked phase types (gate, approval, loop, tournament, script) are never cached cross-run, even under incremental — they must produce a fresh result each run. In the example above, lint re-runs every invocation despite incremental; review is cached.
  • idempotent: false phases are excluded from caching entirely. Under incremental, 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.

Context sharing on
{
  "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

Single-task shorthand
{ "task": "Summarize src/", "agent": "explorer" }

Desugars to one agent phase (id "main", final: true). Optional agent, context, and contextLimit carry through.

Parallel tasks

Parallel-tasks shorthand
{
  "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 shorthand
{
  "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:

audit-auth — complete
{
  "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

FieldTypeDefaultRequiredDescription
namestring (minLength: 1)Yes (saved flows)Workflow identifier. Becomes /tf:<name> and the run-state directory name.
descriptionstringNoHuman-readable summary. Surfaced in /tf list and run records.
versionnumber1NoSchema/flow version tag for author tracking. Not enforced.
argsRecord<string, ArgSpec>NoDeclared invocation arguments with defaults. See ArgSpec.
concurrencynumber8NoDefault max concurrent subagents. Inherited by map/parallel phases.
budgetBudgetNoRun-wide cost / token ceiling. See budget.
agentScope"user" | "project" | "both""user"NoWhere to discover agents.
strictInterpolationbooleanfalseNoPromote validation warnings to hard errors.
contextSharingbooleanfalseNoEnable the Shared Context Tree for all phases.
incrementalbooleanfalseNoDefault every phase to cross-run caching.
phasesPhase[] (minItems: 1)YesOrdered phase definitions. See Phase Types.

ArgSpec

Each entry in args is an object with these fields:

FieldTypeDefaultDescription
defaultunknownValue used when the arg is not supplied at runtime. Type is not enforced — interpolate carefully.
descriptionstringHuman-readable arg documentation.
requiredbooleanAuthor-declared required flag (advisory; the runtime applies default when missing).

Validation behavior

Every flow is validated before execution, in two modes:

  1. Static (/tf verify, no runtime args): structural checks only — schema shape, DAG integrity, cycle detection, reachability of {steps.X} refs. Zero tokens.
  2. Runtime (at run start, with resolved args): everything above plus arg-presence warnings and cwd/codebase mismatch warnings.

Hard errors (always fail the run)

ErrorCause
Missing or invalid 'name'name missing, empty, or not a string.
Taskflow must have at least one phasephases missing or empty.
Duplicate phase id: XTwo 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 -> AKahn'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 dependsOnA {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 underscoresPhase id contains _ (use hyphens).
Phase 'X': agent name 'Y' uses underscoresAgent name contains _.
Phase 'X': agent 'Y' has invalid name formatAgent 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)

WarningNotes
{args.X} referenced but not providedNo default, no runtime value. Placeholder stays literal.
Invocation cwdargs.codebaseAgents relying on cwd may inspect the wrong repo.
Loop-only field on non-loop phaseuntil / maxIterations / convergence are silently ignored.
idempotent: false no-op on approval / flowThese 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 incrementalPhase excluded from caching, re-runs every invocation.
Both eval and score on a gateeval 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:

  • chain is a non-empty array
  • tasks is a non-empty array
  • task is a string

Otherwise it is validated against TaskflowSchema as a full flow.

Next

Last updated on

Was this helpful?

Help us improve the docs or ask a question in the community.

On this page