An experimental attempt to re-implement Claude Code's workflows feature on top of pi.
Workflow scripts fan out agents through pi-subagents, enforce concurrency and output-token limits, and persist
enough state for deterministic replay.
Unreleased.
0.1.0-rc.0is packaging metadata for an unreleased hardening snapshot, not a supported or published release. The runtime milestones and automated build/package gates are complete and verified;pnpm checkand the packed-package CI matrix pass. Do not publish or tag it until the minimum/latest released-host matrix and manual TUI QA are complete.
| Surface | Target | Current verification state |
|---|---|---|
| Node.js | >=22.19.0 |
Source and packed-package tests pass on Node 22.19.0 and latest Node 22 in CI |
| pi | @earendil-works/pi-coding-agent 0.80.9 minimum and latest compatible release |
Minimum/latest side-by-side host runs are unverified |
| pi-subagents | Same-process host protocol version: 1 |
GitHub fork adrianorocha-dev/pi-subagents#main at 1321b3d (containing the required host work from 6462f03); the minimum/latest released-host matrix is unverified |
| Build/package | dist/index.js plus dist/runtime/worker-entry.js, packed production-only install |
Build, artifact validation, tarball install, and installed-package smoke tests pass in CI |
| Interactive behavior | TUI, FleetView, cancellation, resume, narrow/wide terminals, theme changes | Manual TUI gate unverified |
pnpm check and the packed-package CI matrix pass. The remaining release gates are the minimum/latest released-host
matrix and manual TUI QA.
The source suite verifies the completed remediations:
- Nonterminal crash resume preserves the valid cached prefix and durable budget, including interrupted child runs.
- Child workflow calls replay positionally and render nested phases, logs, and agents in journal-backed UI groups.
- Metadata-validated steering is durably captured and invalidates the affected replay suffix.
- Shutdown is bounded, settles/stops active work, terminalizes once, removes listeners, and releases runtime ownership.
pi-workflows discovers the host at Symbol.for("pi-subagents:host"). It deliberately has no production,
peer, optional, or bundled dependency on @tintinweb/pi-subagents: pi packages have separate module roots, and a
bundled copy would create a second manager. Development and contract tests use the fork directly through the
github:adrianorocha-dev/pi-subagents#main dev dependency; the lockfile resolves it to an exact
commit so CI tests the same host implementation.
Install the compatible host first, then Workflows, and restart pi.
While the host work is pending upstream, the supported distribution is the GitHub fork's main branch:
git clone https://github.com/adrianorocha-dev/pi-subagents.git
cd pi-subagents
git checkout main # currently 1321b3d
npm ci
pi install /absolute/path/to/pi-subagentsFor local package testing only (not a release endorsement):
cd /absolute/path/to/pi-workflows
pnpm install --frozen-lockfile
pnpm build
pi install /absolute/path/to/pi-workflowsThe production build and packed-install flow pass in CI. The commands above reproduce the package locally. After all
remaining gates are cleared, a future published package can be installed with
pi install npm:pi-workflows@<released-version>.
/workflows remains available for diagnostics. The Workflow LLM tool is registered only after a compatible root
host is found. A missing host leaves the tool absent and reports:
No pi-subagents host was found. pi-workflows requires a compatible pi-subagents host at Symbol.for("pi-subagents:host"). Install github.com/adrianorocha-dev/pi-subagents commit 6462f03 or a future release containing it and exposing SubagentHostV1 (host version 1), then restart pi.
An old/incompatible host reports its discovered value:
Unsupported pi-subagents host version <found>; pi-workflows requires host version 1. Install github.com/adrianorocha-dev/pi-subagents commit 6462f03 or a future release containing it and exposing SubagentHostV1 (host version 1), then restart pi.
There is no fallback runner and no private pi-subagents/dist/* import.
Upgrade/install pi-subagents first, restart pi, and confirm /workflows can see host version 1 before testing
Workflows. A pre-host or host-version-0 setup is not migrated to a private runner; it stays tool-disabled. Journal
format version 1 is self-contained, so retain the complete workflow-<runId> directory when moving a project.
Unsupported future host and journal versions fail explicitly rather than being guessed. Reload/restart after adding
saved workflows because discovery is snapshot at session start.
The Workflow tool accepts:
| Field | Type | Meaning |
|---|---|---|
script |
string | Inline workflow source |
scriptPath |
string | JavaScript file resolved from session cwd; a leading @ is accepted |
name |
string | Saved workflow from the session-start registry snapshot |
args |
JSON value | Cloned/frozen value exposed as global args |
resumeFromRunId |
string | Prior run with a terminal or interrupted valid journal, used for positional replay |
budget |
positive integer | Hard aggregate output-token ceiling |
Source precedence is scriptPath > script > name. With only resumeFromRunId, the validated prior run's persisted
workflow.js is used. Both terminal journals and valid interrupted nonterminal journals can be resumed. TUI and RPC
starts request confirmation; print and JSON modes are non-interactive.
Every script starts with a pure metadata export followed by an async body:
export const meta = {
name: "review",
description: "Review files and verify findings",
whenToUse: "Before merging a substantial change",
phases: [
{ title: "Review", detail: "Independent reviewers" },
{ title: "Verify", detail: "Adversarial verification" },
],
}
phase("Review")
const values = await pipeline(
args.files,
(file) => agent(`Review ${file}`, { label: `review:${file}`, phase: "Review" }),
(review, file) => review === null
? null
: agent(`Verify this review of ${file}:\n${review}`, { phase: "Verify" }),
)
return values.filter(Boolean)meta.name and meta.description are required. whenToUse and phases are optional. Metadata must be a pure
object literal so discovery and permission UI never execute the body. Scripts are JavaScript, not TypeScript, and
the final value must be JSON-serializable.
Returns Promise<string | object | null>. A terminal agent failure, stop, skip, or missing structured result
returns null. Invalid policy, exhausted budget, and workflow/runtime errors throw and may be caught in the script.
| Option | Type | Meaning |
|---|---|---|
label |
string | Progress label; otherwise derived from the prompt |
phase |
string | Explicit group; use inside concurrent stages to avoid a shared phase-cursor race |
schema |
JSON Schema object | Injects StructuredOutput and returns its validated object |
agentType |
string | pi-subagents registry type; omitted uses embedded general-purpose |
model |
string | Fuzzy host model override, subject to model-scope policy |
thinking |
off…max |
Thinking override, clamped by the host |
maxTurns |
positive integer | Per-agent turn ceiling |
isolation |
"worktree" |
Fresh git worktree |
cwd |
string | Host-validated working directory |
Agent-type frontmatter remains authoritative. Workflow requests use the host's external queue, suppress individual
nudges and parent bookkeeping, carry workflow metadata, and exclude pi-workflows from managed children. The source
contract suite verifies this host binding and metadata-validated steering capture against the required host contract.
parallel(thunks)runs thunks concurrently as a barrier, preserves order, and maps a thrown thunk tonull.pipeline(items, ...stages)advances each item independently without a stage barrier. A failed stage maps that item tonulland skips its remaining stages. Stages receive(previous, originalItem, index).phase(title)changes the current progress phase for later non-concurrent calls.log(message)appends a narrator event to journal and UI. Log deliberate sampling or dropped coverage.argsis the invocation's cloned, frozen JSON value.budgetis{ total, spent(), remaining() }; without a ceilingtotalisnulland remaining isInfinity.workflow(nameOrRef, args?)invokes one saved child name or{ scriptPath }. Children share the parent's budget, concurrency, agent count, and abort signal; grandchildren are rejected. Child calls participate in positional replay, and their phases, logs, and agents appear in nested journal/UI groups.
Guard budget loops with both a nullable-total check and a finite iteration bound:
for (let round = 0; budget.total !== null && budget.remaining() > 4_000 && round < 5; round += 1) {
await agent(`Round ${round}`, { phase: "Review" })
}The runtime rejects more than 1,000 lifetime agent calls or more than 4,096 items in one combinator. Date.now(),
Math.random(), and argumentless new Date() throw to protect replay determinism.
| Pattern | File |
|---|---|
| Non-barrier pipeline | examples/pipeline-smoke.js |
| Barrier fan-out | examples/barrier-smoke.js |
| Budget-aware loop | examples/budget-loop.js |
| JSON Schema result | examples/structured-output.js |
| Resume-safe deterministic calls | examples/resume-safe.js |
| Parent/child composition | examples/child-parent.js and examples/child-review.js |
| Review/verify pipeline | examples/review-changes.js |
Run a file by asking pi to call Workflow with { "scriptPath": "examples/pipeline-smoke.js" }, or copy it to a
saved-workflow root.
Discovery happens once at session_start, with first match winning:
<project>/.pi/workflows/<name>.js<project>/.agents/workflows/<name>.js$PI_CODING_AGENT_DIR/workflows/<name>.js
Project roots are ignored unless trusted. meta.name must match the filename. Canonical duplicate names,
symlinks/root escapes, non-regular or changing files, malformed source, and metadata mismatches are diagnosed and
ignored.
Pi packages execute with the user's full OS permissions, so review this package before installation. Workflow
script bodies run in a worker-backed VM without process, filesystem, network, timers, eval, Function,
require, or dynamic import. The worker is terminated on timeout/cancellation and has a memory limit. This is
capability reduction, not an OS sandbox. Spawned agents retain the tools permitted by their type; project workflows
and project agent definitions are trusted content. Use worktree isolation when agents may edit in parallel.
Each run is stored under .pi/output/workflow-<runId>/ with workflow.js, run.json, journal.jsonl, per-agent
JSONL transcripts, and hashed sidecars for values over 256 KiB. Replay re-executes from the top and matches calls
positionally by prompt/options hash. It returns the longest eligible cached prefix in recorded completion order and
runs the first mismatched suffix live. Logical output includes replayed usage; physical output is newly spent usage.
resumeFromRunId supports terminal or interrupted valid journals. An interrupted journal must be valid and inactive.
Nonterminal crash resume,
including interrupted child workflows and durable budget carry-forward, is covered by a real subprocess-kill test.
When an active run is selected, approval stops and settles it before replay planning; denial leaves it running.
Metadata-validated steering is journaled before completion and makes that invocation and its suffix cache-ineligible.
Global settings are in $PI_CODING_AGENT_DIR/workflows.json (normally ~/.pi/agent/workflows.json). Trusted
projects may override fields in <project>/.pi/workflows.json.
| Field | Default | Meaning |
|---|---|---|
enabled |
true |
Allow a compatible root session to register Workflow |
maxConcurrent |
min(16, max(1, cores - 2)) |
Per-run active-agent ceiling |
globalMaxConcurrent |
null |
Optional session-wide ceiling |
timeoutMinutes |
60 |
Positive whole-minute wall-clock limit |
retentionDays |
14 |
Age limit for terminal, unreferenced run directories |
Malformed files warn and fall back per field/file. Project settings override global settings. Writes are atomic and
project writes are refused when untrusted. TUI users can use /workflows → Settings.
Public event channels and payloads:
| Channel | Payload |
|---|---|
workflows:started |
{ runId, name, scriptPath } |
workflows:phase |
{ runId, title } |
workflows:agent |
{ runId, callKey, phase, status, agentId } |
workflows:log |
{ runId, message } |
workflows:completed |
{ runId, status: "completed", returnValue, tokens, agentCount } |
workflows:failed |
{ runId, status, returnValue, tokens, agentCount, error } |
tokens contains input, output, cacheRead, cacheWrite, logicalOutput, and physicalOutput. Existing
subagents:* events receive workflow metadata from the managed host request.
RPC protocol version 1 uses replies on <channel>:reply:<requestId>:
| Request channel | Request after requestId |
Success data |
|---|---|---|
workflows:rpc:ping |
none | { version: 1, methods: ["ping", "run", "status", "stop"] } |
workflows:rpc:run |
Workflow fields | { runId } |
workflows:rpc:status |
{ runId } |
Full RunSnapshot |
workflows:rpc:stop |
{ runId } |
{ runId, stopped } |
Replies are { success: true, data } or { success: false, error }. Handlers bind only in the root session.
- TUI: returns a background handle and one follow-up completion;
/workflowsmanages runs, saved scripts, journals, cancellation, settings, and nested child workflow groups. Automated UI contracts pass; the manual TUI gate remains unverified. - Print (
pi -p): waits for terminal completion and returns one terminal result before exit. - JSON: waits and emits one inline tool result inside pi's JSONL stream; Workflows must not print human text to stdout, preserving valid JSONL.
- RPC: starts in the background after correlated UI confirmation and uses request-scoped replies.
Automated source contracts cover these mode branches with fakes, and packed-package mode tests pass in CI. Real minimum/latest host runs remain unverified until their explicit matrix executes.
- Open
.pi/output/workflow-<runId>/journal.jsonl; inspectrun_end, then the last agent/log/phase event. Cached values are journal truth too. - Inspect
run.jsonfor status, source hash, resume source, and terminal metadata. - Inspect the relevant per-agent JSONL transcript for provider/tool failures.
- If
Workflowis absent, run/workflows, follow the exact host diagnostic, and restart pi. - If a saved name is absent, verify project trust, filename/
meta.name, diagnostics, and session snapshot timing. - For replay divergence, compare source, args-derived prompts, options, steering, and budget observations at the first diverged call. A steered invocation intentionally invalidates itself and the following suffix.
- For timeout/cancellation, inspect the terminal journal event. Shutdown stops active agents, waits for settlement within a bounded grace period, terminalizes runs exactly once, and cleans up listeners and runtime ownership.
Source-only checks (permitted without producing artifacts):
pnpm install --frozen-lockfile
pnpm checkpnpm check runs the real-host contract against the forked @tintinweb/pi-subagents dev dependency pinned by the
lockfile. During local host development, PI_SUBAGENTS_REPO=/absolute/path/to/pi-subagents pnpm test:real-host
overrides that dependency with a checkout. Neither path creates a production dependency. Use
pnpm update @tintinweb/pi-subagents after pushing a new fork commit to refresh the lockfile, and run the harness
separately against any proposed compatibility target.
The CI-verified build and package flow can be reproduced locally with:
pnpm build
pnpm check:built
pnpm pack
PI_WORKFLOWS_PACKAGE_ROOT=/absolute/path/to/clean/node_modules/pi-workflows pnpm test:packageCI runs frozen-lockfile source checks plus a packed production-only install on Node 22.19.0 and latest Node 22.
The installed-package harness covers TUI/RPC/print/JSON mode contracts and verifies that the built worker graph has no
Effect dependency and remains at or below 256 KiB. These gates passed in
CI run 29601027330.
Manual TUI QA and the minimum/latest released-host matrix remain unverified; until those gates pass, this snapshot
must not be published or described as release-ready.