@nicknisi/pi-workflows
Model-facing front door to the first-party workflow engine — run JS workflow scripts over the subagent runtime, replacing the third-party @quintinshaw/pi-dynamic-workflows extension
Package details
Install @nicknisi/pi-workflows from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@nicknisi/pi-workflows- Package
@nicknisi/pi-workflows- Version
0.3.1- Published
- Aug 27, 2026
- Downloads
- 909/mo · 32/wk
- Author
- nicknisi
- License
- MIT
- Types
- extension, skill
- Size
- 156.7 KB
- Dependencies
- 2 dependencies · 1 peer
Pi manifest JSON
{
"skills": [
"./skills"
],
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@nicknisi/pi-workflows
The model-facing front door to the first-party workflow engine. One workflow tool runs JavaScript workflow scripts that orchestrate subagents over the in-process runtime, replacing the third-party @quintinshaw/pi-dynamic-workflows extension. A /wf command is the thin human-facing wrapper.
The platform story
Four pieces compose the workflow platform: @nicknisi/pi-shared's subagent runtime (hermetic in-process child sessions), @nicknisi/pi-codemode's VM approach (compile a model-written script in node:vm with injected bindings), @nicknisi/pi-shared's workflow.ts engine (declarative multi-stage DAGs with needs/foreach/gates/retries), and this tool as the model-facing front door that ties them together with the script contract the old third-party engine used. The third-party @quintinshaw/pi-dynamic-workflows engine is being evicted — its script contract lives on unchanged here, its built-in pattern library / model tiers / agent-type registry / trigger-word arming do not.
What it adds
workflowtool (model-facing) — actions:run(inline JSscriptORnameof a saved workflow file),list,status <runId>,stop <runId>,pause,resume./wfcommand (human-facing) —/wf list | /wf run <name> [argsJson] | /wf status <runId> | /wf stop <runId> | /wf pause | /wf resume.- Human gates inside scripts —
checkpoint(label?)pauses the run on a confirm dialog (reject/dismiss stops the run);ask(question, options?)asks mid-run (select with options, yes/no without,undefinedwhen dismissed). - Footer status — while a run is active the footer shows
wf <name> [running|paused] <last phase>. autoplanskill — teaches the model the "autoplan this" trigger: run the savedautoplanworkflow with args derived from the conversation, then respect the run's decision outcome. Disable with"skills": ["-skills/autoplan"]on the package entry.
The script contract
A workflow script is a JavaScript statement body (no imports) with a leading export const meta = { name, description } declaration and a trailing return value. The body is wrapped in an async function so a top-level return compiles; export const meta = is rewritten so node:vm compiles it (a stranded export fails loudly) and meta.name/meta.description surface in the result.
Injected globals — the exact names the old third-party tool's scripts use, so existing scripts run unchanged:
export const meta = { name: 'research', description: 'parallel research fan-out' };
const questions = ['How does the auth refresh flow work?', 'Where are sessions persisted?'];
const results = await parallel(questions.map((q) => () => agent(q, { label: 'researcher' })));
return { answered: results.length, results };
| Global | Behavior |
|---|---|
agent(prompt, opts) |
Spawns a hermetic in-process child via the subagent runtime (namespace workflows). Throws ${kind}: ${error} on failure — wrap with a safeAgent that returns { ok, value, error } so a failure inside parallel() reports which stage died instead of collapsing the wave to null. Returns res.data ?? res.text ?? null. |
parallel(thunks) |
Promise.all over zero-arg thunks — pass () => agent(...), not agent(...). |
pipeline(items, ...stages) |
Folds items through stages: each stage maps over the previous stage's outputs in parallel, producing the next array. |
phase(name) |
Logging marker that also drives the footer status (wf <name> [state] <phase>). NOT a budget boundary. |
log(...args) |
Captured into the result logs. |
checkpoint(label?) |
Human gate: suspends the run on a confirm dialog; rejecting throws and stops the run. Without a UI host it is a logged no-op. Put it before destructive or expensive steps. |
ask(question, options?) |
Human answer mid-run: a select when options are given, a yes/no confirm otherwise; undefined when dismissed. Without a UI host it throws — never invent an answer. |
args |
The args JSON value passed to run. |
budget |
{ total, spent, remaining } over the run's token usage. total defaults to Infinity; spent accumulates across agent() calls. Read-only. |
cwd |
The session working directory. |
agent() opts: model ('provider/id'), tools (allowlist — default read-only ['read','grep','find','ls']; pass ['read','bash','edit','write'] for builders), label (child agent label), systemPrompt, schema (validated; parsed JSON lands in result.data), effort (thinking level), timeoutMs, maxTurns, worktree (run the child in an isolated git worktree; on settle the change set is captured to a .patch and agent() returns { value, patchPath, runId } instead of the bare value — opt-in, so non-worktree calls are unchanged), agentType (accepted but ignored — no agent-type registry; resolve systemPrompt in the script itself).
agent() awaits the run's pause gate before every spawn: pause lets the in-flight step finish, then holds the run before the next one; resume releases it. Pause/resume are session-scoped (/wf pause, /wf resume, or the tool actions) — they apply to every active run, in practice one. Stopping or timing out a run aborts the gate, so a parked run rejects instead of hanging.
The script executes in the host process with full Node access — process, require, and fs are all reachable, the same trust boundary as the bash tool. Keep the returned value small: summaries, counts, key findings — never raw file dumps.
Saved workflows
Plain files. The registry is ls — no database, no manifest, no config keys.
~/.pi/agent/workflows/*.js— global..pi/workflows/*.js— project-local, trusted projects only (the same trust gate as codemode's/cx). Untrusted projects see only global workflows.
Names are bare file stems (research, not research.js, never a path — .. and / are rejected to prevent escaping the workflows dirs). Global shadows a same-named project workflow. Files are read on demand, so /reload needs no workflow-specific wiring.
Runs are visible
Every agent() call spawns through @nicknisi/pi-shared's subagent runtime with artifactsDir set to ~/.pi/agent/subagent-runs/, namespace workflows, and the owning Pi session recorded. Active children therefore appear in that session's default fleet view; settled and machine-wide records remain available through the fleet's explicit all scope. status <runId> and stop <runId> here read from / cancel via the same runtime's run records — no parallel store. stop cancels in-flight spawns through a live runId → AbortController registry (mirroring subagents' cascading-cancellation); a run belonging to a different host process is reported as not cancellable from here.
Migration from @quintinshaw/pi-dynamic-workflows
| Old concept | New home |
|---|---|
Built-in named patterns (e.g. research, review) |
Example .js files you drop in ~/.pi/agent/workflows/. No built-in library — the registry is ls. |
Model tiers (fast / balanced / deep) |
Explicit model: strings passed to agent(prompt, { model: 'anthropic/claude-haiku-4-5' }). No tier registry. |
agentType → tool/systemPrompt resolution |
Resolve systemPrompt in the script itself: agent(prompt, { systemPrompt: 'You are a reviewer…', tools: ['read','grep','bash'] }). agentType is accepted but ignored (logged). |
| Trigger-word arming (the tool activates on keywords) | The model calls the workflow tool when intent warrants — no arming, no keyword matching. |
| Phases with per-phase budgets | phase(name) is a logging marker only. Budget is a single run-level { total, spent, remaining }; per-stage budgets are the workflow.ts engine's tokenBudget (use runWorkflow from codemode for that). |
| The third-party engine's script globals | Unchanged: args, agent, parallel, pipeline, phase, log, budget, cwd. Existing scripts run as-is. |
Recipes
The examples/ directory ships standalone, copy-and-adapt workflow scripts — the registry is ls, so these are code you read and copy, never APIs you import (no index re-exports them). Drop any of them into ~/.pi/agent/workflows/ and run via /wf run <name>.
lanes.js— N parallel agents editing FILE-DISJOINT lanes of one repo under a hard-rules preamble (each lane owns a fixed file set; no git, no installs; the parent integrates centrally). Use it when a task splits into independent edits that don't overlap on files. Adapt by settingVERIFYto your typecheck command and filling theLANESarray with{ name, files, brief }per lane.gates.js— three judge/verify prompt builders returning prompt strings: adversarial refutation (defeats confirmation bias), deep-research coverage (defeats silent source omission), and a 3-way code-review verdict (defeats verdict collapse). Use it when you need a reliable gate inside your own workflow. Adapt by copying the builder whose failure mode you need and calling it from anagent()with a JSON schema. Prompt patterns distilled from@quintinshaw/pi-dynamic-workflows.bake-off.js— race N models on the SAME task in isolated worktrees (worktree: true), then an advisory judge reads each contender's.patchand picks a winner. Use it on hard build tasks where a single GLM-5.2-class builder produces decent-but-flawed code; the 2x token cost buys a measurably better hit rate. Adapt by settingCONTENDERSto the models to race and passingtaskinargs; the workflow returns the winner'spatchPathto apply via/patches.autoplan.js— 3 solution candidates in parallel + a Holy Grail pass, an advisor that ranks with a recommendation (curbing Grail ideas that need upstream changes), then the HUMAN decides viaask()— recommendation on top, reject-all always offered — and the chosen option gets the full plan write. Ported from osolmaz/pi-workflows' decision-gate demo. Pass{ problem, scope, constraints }inargs, or say "autoplan this" and the bundledautoplanskill derives the args from the conversation.sanity-check.js— read-only contribution review: evidence collection, four parallel area reviewers (necessity, duplication, contracts, scope/tests), then a verifier that tries to REFUTE every finding before the keep/simplify/refactor/drop/needs-evidence verdict. Ported from osolmaz/pi-workflows. Pass{ baseRef }inargs.autoimplement.js— implement a supplied plan (never devises one) behind anaskplan gate, then a bounded build → verify → review/fix loop where P0/P1 block and the round cap prevents an unbounded fix spiral. Ported from osolmaz/pi-workflows. Pass{ task, plan, verify }inargs.
Dependencies
@nicknisi/pi-shared(workspace:*) — the subagent runtime andrunWorkflowengine.typebox— the tool's parameter schema.@earendil-works/pi-coding-agent(peer) — the extension API,getAgentDir,CONFIG_DIR_NAME.
Caveats
- The script runs in the host process with full Node access — the same trust boundary as the
bashandcodemodetools. Your model, your session. checkpoint/askneed an interactive UI host. In a headless/RPC hostcheckpointdegrades to a logged no-op andaskthrows — scripts that require an answer fail loudly instead of inventing one.- Pause/resume are session-scoped and best-effort for tool-initiated runs: slash commands may queue behind an in-flight turn, so a
pauseissued mid-turn engages at the nextagent()boundary after it's processed. For a guaranteed human gate, putcheckpoint/askin the script itself. - Project-local workflows (
.pi/workflows/) load only in trusted projects; untrusted projects are limited to global workflows so a cloned repo cannot silently inject orchestration scripts. agent()cannot spawn children of its own (the ecosystem recursion guard refuses nested orchestration). For dependent multi-stage work where stages spawn, use@nicknisi/pi-codemode'srunWorkflowinstead.stopcancels only runs spawned by this host process; persisted runs from other hosts show instatusbut are not cancellable here.