Developer path

Build and operate workflows

This page describes the package contract from the host tool boundary through the sandboxed workflow worker and its durable run store.

Installation and trust

Use Node.js 22.19 or newer. Every package is trusted Pi host code with the same filesystem and process access as Pi. Install only packages you trust, and trust the project before running workflows or piewf doctor.

pi install npm:pi-extensible-workflows
npm install -g @piewf/cli

The core package provides workflow orchestration, the reviewLoop starter, and standalone subagent tools. Add @piewf/cli only when you need the piewf command; the optional @piewf/pi-ext-roles package owns pi-role and its role APIs. Workflow and subagents create their own SDK sessions, preserve provider loading and recovery, and transport extensionSettings themselves via the existing consumer session/start/hook seam (session_start event.settings). Add @piewf/herdr alongside core for Herdr session actions and transport integration.

For source installation and local development, clone the repository and change into its directory first:

git clone https://github.com/vekexasia/pi-extensible-workflows.git
cd pi-extensible-workflows
npm ci
npm run check
pi install "$PWD/packages/core"

Core and CLI install and operate without @piewf/pi-ext-roles. Roles are an optional extension. See role migration and release gating.

For a one-session trial without changing Pi settings:

pi --no-extensions --extension "$PWD/packages/core/src/index.ts"

Workspace manifests and package-lock.json are authoritative for Pi dependency versions. Core and Herdr declare Pi runtime peers; the standalone CLI installs its runtime dependencies. This npm-workspaces monorepo publishes pi-extensible-workflows from packages/core, @piewf/cli from packages/cli, and @piewf/herdr from packages/extensions/herdr. Standalone subagent tools and their tests live in packages/core/subagents; there is no separate subagents workspace.

Updates and bundled changelog

Pi has no post-update extension hook for pi update --extensions or pi update --all. After either command, the next session_start compares the installed package version and uses the package-local CHANGELOG.md to show a non-blocking notice once for that version. The first interactive startup also shows the current release section; later starts stay quiet until the version changes. A source checkout installed directly from packages/core normally has no package-local changelog because that file is generated only during packing, so its startup notice is silent until a packed package supplies it.

The notice is offline, is skipped without UI and in print or JSON mode, and stores its marker at <agentDir>/pi-extensible-workflows/changelog-state.json. Missing or malformed changelog data and state read/write failures are best-effort and never prevent Pi startup. A failed state write can repeat the notice on the next startup.

Global and project settings

Core reads only the workflow settings files below. Shared role settings and role catalogs belong to the optional roles extension.

The optional global settings file is <agentDir>/pi-extensible-workflows/settings.json, normally ~/.pi/agent/pi-extensible-workflows/settings.json. Trusted projects may also use <cwd>/.pi/pi-extensible-workflows/settings.json. If PI_CODING_AGENT_DIR is set, it replaces ~/.pi/agent. Missing files use defaults. Both files use a strict schema: unknown keys, malformed JSON, and invalid values stop launch or resume with the source settings path in the error. The backgroundWidget setting is global-only; project settings may configure the other project-overridable fields.

{
  "concurrency": 8,
  "backgroundWidget": true,
  "modelAliases": {
    "reviewer-model": "anthropic/claude-fable-5:high",
    "developer-model": "openai-codex/gpt-5.6-luna:xhigh",
    "cheap-model": "developer-model:low"
  },
  "skills": ["*", "!experimental-*"],
  "extensions": ["**/*", "!**/unsafe.mjs"],
  "tools": ["*", "!write"]
}

concurrency defaults to 8 and accepts integers from 1 through 16. Effective settings use defaults < global settings < trusted project settings < per-run options; a workflow call's concurrency field is highest precedence. Agents queue in the order the workflow calls agent(...): when a script issues several calls concurrently (for example inside parallel(...)), the limit admits them in call order rather than in the order their replay lookups happen to finish.

The strict top-level settings keys are concurrency, backgroundWidget, codemodeTools, modelAliases, skills, extensions, extensionSettings, and tools; unknown keys are rejected. The direct extensions array contains selectors. extensionSettings is an arbitrary map of safe extension-owned JSON values; the core validates only its structure and built-in settings. A trusted project extensionSettings field replaces the complete global map, including with {}. Registered validators receive the complete effective map plus source context. The obsolete extensions.herdr shape and legacy disabledAgentResources field are rejected.

Extension selectors from settings are normalized before matching: ~ and ~/... expand from the home directory, file:// URLs are converted to filesystem paths, and relative paths resolve from the directory containing that settings file. Existing non-magic paths and the static prefixes of magic paths in settings are canonicalized with realpath when possible; *, **, and **/... remain cwd-independent patterns. At call level, those three forms also remain cwd-independent. Other relative selectors resolve from the agent's launch cwd: non-magic selectors are canonicalized, while magic patterns are path-resolved without realpath. Call-level ~ and file:// strings are not expanded by the runtime. Pi's built-in codemode, tool-search, and mcp extensions are matched by their names builtin:codemode, builtin:tool-search, and builtin:mcp, which are never resolved as paths; a list that starts with !* needs builtin:* or the individual names to keep them. Pi does not expose its built-in llama.cpp extension to extension sessions, so agents cannot use models it provides; declare the server as a provider in models.json instead.

