Pi Flow · INIT — create a structured workflow (a DAG of producer/verify nodes coordinating through the filesystem) and stand it up to run as a fleet of efficient pi agents (pi.dev / earendil-works/pi) driven by non-Claude coding-plan models, with Claude Code as the single console. The source of truth is a structured workflow TEMPLATE (`.piflow/<wf>/template/`); the `@piflow/core` SDK loads it into a WorkflowSpec and runs it one `pi` per node. INIT triages your starting point — PORT an existing Claude `.js`, IMPORT another engine's workflow (n8n/YAML/JSON), or COMPOSE fresh — then builds the template and the per-repo runner. Use to "create/author a pi-flow workflow", "stand up the runner in a repo", "port my Claude workflow to pi", "import an n8n workflow", "run my workflow on a non-Claude model", "pi-runner". To RUN/monitor an existing workflow use piflow-start; to IMPROVE one use piflow-enhance.
Pi Flow · INIT — create a structured workflow (a DAG of producer/verify nodes coordinating through the filesystem) and stand it up to run as a fleet of efficient pi agents (pi.dev / earendil-works/pi) driven by non-Claude coding-plan models, with Claude Code as the single console. The source of truth is a structured workflow TEMPLATE (`.piflow/<wf>/template/`); the `@piflow/core` SDK loads it into a WorkflowSpec and runs it one `pi` per node. INIT triages your starting point — PORT an existing Claude `.js`, IMPORT another engine's workflow (n8n/YAML/JSON), or COMPOSE fresh — then builds the template and the per-repo runner. Use to "create/author a pi-flow workflow", "stand up the runner in a repo", "port my Claude workflow to pi", "import an n8n workflow", "run my workflow on a non-Claude model", "pi-runner". To RUN/monitor an existing workflow use piflow-start; to IMPROVE one use piflow-enhance.
Pi Flow · init — create & stand up a structured workflow on a pi fleet
One-line model: the structured workflow template (.piflow/<wf>/template/) is the single source of
truth; the @piflow/core SDK loads it into a WorkflowSpec and runs it, one efficient pi process per
node, while Claude Code owns the graph and monitors the run. A Claude Code Workflow .js is an OPTIONAL
one-time INGEST seed — init lifts it into a template, then it is discarded. One authored template: no
codegen, no hand-sync, no drift, no second source.
init ──(PORT a .js ONCE │ IMPORT n8n/YAML │ COMPOSE fresh)──► .piflow/<wf>/template/ ← the SOURCE OF TRUTH
│ @piflow/core: loadTemplate → WorkflowSpec → compile → runWorkflow
▼ one `pi` per node (non-Claude coding-plan model)
.piflow/<wf>/runs/<id>/{ product · .pi/state.json · .pi/nodes/<id>/io.json } ──► `piflowctl logs` ← piflow-start owns this
CREATE a workflow: triage the source → build the template/ → stand up the per-repo runner
full
piflow-enhance
IMPROVE a running workflow: the capture→route→edit→verify loop, the criteria fixture, Companion Mode judging
stub (scope declared)
piflow-start
RUN & monitor a workflow on the pi fleet: dry-run → live → piflowctl logs
stub (scope declared)
Paths below are relative to the piflow repo root (~/Desktop/piflow). This skill lives at
.claude/skills/piflow-init/; references like reference/sdk-consumer.md, docs/design/template-format.md,
templates/pi-runner/, and packages/core are at that repo root.
Step 0 — triage your starting point (do this FIRST)
Getting a workflow into the template has a few distinct conditions, each with its own method. Match
exactly one row, then route. If none matches, STOP and say so — do not hand-roll an unsupported intake;
that is how a silent, wrong workflow gets built.
You have…
Condition
Method
A proven Claude Code Workflow .js (agent()/parallel()/pipeline()/phase())
PORT
references/parse-claude-workflow.md + scripts/parse-claude-workflow.mjs — extractWorkflow runs it under recording stubs and captures the EXACT realized prompts + DAG; you author the rest (tools/mcp/contract-as-data/hooks/refs). ✅ implemented
A workflow in another engine's format (n8n / YAML / JSON)
IMPORT
Map the foreign graph → the template's DAG manifest + per-node defs. ⛔ not yet — do not improvise; stop and flag the missing importer
Only a task/goal, or a skill/agent system described in prose
COMPOSE
The agent REASONS the task into a DAG — START from a blueprint shape (references/blueprints/; the composition grammar is references/blueprints/AUTHORING-GUIDE.md) and stamp/insert it with the scaffold loop (below), each lane bound to a base agent via --agent-type. ✅
Before authoring a lane's prose, run the skill-marketplace ladder — references/skill-marketplace.md. A
node's craft belongs in a SKILL, not its prompt.md, so first SEARCH for one that already carries it
(piflowctl skill list/search --json, then the remote lane), INSTALL it if remote (piflowctl skill add),
and BIND it (add-node --skill <id>) — reach for an existing bundle before hand-writing craft. No match ⇒
compose the node without a skill; NEVER invent a skill id.
The PORT script is the bridge; its 0 exit is the oracle. It ends by compile()ing its own output and
asserting the DAG survived — trust the exit code, not a glance at the JSON. A non-zero exit means the spec is
not trustworthy; fix the cause named in the error, never hand-edit around it.
Mechanical port = the floor, not the finish.references/parse-claude-workflow.md names what the script
CANNOT recover (data-flow reads, hooks, the contract decisions) and how to refine it. Read that before you
ship the template.
PORT = TRANSLATE the source's INTENT into our idioms; never transcribe its STRUCTURE. The source graph
(Claude .js / LangGraph) is a spec of intent, not a shape to mirror — a step that was a graph node THERE is
not automatically a pi node HERE. Understand our functionality FIRST, then map each step to its MOST NATIVE
construct: an agentic step → a pi node; a deterministic step that STAGES a file the model reads → a PRE-hook
seed; a deterministic step that RUNS a fetch or ESTABLISHES shared state → an upstream programmatic node +
promote; a human pause → a checkpoint; a QA/fix loop → rerouteTo/--from; a build/test gate → an op
gate. The #1 port smell — a pi node whose whole prompt is "run X and return its output" — is a pass-through
the source happened to model as a node; it MUST become a hook or a programmatic node, NEVER a pi. You are
re-expressing the workflow in our SDK, not adding nodes.
New conditions are a new row + a new reference + (if programmatic) a new script — never more prose here.
The scaffold loop IS the convenience pathway — author by flags, never by hand-writing node JSON. Stamp any
template (COMPOSE, or extend a PORT) with the CLI: piflowctl new <dir> → per node piflowctl add-node <dir> --id <id> [--dep …] [--tool …] [--artifact …] [--owns …] [--on-fail block] [--agent-type <base>] [--skill <id>] (config is
emitted from flags, re-runnable as a deterministic function of them; the FULL gate/judge/checkpoint/control
surface is flags too — see the gate-authoring bullet below) → Write each nodes/<id>/prompt.md (the
PROSE — the only part that's yours) → piflowctl extract <dir> to TEST (free DAG preview, no model — the real
loadTemplate gate). --agent-type <base> binds a whole base agent (its tools + skill + the OBSERVABLE label,
via the real mergePreset) in one flag; --skill <id> binds ONE resolvable skill by its bare id →
node.jsonprompt.skill (an explicit --skill OVERRIDES the preset's fallback skills[0]; the id resolves
through the skill rings at run time — find/install it first with the marketplace ladder,
references/skill-marketplace.md). This is how a node is scaffolded or customized in a single line —
reach for it before hand-authoring, and extract after every change to confirm the DAG still compiles.
Gates, judges, checkpoints, and control are FLAGS — never hand-edit node.json for them. The WHOLE
per-node gate surface is reachable from add-node; each flag emits the exact schema field the loader honors,
so node.json stays a deterministic function of the flags and piflowctl extract (the loadTemplate gate) is
the oracle. Author by flag; the only prose you Write is prompt.md (and a judge's judge.md):
Checks — --check <kind[:path[:severity[:param]]]> (post lane) · --check-pre … (pre lane) ·
--on-fail/--on-warn block|warn|stop. severity=fail|warn; param=a dotted field, a regex, or a JSON
object ({min,path} for count-floor) — JSON-parsed when it parses. (terse kind/kind:path still works.)
Execution gate — --gate-run <cmd[:args][@cwd]>: a POST shell gate whose non-zero exit BLOCKS the node.
Distinct from --merge-run (a transform.merge data-derive with NO verdict) — use --gate-run for a test/
build/lint gate, --merge-run to generate an asset.
Judge (a DIFFERENT-model verdict, materialized at load into a real <id>__judge node) — Write the rubric
prose to nodes/<id>/judge.md FIRST, then --judge <judgeTier[:threshold]>; the CLI inlines judge.md into
judgeGate.rubric. judgeTier MUST differ from the node's --tier (no self-judging — the CLI rejects a
collision before any write). --judge-on-fail … · --judge-retry-max <n> · --judge-retry-scope feedback|fix.
HITL — --checkpoint <confirm|input|select:prompt> (+ --checkpoint-choice/-default/-headless/-timeout):
the G5 human gate. It is NOT programmatic — it still needs its prompt.md.
Control — --escalate <tier|model> (on failure, retry on a stronger model → io.escalate) ·
--reroute <node[:max]> (on failure, loop back to an upstream node — the target MUST be a strict ANCESTOR).
The per-run shape (designed, partially confident — not a separate "store" to build). There is no
database/registry layer: the TEMPLATE itself (.piflow/<wf>/template/, D8) is the canonical source — authored
once here, customized per workflow. Each run is a DUPLICATION of it: init-RUN (init(${RUN}), D7/D9)
instantiates a thread by materializing ${RUN}/.pi/ and copying each node's per-node configs (prompt.md ·
tools · mcp · node.json) into ${RUN}/.pi/nodes/<id>/, then filling the runtime STATE on top
(state.json channels + the io.json ledger). So a run = a regenerable instance of the template (template
COMMITTED; runs/<id>/ GITIGNORED). This is the canon's two inits distinction — init-TEMPLATE (authoring,
THIS skill) vs init-RUN (runtime instantiation). The shape is specified in
docs/design/sdk-canonical-build-plan.md (D7 per-run .pi/ layout · D8 source-of-truth · D9 .piflow/
namespaces) + docs/design/template-format.md §10, and partially landed (U6a); the template loader +
init-RUN (U6b–U8) are the remaining build, with open naming nits (.pi/ vs _meta/). INIT today is PORT +
COMPOSE (the scaffold loop + blueprints); IMPORT is still deferred. Do not finalize the template format or
those open per-run items here.
Standing up the project (after the template exists)
The engine is the @piflow/core package; a project does NOT copy an engine, it installs the package and
drops in a thin consumer. The canonical per-project layout, the file→mission map, and the full adopt steps
are in reference/sdk-consumer.md — read it first. The flow:
Confirm the source of truth — the template. The workflow lives as a structured template at
.piflow/<wf>/template/ (DAG manifest + per-node defs as data + refs — see docs/design/template-format.md),
built by Step 0's triage. You edit the TEMPLATE; there is no maintained .js, no second source, no copy.
(Today's PI_RUNNER_WORKFLOW still points at a .js pending the loader — the migration target is the
template dir; see docs/design/sdk-canonical-build-plan.md D8.)
Set the credential ONCE in pi's global config (per machine, not per repo). The model + key live in
pi's native ~/.pi/agent/models.json, which pi resolves for EVERY project — so no product needs its own
key or .env credential. cp templates/models.json.example ~/.pi/agent/models.json, edit
apiKey/baseUrl/model ids, chmod 600, verify pi --list-models cp. See reference/provider-and-headless.md.
Install @piflow/core + drop in templates/pi-runner/. Copy the folder next to .claude/, set the
@piflow/core dependency in pi-runner/package.json (a file: path to this repo's packages/core, a
workspace dep, or the published package), and npm install. The sdk/ glue + hooks/ op engine are the
byte-identical generic consumer — you edit none of them; per-repo specifics live in .env +
package.json + your hooks/. See reference/sdk-consumer.md.
Configure the per-repo wiring .env (wiring only, no secret). cp .env.example pi-runner/.env; set
PI_RUNNER_WORKFLOW (the .js path, repo-root relative), PI_RUNNER_ROOT if pi-runner/'s parent isn't
the repo root, and PI_RUNNER_PROVIDER/PI_RUNNER_MODEL (or leave the provider default). Optional:
PI_RUNNER_NODE_TIMEOUT, PI_RUNNER_STALL_TIMEOUT, PI_RUNNER_THINKING, PI_RUNNER_CONTRACT_EXT.
Author / keep your hooks/. The shipped generic op engine covers the standard
DRIVER-SEED/PROJECT/MERGE/SEED-CONTRACT families; a new deterministic op is one
hooks/<op>.mjs (parser + executor) + a binding in sdk/hook-bindings.mjs. The full pre/post
hook-assembly contract (DRIVER-* marker → @piflow/coreHook → hooks/ executor → runHooks) is in
reference/sdk-consumer.md; the marker grammar in reference/artifact-contract.md.
Dry-run to confirm the DAG (free, no model).init ends here — the first real run + monitoring is
piflow-start's job.
The dry-run prints the stage/node count, each node's [tools: …] [hooks: …], the resolved pi command,
and a ⚠ TOOL BINDING on any un-tokenized allow/deny entry. Pass args with --arg k=v (repeatable) /
--arg-file k=path; --until/--from/--only window a long pipeline. See reference/cli.md. You run
every command; the user runs nothing.
Opt-in hardening (each is one switch + its reference — read before arming):
Output Contract — wrap each producing node with contract({ artifacts, owns, readScope })
(templates/workflow-snippets/contract.js) so the driver verifies REQUIRED artifacts independent of the
self-report. The read-scope is authored at the SAME time as the writes. reference/artifact-contract.md.
Write isolation for fleets — WorktreeSandboxProvider (--worktree): each run in its own git
worktree. reference/worktree-isolation.md.
OS read-scope — SeatbeltSandboxProvider (--sandbox, macOS): a node's readScope becomes a
kernel-enforced deny-all-reads-except. reference/read-scope-sandbox.md.
Escalation gate — on a verified failure, consult a stronger cross-family model once
(reference/escalation.md; engine-baked in the monolith, consumer-side + deferred under the SDK).
The criteria fixture — standing up a workflow seeds <repo>/.agents/skill-system-criteria.md, the
per-node human-judged quality bar (NEVER injected into a prompt). Maintained by hermes-skill-system (the
improve loop — see piflow-enhance).
Claude Code executor (per-node, OPTIONAL) — a node may run on a headless local Claude Code session
instead of a pi: author --executor claude-code on piflowctl add-node (or "executor":"claude-code" in
node.json), and map its Claude-side tier models with piflowctl model set <tier> <id> --claude (aliases
opus|sonnet|haiku or full claude-* ids). The credential is a ONE-TIME, freely-SKIPPABLE step:
piflowctl claude-code connect persists a claude setup-token to ~/.piflow/claude-code.json (chmod 600).
Skip it on macOS — an existing claude login (keychain) is used automatically; the file is the portable
layer for Linux/cloud. The runner resolves the token host-side (env → that file → local login) and strips
ANTHROPIC_API_KEY so a stray key never silently bills the API. Absent on a node ⇒ pi, byte-identical to
today. docs/design/agent-executor-interface.md.
The laws (do not violate)
Single source of truth = the structured template. Improve a wave by editing its node def / its skill in
the template (.piflow/<wf>/template/); the engine loads the change automatically. The Claude .js is an
init-only INGEST seed, discarded after — NEVER a maintained second source. Zero hand-sync.
The engine is the @piflow/core package, not a per-repo copy. The generic consumer glue
(sdk/, hooks/, extract.mjs, logs.mjs) stays byte-identical across repos — 100% of per-repo
specifics live in .env + package.json (wiring) + your hooks/ (your ops), and the credential lives
once in pi's global ~/.pi/agent/models.json. An engine fix is a package bump, not a manual merge; a
glue fix is a one-file copy. If you find yourself editing the consumer glue for one repo, you're
re-introducing the drift this whole pattern exists to prevent — push it into .env/package.json/hooks/
instead. (The Tier-2 glue still ships in the template only because @piflow/core has named gaps; it is
slated to graduate into the package — see reference/sdk-consumer.md.)
Ingest once, then author — not codegen, not a live bridge. To PORT an existing Claude .js,
extractWorkflow runs it under recording stubs and captures the exact prompts + DAG ONCE; init maps
that into the template, and the .js is discarded. Thereafter the engine loads the TEMPLATE directly
(loadTemplate → WorkflowSpec). There is no two-way bridge and no Claude-Workflow execution target — pi + the
human + the verify harness are the proving ground.
Driver owns the graph; pi owns the node. Plain code decides stage order + parallel lanes +
halt-on-failure; the model never decides control flow. Nodes coordinate via the filesystem.
The workflow orchestrates; the SKILL carries the craft — never duplicate craft into a node body.
When a node loads a skill (SKILL TO LOAD AND FOLLOW: …), split content by OWNER. The workflow .js
holds ORCHESTRATION ONLY: the node sequence + parallel lanes (the DAG), each node's I/O contract
(contract({artifacts,owns,readScope}) + the return schema), and a THIN wiring body — who the node is ·
which input artifacts it reads · which output it writes · the load-and-follow pointer. The CRAFT — how to
do the work: the method, the bar, the build path, the domain detail — lives ONLY in the skill, its single
canonical home. A node body that RESTATES the skill's craft is two ground truths: every craft edit then needs
two edits and they drift (the dual-maintenance trap). So put all craft in the skill, keep the body a pointer,
and improve a wave by editing its SKILL / improve the chain by editing the workflow. Extraction is
unchanged — the realized prompt is still the (thin) body, and the model reads the loaded skill at runtime;
only WHERE the craft text lives collapses to one home. (Inline-prompt nodes with no backing skill are exempt —
there the body IS the only home; this law governs the skill-backed pattern.)
Verified against the declared contract, not the self-report. Each node ends with one fenced
json block; the driver stat()s every outputArtifact. But the self-reported list is
honest only when the model is — so a node may also declare, in its prompt, the files it is
required to leave on disk (DRIVER-ARTIFACTS) and the only paths it may write
(DRIVER-OWNS). The driver verifies the required set independent of the self-report: a clean
exit that did not produce a required artifact is blocked, not ok. See The Output Contract
and reference/artifact-contract.md.
Every producing node declares an Output Contract — { artifacts, owns, readScope }.
Requirements live in a skill description, I/O in ## Inputs/## Output prose, the RETURN shape in
schema — but Claude validates the message, never the filesystem. The artifact layer is yours:
declare it once as the contract { artifacts, owns, readScope } fields in each node def; the engine
renders the Definition-of-Done prose AND the DRIVER-ARTIFACTS/DRIVER-OWNS/DRIVER-READ-SCOPE
markers from them (the generic codec in @piflow/core parses them back). The write-contract (artifacts/owns)
and the read-scope (readScope) are the SAME tier — both authored at node creation time, never an
afterthought. Run the fleet under SeatbeltSandboxProvider so readScope is OS-enforced (inert otherwise).
Full spec: reference/artifact-contract.md; read-scope syntax: reference/read-scope-sandbox.md.
A workflow ships with its criteria fixture. Standing up a workflow creates
<repo>/.agents/skill-system-criteria.md — the per-node, human-judged QUALITY bar (sibling of the
skill-system map, complement to the mechanical Output Contract: the contract checks the artifact
exists, the criteria say whether it is good). It is the standard runs are judged against to
converge on quality, and the improvement target sharpened each run. It is never injected into a
node's prompt (that would teach-to-the-test and void the clean-room signal), and the
hermes-skill-system loop (piflow-enhance) maintains it.
Headless invariants are non-negotiable. Close stdin, --offline, --no-extensions (the cp
provider comes from pi's core models.json, which --no-extensions does NOT disable), capture each node's
event stream (recordEvents, on by default) so a silent headless hang is visible. A silent hang is
otherwise invisible — this cost a real ~10-minute mystery stall, and a multi-session "never-write" mystery
(reference/observability.md case study).
Physical isolation for fleets is one provider, not a fork.WorktreeSandboxProvider runs each run in
its own git worktree — concurrent runs cannot see each other. Its only cost, merge-back, is erased by
auto-discovered registration (units register by exporting a descriptor from their own file, never by
hand-editing a shared list). See reference/worktree-isolation.md.
Prompt rules are unenforceable on weak models — put the boundary in the OS. Worktree isolates
WRITES; SeatbeltSandboxProvider (macOS, opt-in) isolates READS: every producing node declares a
readScope in its contract(), and under the sandbox that DRIVER-READ-SCOPE becomes a kernel-enforced
deny-all-reads-except-{toolchain ∪ scope}, inherited by every child process, so a grep / or a
sibling-source spelunk EPERMs instead of bloating context. A node left un-scoped is a hole. See
reference/read-scope-sandbox.md.
A node WIRES its own tools in tools.allow/deny, and as of G11 a catalog tool BINDS on the canonical
path — so author the address, don't fear it. Four address families (tools.allow entries, template
node.jsontools:{allow,deny} — template/types.ts:26): fs:*/sh:* (pi builtins), oc.<plugin>:<tool>
(the OpenClaw sdk catalog, e.g. the live-proven pure oc.calc:add → tool_execution_end{calc_add, sum:5}),
mcp.<server>:<tool> (a per-node MCP server), and contract:submit_result (the typed terminating return
tool, tools/contract-tool.ts:20). These now bind because assembleRunTools (runner/tool-config.ts)
seeds the run registry (builtins + the oc.calc:add seed + the community catalog + submit_result) into
BOTH runFromConfig and runFromTemplate (runner/entry.ts:34-37,132-142) under an explicit-caller-wins
guard — before G11 a selected oc.*/mcp.* had ZERO canonical caller and the node went blocked. The bind
PRE-CHECK runs LOUD and EARLY: verifyToolBinding (runner.ts:771-778, before pi spawns) marks the node
blocked if a declared address is absent from the catalog or two addresses collide on one bare name — so a
selected oc.*/mcp.* MUST exist in the catalog; the dry-run's ⚠ TOOL BINDING audit (line 112 above)
surfaces it for free. Per-node MCP creds: declare mcp.servers in the node.json (the loader reads
def.mcp → NodeIntent.mcp, template/loader.ts:165), and every secret-bearing value MUST be a
$VAR/${VAR} env REFERENCE — the loader REJECTS a committed literal (checkMcpSecrets,
template/checks.ts:318); the runner forwards ONLY the referenced $VARs through the SecretResolver
allowlist, never the full env (on cloud, only the allowlist crosses — runner.ts:447-478). Param s
render Gemini-safe automatically (, / #21) — authoring is
unchanged. CAVEAT: an tool with a pure native execute runs end-to-end TODAY; a tool routed
through the bridge still needs the →value EXPANSION, a follow-on — . NODE-ACTION FORMAT: is the
canonical node-action format (a unified envelope) and it is : the
field is in the node schema and the loader lowers it (/ → /,
). //// are the ergonomic ALIASES the loader
lowers INTO , so authoring EITHER the aliases or directly is fine — they converge on the same
canonical envelope.
Hand-roll the orchestration; reach for pi-native only at the interpretation surfaces. The
driver's own deterministic plumbing (the DAG, filesystem coordination, artifact stat(), worktree)
is YOURS — pi is minimal by design (no sub-agents, no native typed-return) and expects you to own
it. Reach for a pi-native mechanism ONLY where the driver must INTERPRET the non-Claude model's
free-form output — that is where harness fragility concentrates (the return-block parser was the
single most-patched surface). pi's purpose-built seams there: submit_result (typed return) and the
tool_call block (in-loop owned-paths) — both opt-in via the extensions/node-contract.ts-e
(PI_RUNNER_CONTRACT_EXT), both keep the driver fallback so they never break a run.
Every run records its behavior — state + behavior, two files, one join. A run writes
run-status.json (per-node status/exit/timing/artifacts — the verdict) and _pi/<id>.events.jsonl
(the slimmed pi --mode json stream — what the model did), joined on node id. piflowctl logs reads both
(-f live · --summary diagnosis · --node). It is pure reconstruction over data already on disk — no new
per-run field. This is the SDK successor to the legacy pi-tui registry survey. See reference/observability.md.
A verify node verifies; it never CREATES a key artifact. Separate the roles: a PRODUCING node authors
each key artifact the flow binds to; a VERIFY node judges that artifact and may run a bounded inner self-fix
to stabilize it — but it is NEVER the primary creator. The test: remove the verify node entirely and the
producing flow must still yield every key artifact. A verify node that ALSO produces the load-bearing
artifact is the conflation this law forbids — it makes the node un-removable, re-introduces "the student
grades its own homework," and breaks the mode toggle below. Split it: a producer makes the artifact, the
verifier judges it.
Four re-attempt axes — never conflate them (above all, never call reroute "retry"). Four distinct
mechanisms re-touch work; each has an OBSERVABLE discriminator. VERIFY = a gate node judges an
already-produced PRODUCT artifact and writes a pass/fail verdict — it re-runs nothing and creates nothing
load-bearing (its VALIDATION_FAILED is a value INSIDE the written JSON; the node run is still structurally
ok). RETRY (runner/retry.tsrunNodeWithRetries) = the runner's mechanical bounded re-run of the
SAME node at the same DAG position because THAT node's own execution failed (error/blocked), with an
optional single model escalation — a runtime loop around one node, blind to any verify verdict. REROUTE
(rerouteTo → expandReroute) = a COMPILE-TIME DAG unroll that a verify node's on-failure action triggers
to re-invoke an ANCESTOR PRODUCER (a bounded self-fix QA loop), gated by structural artifact presence — a
different code path from RETRY. SELF-OPTIMIZATION (piflowctl optimize) = an out-of-band, human-gated,
cross-run edit of the node/template itself — it improves the NODE, never recovers a single run. The rule:
judges-the-product-and-writes-a-verdict → VERIFY; re-runs the SAME node because IT broke → RETRY; a verify
failure re-runs an EARLIER producer → REROUTE; changes the template between runs → OPTIMIZE.
An output edit is not done until its CONSUMERS are reconciled — keep a node I/O map. A node's output
artifact is an INTERFACE other nodes read. Change what a node writes — its format, shape, filename, or
fields — and you silently break every downstream node still reading the old shape (moving a design doc from
gdd.json to gdd.md orphans every node that opened gdd.json). So every node-output edit has a mandatory
second half: find every consumer of that artifact and reconcile it (re-point the read, update the parse,
migrate the field), then verify a grep for the old shape returns only history. The contract() says what a
node WRITES; the node I/O map (see Designing a node's I/O below) says who READS it. Every
node/subagent edit checks in there FIRST.
Run profiles (product-declared modes) — let the USER choose how many
A workflow can declare named run PROFILES in its template meta.json — each a GENERIC node-ELISION
predicate the SDK applies before compile (docs/design/profiles-and-resume-robustness.md). A profile that
elides the verify/gate phase yields a producing-only DAG (the dev-time "companion" posture, where the
orchestrator IS the verifier); the default runs everything (the unattended "production" posture). The names
are the PRODUCT's vocabulary, declared as DATA — @piflow/core carries none of it (it knows only "elide the
nodes this predicate matches, rewiring deps so the survivor graph is gateless").
Authoring a workflow that has gate/verify nodes? PRESENT THE USER A CHOICE of how many profiles to
declare — and ONE is a fine, common answer. Don't force a pair. Offer, Claude-Code style: (1) just one
default profile [recommended if unsure]; (2) add a dev profile that elides the gate nodes (faster babysat
runs); (3) more, you name them. Declare the chosen set as profiles + defaultProfile in meta.json:
A profile selects nodes to elide by their generic phase tag — so give the gate nodes a shared phase.
Selecting one at run time is --profile <name> (piflow-start owns that).
A profile may only elide nodes that CREATE NOTHING — that IS the verify-node law above. Drop the gates
and every key artifact still exists, because PRODUCERS made them; that is precisely why the gate phase is
safely elidable. A verify node that is also a producer can't be elided — split it FIRST.
When the gates are elided, the orchestrator IS the verifier. Run in the background; the moment a node
goes ok, judge its artifact against (a) the GOLD sample and (b) its criteria-fixture entry — the dual
reference the hermes-skill-system node-validation loop uses (criteria stay a JUDGING reference, NEVER
injected). You cover every surface the elided gates would have.
On a heavy mistake, stop — don't pour effort onto a bad upstream artifact. Fix at the canonical owner
(Hermes), rerun the SUFFIX from the first changed node (--from), reuse unchanged upstream. Borderline →
surface to the human (the eye). Promote a fresh artifact over the gold when it's better.
A dev-time POSTURE on a product-declared profile — no engine branch, no per-workflow mode code. Pairs with
the criteria fixture and hermes-skill-system's node-validation loop.
Designing a node's I/O — the standards + the I/O map
Designing a workflow IS setting each node's input/output standards — this is the most meaningful place to fix
them, and where an edit must be reconciled against the rest. For every node:
One node, one task, so its I/O is a clean boundary; split a two-job node. A producer creates each key
artifact; a verifier never does (the verify-node law above).
Format the output for its CONSUMER, not by default. Strict typed JSON ONLY at a machine boundary (a
parser / schema / the driver). PROSE/Markdown for an LLM-reasoning hand-off (a middle product the next
model THINKS over): reason-in-prose, structure LAST — strict JSON on a reasoning hand-off taxes reasoning
~5–15% and ~35% tokens, worst on non-Claude models, and inverts CoT when a decision field precedes its rationale.
Push the schema boundary as LATE as possible; keep a small fenced-JSON tail only for the fields a parser
reads. Merge/denormalize for a single downstream reasoner; split only for parallel agents. (Full
prompt-craft + citations: agentic-prompt-design §5.)
Declare two things at node-creation time: the contract({ artifacts, owns, readScope }) (what it WRITES
its read surface) AND the artifact's CONSUMERS in the I/O map.
Split MECHANICAL from INTELLIGENT, then push the mechanical into a DRIVER HOOK — don't leave it to the model.
List the node's steps; for each ask: "is this output a fixed function of already-frozen on-disk inputs, with no
judgment?"YES → a deterministic driver hook (a Hook in hooks/, run by runHooks, not the model) — so
it becomes a TESTED CODE PATH, not a per-run gamble. Two verbs, one family: a PRE-hook (DRIVER-SEED) STAGES
inputs before the model (copy a skeleton/tree → FILL-don't-COMPOSE); a POST-hook
(DRIVER-PROJECT/DRIVER-MERGE) DERIVES/validates outputs after the model (project a frozen spec → its
runtime data file, merge fragments, schema-check). NO (design reasoning / open-ended coding / prose
authoring / diagnose-and-fix) → the model. Declare the deterministic part as DATA in the registry, so the
engine stays uniform + genre-agnostic. Why: it removes the non-Claude-model explore-forever / mis-project
thrash surface, makes mechanical output un-hallucinatable, and cuts tokens. The hook envelope + the
DRIVER-marker → Hook assembly: reference/sdk-consumer.md; the marker spec: reference/artifact-contract.md.
Marker-grammar law: skeleton guide prose must be quote-safe for edit anchors — never a bare single-word
bracket token (<id>, <threat>) in a placeholder, since some model gateways mangle it inside a tool-call
argument string as XML; the colon-bearing <FILL: sentinel is the proven-safe convention. Canonical home:
reference/artifact-contract.md (the marker grammar).
A WHOLLY-mechanical step is a PROGRAMMATIC node — never an agent node — and DEFAULTS to a neighbour's
PRE-hook, not its own node. The bullet above splits a mixed node's mechanical parts into hooks around its
model; when a step has NO intelligent part at all (a render, a deterministic merge/derive of two parents, a
final pack-and-ship) it must spawn no pi. Author it as programmatic: true — no prompt, no tools,
no return; its hooks/op (run/merge/project) + checks ARE the node, run + gated by the engine
(@piflow/core dispatches it to runProgrammatic, the no-pi twin of checkpoint/rerouteGate). Whether
it becomes its own node is MECHANICAL, not a complexity judgment. A template PRE-hook can do exactly ONE
thing — STAGE a file the consuming model then reads (DRIVER-SEED → {when:'pre', transform:seed}); it
CANNOT run a shell command and CANNOT write shared state (run + promote are POST-only — op-dispatch
REJECTS a when:'pre' run — and a pre-hook fires at node-lifecycle.ts:452, AFTER the host node's own paths
(:198) and prompt (:363) already resolved). So the default — fold the mechanical work into the
consuming agent node's PRE-hook, reuse, zero new node — holds ONLY for pure file-staging. Promote it to its
OWN upstream programmatic node when ANY holds: (1) no consuming agent node is guaranteed to run on every
run — its would-be host is elided by a shipped profile (companion drops the verify phase) or a bounded
--until, so some runs END on this producing step and nothing is left to carry the pre-hook; (2) the step
needs its own node-scoped rerouteTo an ancestor (reroute is node-granular — a pre-hook op cannot carry
it); or (3) the step RUNS a shell command or PROMOTES a value into shared state (e.g. a DB/id resolve
that establishes {{state.slug}} for every downstream path) — a pre-hook can do neither, and a promoted value
reaches a consumer ONLY via a completed upstream node's POST promote merged at the stage barrier, so this
whole class is ALWAYS its own UPSTREAM programmatic node, never a pre-hook. A programmatic
node is a TERMINAL producer under (1)/(2) but an UPSTREAM establisher under (3). The lesson render is the canonical (1): it must
run whether or not the verify wave does, so it is its own render node joining composer ∥ sketch —
never the verifier's pre-hook. the step's full control logic (blocking gate · retry · reroute)
must read identically wherever it lives — a pre-hook is not a licence to carry weaker logic than a node would.
Design for parallelism from the I/O up — the map is where independent lanes become visible. As you set
each node's I/O, look for nodes whose inputs are ALL already-frozen upstream artifacts and whose owns set is
DISJOINT from a sibling's — those are independent lanes that need not wait on each other, so run them as a
parallel([laneA, laneB]) for wall-clock. The correctness rule is write-disjoint owns: two concurrent
lanes must share NO writable file. If a "dependency" is only that lane B reads an artifact lane A also
produces, check whether B can read the SAME upstream source instead (re-pointing the read dissolves the
artificial edge). When two lanes would BOTH touch one shared file, don't serialize the whole lane — split that
file: give each lane its own per-node fragment and add a tiny SERIAL JOIN node after the parallel stage to
merge them deterministically. (The script has no fs at eval time, so the join is a NODE, never raw fs in the workflow.)
Agent-type presets (G6) — a node may START from a branded preset, then customize above it. When an
author assigns a node agentType: <id> (e.g. market-research), EXPAND it at author time. The scaffolder
does this for you:piflowctl add-node --agent-type <id> folds the preset's tools + skill + the OBSERVABLE
agentType label into node.json via the real mergePreset (the GUI then renders the base agent the node
INHERITS from). The role-prompt is inherited BY REFERENCE at render — keyed off agentType,
single-sourced from ~/.piflow/agents/<id>.md (role first, the node's TASK after, applied exactly once by
renderRealizedPrompt) — so you Write into prompt.md ONLY the node's TASK, NEVER the role body; the
scaffolder still never touches prose. To bind BY HAND instead — do not treat the name as magic: add the
preset's base tools to the node and set agentType: <id> (the role-prompt then comes free at render; the
GUI renders the base agent's icon via observe), and choose the node's model/tier yourself — a preset NEVER
sets a model. Unknown <id> ⇒ HALT, never invent one.
Presets are an optional convenience, not a lock-in — skipping them and wiring tools/prompt/model by
hand is the common path. Full contract + the seed presets + how to author a new one:references/agent-presets/README.md. On init, materialize any missing seed into ~/.piflow/agents/
(create-if-absent — never overwrite a user-edited preset).
The node I/O map — <repo>/.agents/skill-system-io-map.md, the THIRD standing artifact beside the
skill-system map (composition) and the criteria fixture (quality). It is the producer→consumer ledger keyed by
ARTIFACT: for each on-disk artifact, which node PRODUCES it, which nodes CONSUME it, and HOW (strict parse vs
LLM read — because that is a format change's blast radius). Derive it once from the node read-lines; update it
on the SAME trigger as the map. Before changing a node's output, read the artifact's consumer row and reconcile
every one; after, verify no consumer reads the stale shape.
Files in the piflow repo (paths relative to the repo root, ~/Desktop/piflow)
reference/sdk-consumer.md — READ FIRST to stand up a project. The canonical per-project layout (the
three tiers + the file→mission map), the adopt steps, and the pre/post hook-assembly contract.
reference/observability.md — piflowctl logs (docker-logs for a run): the run-status + event-archive
contract, the CLI (-f/--summary/--node/--raw), the pre-run tool audit, the RunOptions knobs, and
the failure-signature table. Read (with piflow-start) when a run misbehaves.
reference/cli.md — the run flags + the --from/--until/--only node-range model + the .env knobs.
Applies to sdk/run.mjs (some monolith-only flags are legacy). Read so node ranges are never guessed.
reference/architecture.md — why the workflow runs unchanged: the invariants + the one dynamic-workflow
caveat. Read to understand the pattern.
reference/artifact-contract.md — the Output Contract + the DRIVER-* marker grammar (the authoring
surface for hooks + the write/read contract). Read to make a node deliver the right artifact + to author a hook.
reference/escalation.md — the escalation gate (engine-baked in the monolith; consumer-side + DEFERRED
under the SDK). Read before arming escalation.
reference/orchestration.md — Claude-Code-as-console: dry-run → background live → poll. Read to operate a run (piflow-start).
reference/worktree-isolation.md — WorktreeSandboxProvider physical write-isolation for fleets. Read before a --worktree fleet.
reference/read-scope-sandbox.md — SeatbeltSandboxProvider OS read-scope (macOS): the DRIVER-READ-SCOPE
marker + the full-runtime-surface grants. Read before --sandbox.
reference/provider-and-headless.md — the native ~/.pi/agent/models.json credential + the headless
invariants/watchdog. Read for setup + when a node hangs.
docs/design/template-format.md — the template-format keystone (the D8 source of truth): the
contract-as-data node def, the DAG manifest, the loader, the {{ }} token vocabulary, the one-time .js ingest.
docs/design/sdk-canonical-build-plan.md — D1–D9 + the U-unit table (the runtime build status).
references/parse-claude-workflow.md + scripts/parse-claude-workflow.mjs — the PORT condition (Step 0):
the .js → template bridge + what it cannot recover. (These live inside this skill.)
references/blueprints/ — the COMPOSE starting shapes (Step 0): the canonical AUTHORING-GUIDE.md
(the DAG-composition grammar — slot model · stamp/insert-anywhere/hand-add · the {{RUN}}/{{WORKSPACE}}
token file-transfer rules) + the parametric topology recipes (README.md catalog: research-synthesize-author
· produce-verify-fix · fan-out-map-reduce · spec-fanout-build · candidate-fusion-refine), each lane
bound to a base agent via --agent-type; the graph-level sibling of references/agent-presets/. This same
guide drives the design-next (long-horizon redesign) and improve-prev (optimizer) loops. (Inside this skill.)
templates/pi-runner/ — copy this into a repo: the SDK consumer.sdk/ (the thin glue) + hooks/
(the deterministic op engine) + extract.mjs + logs.mjs + extensions/ + package.json + .env.example.
Generic + byte-identical; only .env/package.json/hooks/ are yours. See reference/sdk-consumer.md.
templates/legacy/ — the archived pre-SDK monolith (the 153 KB run.mjs + its bespoke monitors /
sandbox profile / provider ext): a parity bridge with a successor-map README. Do not build on it.
templates/models.json.example — copy to ~/.pi/agent/models.json (once per machine): the provider +
credential pi resolves natively for every project.
templates/workflow-snippets/contract.js — the contract() helper to paste into your workflow .js (the
only per-workflow edit to adopt the Output Contract).
templates/examples/auto-discover-registry.example.mjs — adapt-me generator for auto-discovered
registration (the worktree merge-back enabler — stop hand-editing a shared registration list).
packages/core (@piflow/core) — the engine, installed not copied:runWorkflow/compile, the
contract codec, runHooks, the sandbox providers, the tool registry, and the piflowctl bin + observability.
An engine fix is a package bump. (This repo IS the @piflow/core product repo; docs/ holds its
design canon — see docs/INDEX.md.)
Reference implementation
The worked instance is game-omni: its .claude/workflows/game-omni-v1.6.js is the INGEST SEED being
ported into a .piflow/game-omni/template/ (the extractWorkflow → init path); pi-runner/sdk/ runs
@piflow/core on the in-place LocalSandboxProvider with hooks/ bound in. The original battle-tested
monolith lives in the animation-test repo (lesson-build → 14 nodes / 10 stages, matching
templates/legacy/run.mjs) — the pre-SDK reference. When in doubt, those repos are the worked examples.
المزيد من هذا المستودع
Topology — --fusion <moa|best-of-n> (+ --fusion-n/-panel/-judge/-obligations/-no-verify: panel/
best-of-n + judge expansion) · --subworkflow <ref> (inline a sub-template as a sub-DAG in place of the node).
Contract — --full-access (per-node jail-off, LOCAL only) · --fill-sentinel <s> · --artifact-schema <p>
(per-ARTIFACT output validation → contract.schema; NOT the structured-RETURN handshake) ·
--return-mode required (the zero-artifact gate-node idiom). These ride INSIDE contract.