pi-dynamic-workflow
A pi extension that lets the LLM author a JavaScript orchestration script and fan it out into isolated pi subagent subprocesses.
/workflow <task>— author a multi-agent workflow on the fly and run it.
The session LLM receives an authoring brief, writes a small async JS body that calls agent(...) to spawn fully-isolated subagents (each with its own context window, in its own subprocess), and composes them with parallel() / pipeline(). Subagent failures become null rather than rejecting the batch, so scripts can .filter(Boolean) and keep going. The script's return value becomes the tool's result.
Install
Install from npm with pi's package manager:
pi install npm:pi-dynamic-workflow
Or install directly from the GitHub repository:
pi install git:github.com/milanglacier/pi-dynamic-workflow
For local development, drop the path into your ~/.pi/agent/settings.json:
{
"packages": ["/path/to/pi-dynamic-workflow"]
}
User Guide
When to reach for /workflow
/workflow shines when a task benefits from independent parallel work or multi-stage pipelines with quality gates. Good fits:
- Auditing or reviewing across multiple files, lenses, or criteria (security, correctness, style).
- Batch transformations where each file/item flows through the same pipeline (scan → fix → verify).
- Research or comparison that needs multiple perspectives synthesized by a judge.
- Any task where you'd otherwise say "do the same thing to each of these" or "check my work from a different angle."
Not every task needs an orchestra. A single-file edit or a straightforward question is better handled directly — the overhead of spawning subagents isn't worth it for a one-step job.
Prompting the agent
The LLM decides what script to write and how to structure the workflow. You steer it with natural language in your /workflow prompt. Here are the levers you can pull:
Pick a quality pattern
Tell the agent which pattern to use by name:
/workflow review src/auth.ts using the adversarial verify pattern
/workflow audit the API layer — use a judge panel with 3 reviewers
/workflow scan the codebase for bugs — multi-angle sweep: correctness, security, and performance
The agent knows these patterns from its authoring brief. If you don't name one, it picks what fits the task. See the Quality patterns reference for what each one does.
Set a spending cap
/workflow audit every module — cap spending at $2
/workflow review the entire codebase with a budget of 50k tokens
This sets maxCost or maxTokens on the tool call. The script gets a budget object it can check — once exceeded, new agent() calls throw a BudgetExceededError. In-flight agents still finish, so you get partial results rather than nothing.
Control concurrency
/workflow review all 40 services — limit to 4 at a time
Sets maxConcurrency. Useful when you're hitting rate limits or don't want to saturate your machine. The default is max(2, min(8, cpus - 2)).
Run in the background
/workflow audit the entire monorepo — run this in the background
Sets background: true. The tool returns immediately and the workflow keeps running detached. A notification message arrives when it finishes (triggering a new turn). Stop it early with /workflow-stop <runId> (no argument lists active runs).
Use a custom agent type
If you've defined agent types (see Configuring agent types), ask the LLM to use them:
/workflow review the PR — use my security-reviewer and style-checker agent types
The LLM will set agentType on the relevant agent() calls. Each agent type brings its own system prompt, default model, and tool restrictions.
Restrict what subagents can do
/workflow audit the codebase — all subagents should be read-only
The LLM will set tools: ["read", "grep", "find", "ls"] on agent() calls so subagents can't modify files.
Resume a previous run
If a run crashed or you want to re-run with small changes, ask the agent to resume:
/workflow re-run my last audit but add a performance lens — resume from the previous run
Changed prompts re-run; unchanged ones replay from cache instantly (free, marked with ↺). See Resume for how this works under the hood.
Compose with saved workflows
/workflow pipeline: first run my review-sweep on src/, then aggregate the findings
If you have saved workflows (see Creating saved workflows), the agent can compose them inline.
Quick reference: all the knobs
| What you want | How to say it in a /workflow prompt |
|---|---|
| Use a quality pattern | ... using the judge panel pattern |
| Cap spending | ... with a $3 budget or ... cap at 100k tokens |
| Limit parallelism | ... limit to 4 concurrent agents |
| Run detached | ... in the background |
| Use a custom agent type | ... use my security-reviewer |
| Read-only subagents | ... all subagents should be read-only |
| Resume a prior run | ... resume from the previous run |
| Compose saved workflows | ... first run my review-sweep, then aggregate |
| Set per-agent timeout | ... give each subagent at most 2 minutes |
| Structured output from subagents | ... each subagent should return JSON with {severity, summary} |
Configuring agent types
Agent types are reusable subagent profiles — like preset characters with their own system prompt, default model, and tool set. Define them as markdown files:
Location: ~/.pi/agent/agents/*.md (global) or <project>/.pi/agents/*.md (per-project; overrides global on name collision).
Format: YAML frontmatter for metadata; the body is the system prompt.
---
name: security-reviewer
description: Reviews code for security issues with an adversarial mindset
tools: read, grep, find, ls
model: sonnet
---
You are a security reviewer. For every file you examine:
1. Identify all trust boundaries (user input, network, filesystem, DB).
2. Trace data flow across each boundary.
3. Flag any missing validation, sanitization, or authorization.
4. Suggest concrete fixes with code examples.
Be concise. Your final message should be a bulleted list of findings
with severity (critical/high/medium/low) and a one-line recommendation.
Frontmatter fields:
| Field | Required | Description |
|---|---|---|
name |
Yes | Identifier used in agentType (kebab-case recommended) |
description |
Yes | Shown in the /workflow brief so the LLM knows when to reach for it |
tools |
No | Comma-separated tool list (e.g. read, grep, find). Omit to grant all tools |
model |
No | Model override for this agent type (e.g. haiku, sonnet) |
Once defined, the LLM can use them via agent("...", { agentType: "security-reviewer" }). Changing an agent type's .md file invalidates cached results for runs that used it — so resume will re-run those calls live.
Creating saved workflows
If you find yourself repeating the same orchestration pattern, save it as a reusable script:
Location: ~/.pi/agent/workflows/*.js (global) or <project>/.pi/workflows/*.js (per-project; overrides global on name collision).
Format: A .js file whose name is the workflow name. The first line, if a // comment, is its description (shown in the /workflow brief). The body is the same async JS you'd write inline — agent(), parallel(), pipeline(), args, etc. are all in scope.
// Scan every file in a directory for a specific issue and report findings
const files = await agent(
`List every .ts file under ${args.dir || "src/"}. Output one path per line.`,
{ label: "list-files", tools: ["find"], schema: { type: "array", items: { type: "string" } } }
);
if (!files || files.length === 0) return { findings: [], message: "No files found" };
const results = await parallel(
files.map(f => () =>
agent(`Scan ${f} for ${args.issue || "hardcoded secrets"}. Report findings concisely.`,
{ label: `scan-${f}`, phase: "scan", tools: ["read", "grep"] }
)
)
);
const findings = results.filter(Boolean);
return { count: findings.length, findings };
Usage: Reference it from a prompt (/workflow run my scan-sweep on src/auth) or compose it inline from another script (workflow("scan-sweep", { dir: "src/auth", issue: "SQL injection" })).
Reference
/workflow command
/workflow review the auth module for security issues across 3 lenses
This injects the authoring brief into the next turn. The LLM plans phases, calls the workflow tool with a script parameter, and the orchestration runs.
The workflow tool
| Parameter | Type | Description |
|---|---|---|
name |
string |
Short kebab-case workflow name |
description |
string |
One-sentence description |
phases |
string[]? |
Optional ordered phase titles (documentation of the plan) |
script |
string? |
Plain async JS body (top-level await / return allowed) |
workflowName |
string? |
Run a saved workflow by name instead of an inline script |
scriptPath |
string? |
Run a workflow script from a .js file (relative to the session cwd) |
args |
any? |
JSON value exposed to the script as args |
maxConcurrency |
number? |
Cap on concurrent subagents (default max(2, min(8, cpus - 2))) |
maxAgents |
number? |
Cap on total agent() calls per run (default and hard max 200) |
maxCost |
number? |
Budget in USD; once spent, further agent() calls throw a BudgetExceededError (soft cap) |
maxTokens |
number? |
Budget in tokens (input+output); same enforcement as maxCost |
resumeFromRunId |
string? |
Replay a prior run's journal; unchanged agent() calls return cached results |
background |
boolean? |
Return immediately; a workflow-complete message arrives when the run finishes |
Provide exactly one of script, workflowName, or scriptPath.
Script API
// Spawn an isolated subagent. Resolves to its final text, or null on failure.
const text = await agent("Summarize src/foo.ts", { label: "summarize-foo" });
// Structured output: pass a JSON Schema; resolves to a matching object (or null).
const info = await agent("Count the exported functions in src/foo.ts", {
label: "count-foo",
schema: {
type: "object",
properties: { count: { type: "number" } },
required: ["count"],
},
});
// Per-agent options: label, phase, model, tools (e.g. ["read","grep"]),
// cwd (working dir relative to the session), schema (JSON Schema),
// timeout (ms; kills the subagent and resolves null), systemPrompt,
// appendSystemPrompt, and agentType — a saved agent definition from
// ~/.pi/agent/agents/*.md or <project>/.pi/agents/*.md (frontmatter
// name/description/tools/model; body = system prompt).
const verdict = await agent("Review src/auth.ts", { agentType: "security-reviewer", timeout: 120000 });
// Run thunks concurrently. Failures become null; the batch never rejects.
const results = await parallel(files.map(f => () => agent(`Review ${f}`, { label: f })));
// Per-item pipeline: each item flows through all stages independently
// (no cross-item barrier). A throwing stage drops that item to null.
const fixed = await pipeline(files,
(_, f) => agent(`Find bugs in ${f}`, { label: `scan ${f}`, phase: "scan" }),
(bugs, f) => bugs?.includes("BUG")
? agent(`Fix these bugs in ${f}: ${bugs}`, { phase: "fix" })
: bugs,
);
phase("aggregate"); // set the default phase for subsequent agent() calls
log("merging results"); // progress note shown in the TUI
const inputs = fixed.filter(Boolean); // always drop nulls before aggregating
// budget reflects the tool's maxCost/maxTokens caps.
if (budget.exceeded()) return { partial: inputs.length };
// Compose a saved workflow inline (one nesting level; shares this run's
// concurrency, abort, budget, and agent accounting).
const sub = await workflow("review-sweep", { target: "src/auth" });
return { summary: inputs.length }; // return value becomes the tool result
Scripts must be deterministic so runs can resume: Date.now(), Math.random(), and
zero-arg new Date() throw in the sandbox — pass timestamps or seeds in via args.
Saved workflows (technical)
Saved workflows are discovered from ~/.pi/agent/workflows/*.js (user) and <project>/.pi/workflows/*.js (project; overrides user on name collision). The file name (minus .js) is the workflow name; a leading // comment is its description. Run them via the workflowName tool parameter or compose them from a script with workflow(name, args). See Creating saved workflows for a step-by-step example.
Resume
Every run writes an append-only journal to ~/.pi/agent/pi-dynamic-workflow/runs/<runId>.jsonl
(successful agent calls only, keyed by a hash of prompt + behavioral options — including
the resolved agentType definition, so editing an agent .md invalidates its cached
calls; the 50 newest runs are kept). Re-invoking the tool with resumeFromRunId replays
matching calls from cache — shown with a ↺ icon, free of budget — and only runs what
changed.
Background runs
background: true makes the tool return immediately with the run id; the run continues
detached and injects a workflow-complete message (triggering a turn) when it finishes.
Stop one early with /workflow-stop <runId> (no argument lists active runs). Background
runs die with the pi process, but their journal makes them resumable.
Design rules
These are the rules the LLM follows when authoring workflow scripts. They're also a good mental model for understanding how workflows behave.
- Pipeline by default. Only add a barrier (sequential
parallelbatches) when a stage genuinely needs to see all items at once (e.g. cross-file dedup, global ranking). Independent per-item work should flow throughpipelineso fast items don't wait for slow ones. - Null-filter religiously.
agent(),parallel(), andpipeline()all yieldnullfor failures..filter(Boolean)before joining or aggregating. - Keep prompts self-contained. Subagents share nothing with you or each other. Include file paths, acceptance criteria, and output format in every prompt. Tell agents to be concise — their final message is the return value.
- Use
schemawhen you need machine-readable output (counts, verdicts, lists). Plain text is fine for prose to be aggregated by another agent. - Scale to what the user asked for. A two-step task needs two agents, not a judge panel. Reserve heavy patterns for tasks that demand rigor.
- Concurrency is capped (default
max(2, min(8, cpus - 2))); you may launch many agents and let the scheduler queue them. SetmaxConcurrencylower for heavy tasks. - Restrict tools for read-only analysis agents (
tools: ["read","grep","find","ls"]) so they cannot mutate the repo. - Set
timeouton agents that could wander andmaxCost/maxTokenson expensive fan-outs; a timed-out agent resolves to null like any other failure. - Use
background: truefor long runs the user shouldn't wait on; report the run id so it can be stopped (/workflow-stop) or resumed later.
Quality patterns
Patterns the LLM can use when a task demands rigor. Name them in your prompt to steer the agent (see Prompting the agent).
- Adversarial verify: producer agent creates, verifier agent (different prompt, fresh context) checks against explicit criteria; loop or fix on failure.
- Loop until clean:
whilea checker agent reports issues (bounded iterations, e.g. 3), run a fixer agent on the report. - Judge panel: N agents answer independently, one judge agent compares and synthesizes — good for ambiguous questions.
- Multi-angle sweep: fan out agents with different lenses (correctness, security, performance) over the same target, then merge.
Safety
The script is the session LLM's, not untrusted user input. The node:vm sandbox provides isolation hygiene, not a security boundary — treat it as an execution context for code the same model wrote, not a jail.
To prevent the obvious failure mode (a script that spawns subagents that spawn workflows that spawn…), nested subagent depth is capped at 3, total agent() calls per run at 200, and in-script workflow() composition at one nesting level.
Development
npm install
npm run typecheck # tsc --noEmit
npm test # node --test "test/*.test.ts"
Tests use zero deps — node --test with native TypeScript type stripping, and route all subprocess spawning through a fake pi stub so they never invoke the real binary.
License
MIT.