Background workflow widget

backgroundWidget is a global boolean that defaults to true. Set it to false to disable the live background-run tree and durable background-run transcript receipts. Foreground workflow rendering is unchanged. The setting is accepted only in the global workflow settings file; it is not a trusted-project override. Non-interactive and headless hosts do not draw the widget or append receipts.

codemodeTools is a global setting that decides which workflow and subagent tools Pi codemode scripts may call. "all", the default, allows every tool. "read-only" allows only workflow_status, workflow_catalog, and subagents_inspect. "none" allows none of them. The model still calls every tool directly. Tools are registered when Pi loads the extension, so a change applies after /reload, and trusted project settings cannot set it.

Workflow and subagent tools declare an outputSchema, so codemode scripts receive their results as objects instead of JSON text; a completed foreground workflow, workflow_retry, or workflow_resume call resolves with the full value. The child agent tools agent and get_subagent_result do the same inside workflow agents. workflow_status, workflow_catalog, and subagents_inspect carry readOnlyHint, so permission extensions that read tool annotations can skip confirming them.

Model aliases

Alias names are case-sensitive and must match [A-Za-z][A-Za-z0-9_-]*. Targets may be concrete provider/model:thinking references or other aliases. Unknown targets and cycles are rejected.

Aliases work anywhere the workflow runtime accepts a model, including role files from the optional roles extension. An exact configured alias wins over a native bare model name. A thinking suffix on an alias reference overrides the resolved target's suffix, so cheap-model:low can reuse a model while changing its thinking level. Concrete call-level models must include :thinking. Resolution never falls back: an unavailable target fails with UNKNOWN_MODEL and identifies the alias, resolved target when known, and settings path.

Each launch and resume snapshots one effective alias mapping for that execution segment. A trusted project's modelAliases replaces the global map, including {} to clear it; omitted project fields inherit global settings. Completed and replayed calls retain their results; prepared identities retain their concrete models on retry and resume. Calls not yet prepared and new identities use mappings reloaded on resume. Mapping changes produce one persisted warning in run events and the inspector.

workflow_catalog lists effective alias names, kinds, and provenance without probing availability or injecting them into guidance; concrete static and resolved targets remain available through settings, doctor, snapshots, and the inspector. /workflow can list, add, edit, and delete aliases in the active effective settings source. It atomically updates only modelAliases, preserves other settings, and affects new launches and resumes without /reload.

Workflow-agent resource selectors

Trusted project settings are appended after global selector arrays, followed by generic preparation and per-agent call options. The optional roles plugin composes role selectors during preparation; all layers remain inside the root and parent ceilings. Each selector is a Minimatch-compatible pattern evaluated in declaration order; every discovered candidate starts enabled, a positive match enables it and !pattern disables it, with the last matching selector winning. Use !* followed by a positive pattern to clear the current selection before adding a narrower set. An empty selector array contributes no matches as a selector layer. Prepend !* to a positive list when it must restrict the candidate set, or use !* alone to select none.

{
  "skills": ["*", "!experimental-*"],
  "extensions": ["**/*", "!**/unsafe.mjs"],
  "tools": ["*", "!write"]
}

Skills match discovered names, extensions match discovered normalized paths, and tools are limited to the current root or parent candidate set. Patterns cannot create unavailable capabilities. The legacy disabledAgentResources field is rejected.

piewf doctor reports an error when active workflow settings still use disabledAgentResources, including empty values, because the runtime rejects that configuration. The diagnostic explains the selector polarity change and links to issue #205. It also warns when a positive-only tools selector does not form an allow-list; prepend !* when restriction is intended. Untrusted project settings remain inactive and are not reported.

Tool API

The extension registers these core tools and one command:

  • workflow runs a named inline or file-backed deterministic JavaScript workflow.
  • workflow_status reads a compact run summary for any session under the current project cwd.
  • workflow_resume resumes a budget-exhausted run with an optional budget patch and preserves its foreground/background launch mode unless overridden.
  • workflow_retry creates a linked child for an explicitly failed run, replays its completed journal operations, and preserves its launch mode unless overridden.
  • workflow_respond approves or rejects a pending checkpoint or budget decision.
  • workflow_stop stops an active workflow run by ID.
  • workflow_catalog lists registered functions and model aliases; it is registered only when the active registry has one of these entries.
  • /workflow lists and controls runs in the current Pi session.

workflow parameters

FieldTypeContract
namestringRequired and non-empty for every launch.
descriptionstringOptional human-readable metadata.
scriptstringImmutable workflow source. Exactly one of script or scriptPath is required.
scriptPathstringPath to a JavaScript workflow file, resolved from the launch project directory and read once at launch. Its exact contents become the immutable persisted source. Exactly one of script or scriptPath is required.
argsJSON valueAvailable inside an inline or file-backed script as args. Defaults to null.
foregroundbooleanWait for the final value instead of returning a run ID; the model-visible result also includes the completed run ID.
concurrencyinteger 1-16Per-run active-agent limit.
budgetobjectOptional aggregate soft/hard limits for tokens, costUsd, durationMs, and agentLaunches.
parentRunIdstringOptional terminal run in this project and Pi session for a new launch. Matching explicit named withWorktree scopes reuse its existing checkout; other scopes remain new worktrees. It never replays or resumes a run.

workflow_respond parameters

{
  "runId": "...",
  "proposalId": "...",
  "approved": true
}

Use name instead of proposalId for checkpoint decisions. A budget relaxation proposal remains pending until this tool approves or rejects its exact proposal ID.

workflow_stop parameters

{ "runId": "..." }

Stopping cancels the workflow, descendant agents, and scheduler work before persisting state: "stopped". The result reports stopped: true on success, unknown_run for an unavailable or differently owned run, and already_terminal for a completed, failed, or stopped run.

workflow_status parameters

{ "runId": "..." }

workflow_status is read-only: it searches all persisted sessions under the current project cwd without cleaning run directories. It returns RUN_NOT_FOUND for missing, corrupt, or foreign run IDs. The JSON summary includes run identity, state, failure metadata, phase, delivery mode/state, configured budget and usage, and agent shims containing only IDs, labels, paths, state, last activity, and accounting stats; it does not expose transcripts or session payloads.

workflow_resume parameters

{ "runId": "...", "expectedState": "budget_exhausted", "budget": { "tokens": { "hard": 120000 } }, "foreground": true }

workflow_retry accepts { runId: string, expectedState?: string, foreground?: boolean }. After workflow_status({ runId }), pass its returned state as expectedState; recovery rejects the call if the persisted source state changed. The source must be a persisted failed run in this project and Pi session. Recovery inherits the source snapshot's launch mode by default; set foreground to explicitly override it. Foreground recovery waits for the terminal value and run details inline; background recovery returns immediately and delivers completion or failure as a follow-up. Failure diagnostics expose that exact source ID, replayable and incomplete paths, artifact locations, and validated named worktrees for direct use with the advertised action. It returns a fresh child ID linked by parentRunId; completed agent, shell, registered-function, and checkpoint operations replay from the parent lineage, while incomplete operations execute normally. Parent usage and budget version carry forward, and concurrent retry children sharing a lineage are rejected. A budget_exhausted source must use workflow_resume({ runId, expectedState, budget? }); completed, stopped, and interrupted sources return state-specific recovery guidance. Replay does not repeat journaled side effects, but external effects performed before failure are not guaranteed exactly once.

{ "runId": "failed-run-id", "expectedState": "failed" }

Background workflow and recovery calls return a run ID immediately; completion or failure is delivered as one follow-up. Foreground workflow and recovery calls return the terminal workflow value and run details inline. Recovery control results retain their state and run identifiers alongside the inline value or descriptor. Completion values are inlined only when the persisted result is within Pi's 50 KiB tool-output limit and the current main-model context has room, including response headroom; otherwise successful completion delivers a descriptor with state, runId, resultPath, resultBytes, and inlined: false. The mode is persisted in snapshot.json, so budget resume, retry, and interrupted-run resume keep the original mode unless the recovery caller supplies an explicit override. Current snapshots without launch-mode metadata default to background recovery.

Aggregate run budgets

A launch may set run-wide soft and hard limits. Every dimension and threshold is optional; omitted values are unlimited.

{
  budget: {
    tokens: { soft: 100000, hard: 120000 },
    costUsd: { soft: 5, hard: 6 },
    durationMs: { soft: 900000, hard: 1200000 },
    agentLaunches: { soft: 8, hard: 10 },
  },
}

Token, duration, and launch limits are non-negative integers; cost limits are finite non-negative numbers. Zero is valid, and soft must be lower than hard when both are set. The former maxAgentLaunches setting and launch field have been removed; use budget.agentLaunches.

Usage covers all workflow-agent attempts, retries, and descendants but excludes the main orchestrator. Tokens count reported input plus output and exclude cache reads and writes. Accounting uses Pi's cumulative session statistics, so compaction cannot remove earlier token or cost usage from progress, budgets, or persisted attempt totals. Replayed calls retain historical usage without counting as new launches.

A soft crossing is recorded once per budget version. If more model work remains, active agents receive one wrap-up instruction before their next turn and later agents are annotated; a final crossing response completes without an injected message.

Hard launch limits are checked immediately before dispatch. Token, cost, and duration limits are checked between model turns. In-flight turns and tools may finish: a final overshooting response is accepted, while a response requiring tools or another turn stops with BUDGET_EXHAUSTED before that work begins. If accepted results let the workflow return without more model work, the run completes with an overrun event; otherwise it enters the resumable budget_exhausted state.

Duration includes active workflow JavaScript, agent work, and tool calls, but excludes pauses, checkpoint waits, and offline time. Historical usage remains charged after resume.

workflow_resume accepts the exact run ID and an optional budget patch. Omitted patch values stay unchanged and explicit null removes a limit. Tightening applies immediately; raising or removing a limit creates a persisted human-approval proposal. Every exhausted hard dimension must be raised above retained usage or removed. Answer the exact proposal ID with workflow_respond. This control is distinct from workflow_retry, which accepts only failed runs, and from per-agent retries.

/workflow and the inspector show limits, usage, percentages, budget versions, crossings, and overruns. Compact TUI surfaces omit budget rows entirely when a run has no effective limit, including after a patch removes every limit. Required exhaustion, adjustment, and approval context remains visible.

Workflow DSL

Workflow source is plain sandboxed JavaScript. It has no imports, filesystem, network, timer, or dynamic-code globals, and no direct access to the host process. The trusted host-mediated shell() primitive is the explicit exception for command execution:

PrimitiveUse
agent(prompt, options)Run one Pi agent and return its bare text or JSON value.
agent.create(options)Create a persistent agent handle with a stable explicit name and the config frozen for every turn.
handle.send(prompt, options)Run the next turn of a handle in the scope that created it, continuing its transcript. Options are outputSchema and timeoutMs.
shell(command, options)Run a platform-shell command and return its exit code, stdout, and stderr.
tools.<name>(args)Call a tool of the launching Pi session, as Pi codemode scripts do.
prompt(template, values)Safely interpolate JSON-compatible values into a prompt.
parallel(name, tasks)Run keyed independent tasks concurrently.
pipeline(name, items, stages)Apply ordered stages to keyed items.
phase(name)Record progress telemetry. In a sequential loop, use phase(slice.name) for semantic labels such as backend-safety.
log(message)Persists each bounded message with a timestamp in the run events. Attached foreground logs render in the live workflow item; background or detached logs append as bounded TUI transcript entries and are shown in Trajectory, capped at 4 KB.
checkpoint(input)Wait for an approval or rejection.
evidence(content)Persist large string or JSON values to run-scoped content-addressable storage (evidence/<sha256>.txt) and return a compact { ref, sha256, path, bytes } record to protect prompt context budgets. Read it in the script with tools.read({path: record.path}); give agents the path and a read tool. Saving identical content rejects a modified existing artifact. Resumed and retried runs get back the record first stored for that content, so a journaled read of its path replays.
withWorktree(name, callback)Materialize one explicitly named worktree scope before invoking the callback; pass the callback a frozen { path, branch } reference.

shell(command, options) inherits the workflow cwd, or the active named withWorktree(name, callback) cwd. Options are timeoutMs and string-valued environment overrides merged over the host environment. A nonzero exit is a result, not a workflow failure; launch failures and timeouts use SHELL_FAILED. Output is not shell-truncated, but a result that exceeds the 10 MB RPC boundary fails with RPC_LIMIT_EXCEEDED.

await tools.<name>(args) calls a tool of the Pi session that launched the run through Pi's ctx.executeTool(), so argument validation, tool_call and tool_result handlers, and permission checks apply as for model calls. Names use the same identifiers as codemode. A tool with an outputSchema resolves to its structured content, also when it reports an error with one, any other tool to its text; other failed or blocked calls reject with TOOL_FAILED. Calls are identified by scope, tool, and arguments, so concurrent calls replay correctly. Probing with "name" in tools throws, because the answer would depend on the session that resumes the run; the launch already checks the tools a script names. Workflow and subagent tools are not callable, and calls inside withWorktree fail because session tools resolve paths against the session cwd. A launch whose script names a tool the session cannot call fails with UNKNOWN_TOOL before any agent runs. Results are journaled, so resume and retry replay them. New calls need a Pi tool call: runs launched or recovered by the workflow, workflow_resume, or workflow_retry tools have one, while runs resumed from /workflow or at session start only replay journaled calls. Calls from a background run still execute, but Pi no longer records them on the launching tool result. The live workflow item shows calls in flight. The run state records each executed tool or shell() call with its name, path, phase, timing, and status; Trajectory lists them under CALLS and on a status-coloured script Gantt lane. For safety, tool arguments and results are not recorded in call state or shown in Trajectory, including live state and HTML exports/shares; tool errors record only the code, not the message. Tool results remain in the journal for replay. Shell calls retain their inputs, results, and error messages; inputs and errors over 16 KB are kept as prefixes, and environment overrides record names but not values. Call metadata writes are best-effort and do not change tool outcomes. A journaled result proves completion even without finish timing; its duration remains unknown. Replayed calls are not recorded again, and the number of recorded calls is not capped.

return await withWorktree("fix-tests", async () => {
  for (let attempt = 1; attempt <= 5; attempt += 1) {
    const tests = await shell("yarn test");
    if (tests.exitCode === 0) return tests;
    await agent("Fix the test failures:\n\n" + (tests.stderr || tests.stdout));
  }
  return shell("yarn test");
});

Completed shell results are journaled only after process exit and RPC validation, so cold resume replays them without rerunning the command. A host crash after a command performs side effects but before its result is journaled can run it again; use shell() primarily for verification and bounded command gates, not exactly-once mutations.

Agent options

Use label as an optional non-empty display name. Without it, a preparation hook may supply a label; otherwise the effective model name is used. Use model as a configured model alias or provider/model:thinking. Other core options are skills, extensions, tools, excludeTools (exact tool names removed after preparation hooks, as in Pi's excludeTools; patterns and workflow_result, which the agent needs to return, are rejected, the names also remove custom tools of those names, including ones setup hooks add, for the agent and its descendants; names matching no root or custom tool only warn, and setup hooks or children cannot restore an excluded tool; the names are frozen with the agent's configuration, so a later handle turn, retry or resume keeps them even if the re-evaluated option changes), contextFiles, outputSchema, retries, and timeoutMs, with withWorktree(name, callback) for worktree scopes. Unknown JSON options are forwarded to loaded extensions and otherwise ignored. For role, explicitly enable the optional roles plugin.

const report = await agent("Review the implementation", {
  label: "Implementation review",
  model: "openai-codex/gpt-5.6-sol:high",
  tools: ["!*", "read", "grep"],
});

Results are bare values. Every agent submits its final result through workflow_result; unstructured agents submit { result: "..." } and return the contained string, while outputSchema agents submit a schema-valid value. Prose is not parsed as a result. The user message of every agent(...) call and handle.send(...) turn is exactly the text you supply: the host adds no workflow, agent, phase, or parent header and no task wrapper. The once-per-send workflow_result contract lives in that tool's description, rebuilt for each turn with that turn's schema, and the child-ownership contract lives in the agent tool description, which only agents allowed to spawn children receive. Transient retry state and soft-budget instructions are not part of the task text or the system prompt, which stays stable for prompt caching. They are delivered as separate control text right after the task: a steered message on local sessions, or appended to the pane launch prompt by a transport that sets turnControlDelivered on its session (Herdr). The serialized result and every worker RPC value must fit within 10 MB.

Composition

const reports = await parallel("review", {
  api: () => agent("Inspect the API"),
  ui: () => agent("Inspect the UI"),
});
return agent(prompt("Combine these reports:\n{reports}", { reports }));

Calls from one source location must not race outside parallel or pipeline. Their structural keys make replay deterministic. Ordinary branch failures wait for sibling work to settle; cancellation is immediate.

Extensions

Trusted Pi extensions can register reusable workflow functions and per-agent setup hooks. The complete field-level authoring contract, validation rules, replay semantics, and copy-pasteable examples now live on the dedicated Extensions page.

Use the Roles page for the optional roles integration. Role-file format and authoring live in the @piewf/pi-ext-roles guide.

Prioritized agent setup hooks

Security: setup hooks are trusted extension code. They run after generic preparation and concrete model, tool, context, cwd, and capability validation, but before the selected AgentTransport creates a session. Hooks may replace or wrap the transport, but the immutable prepared launch remains the capability ceiling; never register untrusted code as a setup hook.

TypeScript: hook callbacks are contextually typed. agent.options is the workflow-owned AgentOptions object, including its typed core fields and JSON-compatible extension options. agent.sessionInput is the workflow-owned pre-session input with typed cwd, model, tools, and system-prompt fields; its customTools and extensionFactories fields use Pi's ToolDefinition and InlineExtension types. agent.transport receives an immutable PreparedAgentSession and an AgentTransportContext.

Register named hooks with an optional finite priority. Hooks run in ascending priority order (default 10); equal priorities use the stable hook name as the tie-breaker. Each retry starts from a fresh baseline and runs every hook again. Hooks receive the workflow cancellation signal, and their asynchronous runtime is not charged against timeoutMs. A thrown hook error fails the agent immediately, prevents later hooks and session creation, and is not retried as a native-session failure.

registerWorkflowExtension({
  version: "1.0.0",
  headline: "Advisor setup",
  agentSetupHooks: {
    advisor: {
      priority: 10,
      async setup(agent, context) {
        if (agent.options.advisor !== true) return;
        agent.sessionInput.extensionFactories ??= [];
        agent.sessionInput.extensionFactories.push(scopedAdvisorFactory);
        if (context.signal.aborted) return;
      },
    },
  },
});

Unknown JSON-compatible agent options are preserved for setup hooks and the selected transport, but are not inherited by child agents. Persisted attempts use transport-neutral references in agentSessions; they do not require a transcript path. The workflow session contract requires state, synchronous stats, progress subscriptions, prompt/steer/abort, and awaited idempotent disposal. Cold recovery replays the journal and never reconnects a persisted session reference.

Extensions can also register agentAttemptActions for /workflow and, when both visibleStandalone and runStandalone are supplied, for standalone /subagents runs. Workflow actions receive workflow snapshots; standalone actions receive a standalone agent snapshot and the real attempt/session/setup/handoff data without a fabricated workflow record. Action errors are reported and the relevant navigator is refreshed after completion.

Lifecycle, checkpoints, and worktrees

Run states are queued, running, pausing, paused, awaiting_input, budget_exhausted, completed, failed, stopped, and interrupted. budget_exhausted is terminal for the current execution segment but can be resumed with an acceptable budget. A run stays in pausing until its active operations finish; resuming it there (/workflow "Cancel pause", Trajectory, or resume) cancels the pending pause and keeps it running. Agent states include queued, running, waiting_for_child, paused, retrying, completed, failed, and cancelled.

Direct consumers of createLocalPiSession() receive an extension-bound session. Always await session.dispose(); disposal emits session_shutdown before releasing the native Pi session and is idempotent. Handoff code uses the same lifecycle path with a resume shutdown and a matching session_start carrying the previous session file.

Extension load failures are CLI warnings unless the workflow runtime itself cannot start, or a workflow registration cannot be proven to come from a loaded extension (see below); then the CLI stops before running anything. A workflow that calls a function of a failed extension fails with its own error. Agent options that no preparation hook of a loaded extension declares in its optionsSchema, with that extension's entry as registration source, then fail with INVALID_METADATA instead of being ignored, because a failed extension may have owned them (see the extensions guide).

The headless CLI keeps the parent session and its inter-extension event bus alive for the whole operation. On success or failure, it awaits session_shutdown and disposes the session before clearing the bus, so shutdown handlers can still communicate through pi.events.

Use a checkpoint when an agent must wait for a human or parent decision:

const decision = await checkpoint({
  name: "ship",
  prompt: "Ship this build?",
  context: { commit: args.commit },
});
if (decision !== "approved") return { approved: false };

Checkpoint prompts are limited to 1 KB and serialized context to 4 KB. Sandboxed workflow scripts receive "approved" or "rejected" from direct checkpoint(); registered functions receive a boolean from WorkflowOrchestrationContext.checkpoint. Responses are journaled and replay after cold recovery.

Use withWorktree(name, callback) for canonical worktree scopes: put collaborating top-level agents in one named callback, or put each independent parallel branch in its own named callback. The runtime creates and resolves each deterministic owned branch and worktree before invoking the callback; the callback receives a frozen public reference containing only path and branch. The caller branch is unchanged, children and retries reuse the enclosing scope, and recovery preserves the existing ownership and cleanup behavior. A later run can pass the previous terminal run's ID as parentRunId to reuse matching named scopes without copying, merging, or creating another branch. Borrowed bindings are validated on cold resume and remain invalid if their originating run is deleted.

const results = await parallel("issues", {
  issue81: () =>
    withWorktree("issue-81", async ({ path, branch }) => {
      const devResult = await developUntilApproved({
        task: "Resolve issue #81",
      });
      const worktree = { path, branch };
      return { devResult, worktree };
    }),
});
return agent(prompt("Merge these verified worktrees: {results}", { results }));

Agent grouping and drilldown

The live workflow card and /workflow group agents by their structural workflow path and include registered-function context as a breadcrumb. Repeated calls in one structural scope keep the first function name unsuffixed and label later invocations #2, #3, and so on; nested calls compose each occurrence-aware segment. Groups remain stable as later agents start, creation order is preserved inside each group, and actual parent-child agents remain nested. Use phase(slice.name) in sequential loops when semantic phase labels are more useful than function occurrence labels.

The run navigator uses a selectable Workflow root above a phase-first tree. On wide terminals, the selected node's details appear beside the tree; Enter on the Workflow root opens run actions, Enter on a phase or operation expands/collapses children or focuses leaf details, and Enter on an agent opens only agent actions. On narrow terminals, the Workflow root opens actions directly, while a phase or agent first opens details and a second Enter opens only the agent action list. Escape returns to the tree. When agent actions are open, h or the left arrow closes them; Escape remains available. Press a or A as a legacy shortcut for the applicable action list. The run script, agent prompts, effective system prompts, and completed top-level agent results can be opened in the configured external editor from the TUI; prompts are available while agents are running or after they settle. Each uses a read-only temporary copy with .js, .json, or .md content types. Phase and operation nodes have no artifact action. Agent attempt actions registered by extensions are evaluated for the latest attempt only and run with a narrow action context; built-in actions copy the agent ID and, when it owns a worktree, that branch or worktree path. Worktree ownership IDs and per-worktree copy actions are not exposed in the top-level run actions.

Run storage and executed source

Runs are stored under ~/.pi/workflows/projects/<cwd-slug>-<cwd-hash>/sessions/<session-id>/runs/<run-id>/. Identity checks use the resolved launch cwd and Pi session ID.

Every new run contains workflow.js with the exact UTF-8 JavaScript source that was preflighted and executed. It is an immutable inspection artifact written during atomic run creation and is opened through a read-only temporary copy. Completed workflow results are stored as readable pretty UTF-8 JSON in result.json; its exact byte size is reported when a completion descriptor is delivered. snapshot.json remains authoritative for execution and resume; runs from previous identity contracts cannot be resumed.

Launch snapshots require identity version 6 and persist generic agentConfigurations by logical operation or handle identity before dispatch. Older snapshots fail with RESUME_INCOMPATIBLE; relaunch instead of migrating them. Preparation is frozen for each logical agent, including model, plugin instructions, selectors, own and inherited excludeTools, context scopes, and settings. A stored configuration without excludeTools was prepared before exclusions were frozen, so its exclusions are unknown and it fails resume and retry with RESUME_INCOMPATIBLE; relaunch instead. Resume and retry revalidate trust and capabilities without rerunning preparation. New identities resolve current configuration. Extension code, skill/AGENTS/SYSTEM/APPEND bytes, and external state are not frozen and are reread under current trust and ceilings. Launch-mode changes use the same atomic mutation lane as prepared configurations. This is not an exactly-once guarantee for external effects.

Each run's private system-prompts.json records the full effective system prompt at every native Pi agent_start, after before_agent_start extensions have run. Entries include the native session ID, attempt, turn, SHA-256 digest, and prompt. New artifacts use append-only records and content-addressed prompt bodies under .system-prompts; version-1 artifacts remain readable. Native Pi JSONL transcripts intentionally omit system prompts.

Run directories use mode 0700, prompt artifacts use 0600, and prompt storage directories use 0700. Treat them as sensitive because they can contain role bodies, project instructions, tool guidance, and extension modifications. Confirmed run deletion removes them; native Pi transcripts remain in Pi session storage.

Workflow lifecycle events

The package exports WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, and WORKFLOW_WORKTREE_CREATED_EVENT, plus matching TypeScript payload types such as WorkflowEventBase and WorkflowRunCompletedEvent.

Subscribe through pi.events. Events are in-process, best-effort, emitted after the corresponding run artifact or state persistence, and ordered within each run. Payloads contain correlation and safe inspection metadata only: prompts, responses, results, tool data, environment values, and workflow source are never included. Manual stop, interruption, pause, awaiting input, and budget exhaustion are state changes, not run completion or failure.

EventFires when
WORKFLOW_RUN_STARTED_EVENTAfter a new run is created and execution begins.
WORKFLOW_RUN_RESUMED_EVENTAfter a run transitions from paused, interrupted, or budget_exhausted to running.
WORKFLOW_RUN_STATE_CHANGED_EVENTAfter every persisted run-state transition.
WORKFLOW_RUN_COMPLETED_EVENTAfter the result is saved and the run enters completed.
WORKFLOW_RUN_FAILED_EVENTAfter a run enters failed; it does not fire for stopped, interrupted, or budget_exhausted.
WORKFLOW_AGENT_STATE_CHANGED_EVENTAfter persisted agent state or attempt-count data changes.
WORKFLOW_PHASE_CHANGED_EVENTAfter the persisted phase changes to a different value.
WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENTAfter a checkpoint is persisted as awaiting, approved, or rejected.
WORKFLOW_BUDGET_EVENTAfter each newly persisted budget event is recorded.
WORKFLOW_WORKTREE_CREATED_EVENTOnce after a new worktree is persisted for an owner.
pi.events.on(WORKFLOW_RUN_COMPLETED_EVENT, (event: WorkflowRunCompletedEvent) =>
  console.log(event.runId, event.resultPath),
);

Operations and debugging

Doctor and inspector

npx -p @piewf/cli piewf doctor [--agent-options <json>] [--prompt <text>] [--json]
npx -p @piewf/cli piewf doctor cleanup [--older-than-days <days>] [--yes]
npx -p @piewf/cli piewf inspect [session-id] [--json|--summary] [--failed]
npx -p @piewf/cli piewf transcript <session-file>
npx -p @piewf/cli piewf run <workflow-name> [workflow arguments]
npx -p @piewf/cli piewf run --script <workflow.js> [--name <workflow-name>] [--input <json>]
npx -p @piewf/cli piewf export <workflow-name> [--name <command>] [--output <path>] [--force]
npx -p @piewf/cli piewf bundle <workflow-name> [--name <command>] [--output <directory>] [--force]
npm run inspect -- [session-id] [--json|--summary] [--failed]

piewf doctor reports native resources, settings, model aliases, and registered functions. Use --agent-options with a JSON object and optional --prompt to inspect a generically prepared agent without a provider call. --json returns the report; warnings do not fail the command. Role-specific flags and positional role/file targets are removed. piewf doctor cleanup remains explicit and dry-run-first. piewf inspect and transcript inspect persisted artifacts.

Doctor JSON includes cwd, agentDir, settingsPath, settings, settingsSources, trust, activeTools, piExtensions, piSkills, functions, modelAliases, resourcePolicy, optional agentInspection, and diagnostics. Inspection includes the resolved model, tools, native resources and prompt, and setup diagnostics. Registered extension validators check settings produced by preparation, just as during execution. There is no roles catalog or roleInspection field.

Consumers should use diagnostic code and the named fields, treat optional fields as absent, and ignore unknown fields so additions remain compatible. There is no schema-version field. settingsSources identifies the source path for represented effective settings. resourcePolicy.globalSettingsPath and projectSettingsPath identify selector files, and top-level resourcePolicy.selectorSources carries only the global and project layers; prepared agent selectors appear under agentInspection.resources.selectorSources.

Portable piewf bundle artifacts use manifest version 2 and bundle the defining extension module plus its import graph into payload/extension.mjs; they no longer reconstruct a workflow from Function.prototype.toString(). Set source: import.meta.url on the registered extension and list third-party package names in dependencies. esbuild is loaded only while creating the bundle, declared packages are inlined, and undeclared packages or unsupported dynamic imports fail before an artifact is written. Node builtins and pi-extensible-workflows remain external; Pi packages (@earendil-works/*) are neither inlined nor external, so importing one fails the bundle: use the pi-extensible-workflows API instead. The manifest records the source module/export, esbuild version, and declared dependencies. The launcher still accepts version 1 bundles. Bundles do not copy Pi, credentials, or the workflow engine. Run <command> setup to verify npm-installed Pi, approve the documented engine install if needed, validate aliases, tools, commands, and environment variables, and persist a local bundle state file before launching.

Workflow consumers use Pi's project trust decision. Pi's automatic trust gate recognizes native .pi resources, not custom role directories alone; defaultProjectTrust: "never" is not a blanket denial when Pi considers a project to have no trust-requiring resources. Use --no-approve to explicitly exclude project resources/settings, or --approve to authorize them. The independent role launcher resolves its own authorization without that native no-resources shortcut.

run accepts flat workflow arguments plus --approve or --no-approve trust overrides. The generated export launcher forwards later arguments to run. A file-backed launch uses --script <workflow.js>; its workflow name defaults to the filename without the final extension and can be overridden with --name. Pass its JSON launch value with --input. Before --, these are launcher options; -- ends launcher option parsing and passes later tokens to workflow input. The headless run command and generated export launchers cannot execute workflows containing checkpoints; use the Pi workflow tool/UI path for checkpointed workflows. The final workflow value remains on stdout and the run ID is reported on stderr as soon as the run is created.

/workflow controls

/workflow

/workflow is the sole workflow slash-command. It opens the picker; select a run to open its dashboard and use the Workflow root for contextual pause, resume, foreground/background, stop, delete, budget, and checkpoint actions. Selecting a phase expands or focuses its details, and selecting an agent opens only that agent's actions. Model aliases remain available from the initial picker. /workflow without UI is read-only; use the workflow tools for mutations. Arguments such as /workflow resume are rejected and direct users back to /workflow. Manual deletion protects dependencies of surviving runs: individual deletion is refused with the dependent run named, while bulk deletion skips protected runs and reports them.

The navigator discovers only runs from the exact current cwd and Pi session. It exposes Model aliases even when no runs exist, groups agents by structural scope, and shows budget status and adjustment controls only when relevant. The run picker keeps unresolved runs in attention order and orders resolved runs newest first by terminal resolution time. The selected-run dashboard presents a selectable Workflow root above the phase tree, with explicit status and agent counts. Pause is cooperative, stop is immediate and irreversible, and delete is available only for terminal runs after confirmation.

Invocation options override settings defaults, and the session-wide active-agent ceiling is 16. Host-facing failures are WorkflowError values with stable codes such as CONFIG_ERROR, INVALID_SYNTAX, UNKNOWN_MODEL, UNKNOWN_TOOL, TOOL_FAILED, AGENT_FAILED, AGENT_RESULT_COLLECTED, AGENT_TIMEOUT, RESULT_INVALID, CANCELLED, WORKTREE_FAILED, BUDGET_EXHAUSTED, and RESUME_INCOMPATIBLE. Fix the reported trust, model, tool, role, budget, settings, or script issue before retrying.

Contributor verification

Start with the repository's code map and completion rules. Commands below run from the repository root; their implementations live in the workspace manifests, not in a separate CI test runner.

npm ci
npm run check
npm run test:packages

Pull requests and main-branch pushes run these same commands on Node.js 24. check builds and type-checks all packages, lints, runs the workspace and template tests, and validates documentation links. test:packages additionally packs and installs all three artifacts in temporary directories, checks imports and entrypoints, exercises the installed CLI and Pi package discovery, and runs npm audit. It requires npm registry access but does not publish anything. Release checks remain in RELEASING.md.

Focused iteration

Build before running compiled tests; otherwise test:run can exercise stale output. TEST_FILES paths are relative to the selected workspace. For example:

npm run build --workspace=packages/core
TEST_FILES='dist/test/starter.test.js' npm run test:run --workspace=packages/core
npm run build --workspace=packages/cli
TEST_FILES='dist/test/doctor.test.js' npm run test:run --workspace=packages/cli

Match additional checks to the affected boundary: npm run acceptance exercises deterministic runtime acceptance; npm run docs:check checks documentation links and markup conventions. Keep a regression that fails before a behavior fix and passes after it. Existing starter tests execute the role-call examples from shipped prompts; do not replace behavioral checks with prose-only assertions.

For cross-repository roles integration, build and pack both products locally, then run PI_OFFLINE=1 node scripts/verify-local-roles-adapter.mjs <local-tarball-directory>. The harness requires core, CLI, and roles tarballs, installs only those local products in a temporary fixture, exercises SDK and CLI sessions against a local synthetic HTTP provider, and removes the fixture. It does not verify paid providers or remote cache hits. PI_OFFLINE=1 node scripts/verify-pre6-parity.mjs <local-tarball-directory> [--old <pre6-tarball-directory>] uses the same local products to check restored pre-6 behavior through the real pi and piewf CLIs, including cold resume from the interactive picker through a pseudo-terminal (Linux script); optional pre-6 tarballs run as controls.

What a green check does not prove

Core test:herdr skips navigator integration unless run inside a real Herdr pane with HERDR_ENV=1. Run npm run test:herdr --workspace=packages/core there after building when changing that UI; do not set the variable merely to bypass the skip. Ordinary CI does not prove live Herdr behavior. Inspect the actual TUI or browser target for UI changes.

Model-backed evaluations are separate, opt-in checks. Deterministic tests do not prove live provider behavior. Run these only with an explicitly approved provider/model and external cost:

npm run evals -- --provider "$PROVIDER" --model "$MODEL" --case direct-answer,parallel
PI_WORKFLOW_EVAL_AMBIENT=1 npm run evals:ambient -- --provider "$PROVIDER" --model "$MODEL"

Before handoff, report the user outcome exercised, exact checks and failures, and any skipped or unverified paths. A reviewer verdict is not execution evidence; optional improvements are not blockers. Do not merge or deploy as part of verification.