Skip to the content.

Envelopes

When a stage finishes, it hands off structured state to the pipeline via an envelope. The next stage (and the operator console) read this contract instead of scraping chat transcripts.

Envelopes are domain-neutral — you choose what goes in summary, payload, and artifacts for your workflow.

Contract

Type definition (src/types/envelope.ts):

type StageEnvelope = {
  status: "success" | "failure";
  summary: string;
  artifacts: string[];
  payload?: Record<string, unknown>;
  fork_choice?: string[];
  clone_forks?: CloneForkItem[];
  feedback_loop?: FeedbackLoopAction;
  stage_id?: string;
  notes?: string;
};

type FeedbackLoopAction =
  | { action: "continue" }
  | { action: "send_back"; target: string };
Field Required Description
status yes "success" advances the pipeline. "failure" on a named stage skips successors whose needs do not accept failed (legacy scalar needs accepts succeeded only). A generic fan-in join that lists failed in that parent's on set continues. A parallel clone failure lets sibling clones finish and skips the clone-list join and its descendants
summary yes Non-empty human-readable summary
artifacts yes Array of run-relative artifact paths (may be empty [])
payload no Structured data for downstream stages; required on success when the stage declares payload_schema
fork_choice no* Non-clonable immediate successor ids to run; required on success when the stage has a fork field and at least one non-clonable child
clone_forks no* Clone actions for clonable successors; required on success when any immediate successor is clonable; illegal items are rejected by emit
feedback_loop no† Continue or send-back decision; required on success when the stage declares feedback_loop policy
stage_id no Optional stage id echo
notes no Optional free-form notes

* Required for fork stages on success (fork_choice) and when any immediate successor is clonable (clone_forks). On failure, neither field is required or validated. Extra clone_forks is ignored only when the emitting stage has no clonable children; if any clonable child exists, clone_forks must cover every clonable successor exactly once (extra successor_ids are rejected).

† Required on success for stages with a configured feedback_loop policy. Forbidden on failure and on stages without that policy. See Feedback loops.

Emitting an envelope

Stage agents call the Pi tool emit_stage_envelope exactly once when finished. The pipeline cannot advance until this succeeds.

Example success emit (conceptual):

{
  "status": "success",
  "summary": "Clarified requirements and listed three open questions.",
  "artifacts": [],
  "payload": {
    "requirements": ["…"],
    "open_questions": ["…"]
  }
}

On status: "failure", the envelope is accepted. A named-stage failure skips paths whose dependency contract rejects failed. Independent siblings and generic fan-in joins that list failed in that parent's on set continue. A parallel clone failure does not stop sibling clones; the clone-list join successor and its descendants are skipped. Sequential clone failure skips remaining clones of that successor and the clone-list join. Neither fork_choice nor clone_forks is required or validated on failure.

If the stage declares payload_schema in YAML, payload is validated against that JSON Schema subset on success — see payload_schema.

Fork stages

If the stage's pipeline entry has a fork field and at least one non-clonable child, the success emit must include fork_choice: string[] naming which of those successors to run. A fork parent whose every child is clonable does not require fork_choice. Absent or illegal choices cause the emit to be rejected (isError: true); the stage fails when no valid emit follows before the session ends.

{
  "status": "success",
  "summary": "Chose design-doc branch.",
  "artifacts": [],
  "fork_choice": ["design-doc"]
}

Rules:

Unchosen successors are skipped — the same status used when a parent fails. See YAML catalog for the fork field.

Clonable successors {#clonable-successors}

If any immediate successor is clonable: true, the success emit must include clone_forks. Tokens are skip | once | fanout unless the parent declares clone_actions (a non-empty subset). Omit clone_actions to keep all three. once is not fan-out of 1; fanout N is 2 through clone_cap. See YAML catalog.

The user prompt and emit tool both name the legal successor ids, clone caps, allowed actions, and each successor's assignment schema. successor_id is an enum of those ids. Invented ids, an empty clone_forks list, a disallowed action, or an assignment payload that fails clone_input_schema stay in-session (isError, no terminate).

Item shape (exact coverage of every clonable successor):

action Required Forbidden
skip successor_id, action envelope, mode, clones
once successor_id, action, envelope mode, clones
fanout successor_id, action, mode, clones (length in [2, clone_cap]) top-level envelope

Nested clone_forks[i].envelope (for once) and clones[j].envelope (for fanout) are full StageEnvelope objects: they require status, summary, and artifacts, and may include payload. After the parent emits, that nested envelope becomes the clone child's prior envelope (the child reads it like any predecessor).

{
  "status": "success",
  "summary": "Fan-out author-diagrams.",
  "artifacts": [],
  "clone_forks": [
    {
      "successor_id": "author-diagrams",
      "action": "fanout",
      "mode": "parallel",
      "clones": [
        { "envelope": { "status": "success", "summary": "clone 1", "artifacts": [] } },
        { "envelope": { "status": "success", "summary": "clone 2", "artifacts": [] } }
      ]
    }
  ]
}

Illegal items are rejected by emit. A successor may declare clone_input_schema (same JSON Schema subset as payload_schema). That schema validates envelope.payload — assignment fields belong there, not at the top level of the clone_forks item. Parent emit checks once and fanout assignment payloads against it. Omit the field to skip the assignment-payload check. Never validate clone briefs against the child's output payload_schema. skip does not need an assignment payload.

Sequential vs parallel join: in parallel, sibling clones still finish after a failure, but the join successor and its descendants are skipped unless every clone succeeded. In sequential, the first failure skips remaining clones of that successor and the join successor does not run.

clone_forks is required for each clonable successor. When the parent also has fork, fork_choice names only non-clonable siblings. A fork parent whose every child is clonable does not require fork_choice. See clone-fanout-mix.pipeline.yaml and examples/clonable-fanout/ scenario F.

A clone may skip / once / fan-out its next stage only when that successor is clonable. Extra clone_forks is ignored only when the emitting stage has no clonable children (for example a nested clone whose successor is a non-clonable join). If any clonable child exists, clone_forks must list every clonable successor exactly once; extra successor_ids are rejected. See clonable-nested-gate.pipeline.yaml and examples/clonable-fanout/. Two clones fanning out the same successor is unsupported in v1 because instance ids are {catalogId}~{n}. Dual-parent nested fan-out is fail-closed at apply.

After fan-out, workspace paths and --stage keys use the instance id ({catalogId}~{n}); run-once keeps the catalog id. See YAML catalog — instance ids.

Feedback loops {#feedback-loops}

When the emitting stage's pipeline entry declares feedback_loop (see YAML catalog), a successful emit must include feedback_loop:

{ "action": "continue" }

or

{ "action": "send_back", "target": "implement" }
Action Effect
continue Accept the stage and schedule normal successors (exit the loop / advance past the source).
send_back Replay from target through the source. target must equal the policy's target.

Rules:

On replay, agents see a Feedback Loop Context section (JSON) with loop/replay ids, the source's send-back envelope (summary, artifacts, payload), remaining replays, route stage ids, and optional prior-attempt / active fork-generation hints. Use that context — not scraped transcripts — to address the feedback.

After max_replays, behavior follows on_max_replays (require_continue or wait_for_human). Human decisions: CLI, MCP decide_feedback_loop, or POST /api/runs/:runId/stages/:stageId/feedback-decision.

Walkthrough: examples/feedback-loop/. Fixture: feedback-loop.pipeline.yaml.

payload_schema {#payload-schema}

When a stage declares payload_schema, success payload is required and checked against a JSON Schema subset (src/envelope/payloadSchema.ts). The root must be type: object and cannot be nullable. Supported node types: object, string, number, integer, boolean, array. Keywords: properties, required, items, additionalProperties (boolean only), minItems, enum (string and integer), minimum, maximum. String nodes also accept pattern (a JavaScript RegExp string, unicode semantics), minLength, and maxLength (non-negative integers). Nested nodes may set nullable: true, compiling to a union of that type with null. Unknown keywords are ignored.

Fixture: tests/fixtures/stages/name-selection.yaml.

pre_emit_checks {#pre-emit-checks}

A stage may declare pre_emit_checks (src/types/preEmitCheck.ts) — a small, in-session gate emit_stage_envelope itself enforces on every success emit, this attempt, before a candidate envelope is even captured:

id: approve-plan
gate_kinds: [artifact_backed]
pre_emit_checks:
  - id: plan-approved
    type: gate
    kind: artifact_backed          # last artifact_backed exchange this attempt must be accept
  - id: plan-artifact-present
    type: artifact_declared
    basename: implementation-plan.md  # must appear in envelope.artifacts (suffix match, no disk I/O)
system_prompt: |
  Review the plan artifact. Ask the operator to accept it, then emit success.
model: anthropic/claude-sonnet-4-5
Check type Required fields Semantics
gate id, kind The last ask_operator exchange of this kind in this attempt must satisfy it: confirm/artifact_backed need decision: "accept"; free_text/multi_question need any completed (answered) exchange.
artifact_declared id, basename basename must appear in the emitted artifacts list, either verbatim or as a /<basename> path suffix. Purely a list check — no filesystem access.

A failing check rejects the emit (isError: true, no terminate) so the agent can retry in the same turn; it never fails the stage outright. Checks run in declaration order and stop at the first failure. pre_emit_checks is skipped entirely on status: "failure" emits, and omitted/empty pre_emit_checks is a no-op — existing stages are unaffected.

Not the same as completion. completion is a pipeline-wiring field that runs after a candidate envelope has already been captured (via repair/manual recovery), and its gate/artifact check types are disk- and history-aware (completion's gate accepts any accepted decision over the run so far; its artifact type does a real on-disk lstat/sha256 check). pre_emit_checks is a stage-body field the agent's own emit_stage_envelope call enforces synchronously, mid-turn, with no disk access — a different mechanism for a different moment. A stage may declare both; they are allowed to look at overlapping facts (defense in depth), never the same field or the same check runner.

Artifacts

Use write_stage_artifact to create files under the stage attempt directory. The tool path is relative to stages/<stageId>/attempts/<n>/artifacts/:

stages/<stageId>/attempts/<n>/artifacts/<your-file>

The tool returns a run-relative path for emit_stage_envelope and ask_operator.

Example paths referenced in fixtures:

Artifact-backed HITL gates reference these paths in ask_operator — see HITL.

Storage

Accepted envelopes persist in the SQLite run store (SF_STORE=sqlite only; see CLI storage). Artifacts still live under stages/<stageId>/attempts/<n>/artifacts/. CI recipes that write envelope.json (see CI consumption below) are sf envelope get exports, not run-workspace storage.

Rules

  1. One advancing emit per stage attempt — later emits are ignored after the first acceptance
  2. artifacts is required — pass [] when there are no files
  3. summary must be non-empty
  4. ask_operator does not complete the stage — call emit_stage_envelope after gates are resolved

Downstream consumption

Later stages receive prior envelope context through the stage bootstrap (task + upstream summaries/payloads). Exact prompt assembly is handled by the runtime; authors focus on meaningful payload and summary content.

Two join shapes — do not reuse one field for the other:

  1. Keyed generic fan-in — the stage needs array has length ≥ 2. Join input is priorEnvelopesByStage, a record keyed in YAML declaration order. A normal parent maps to one terminal envelope; a clonable parent maps to that parent's clone-list-ordered envelope array (or [] when a skip of the definition is accepted). priorEnvelope is null. priorEnvelopes is omitted. A failed parent uses its emitted failure envelope or a synthetic { status: "failure", summary, artifacts: [] } from the persisted failure reason. A skipped parent uses a synthetic { status: "skipped", summary, artifacts: [] } rebuilt from persisted lifecycle state — agents cannot emit skipped. See YAML catalog — generic fan-in, examples/generic-fan-in/, and diamond-fan-in.pipeline.yaml.

  2. Clone-list join — still one catalog parent id. After clonable fan-out, the join successor receives every clone envelope as an ordered list in clone-list order (priorEnvelopes). The join stage only runs if every clone succeeded (parallel and sequential); those priors are success envelopes only (0.7). See examples/clonable-fanout/ collect checks.

Inspect envelopes in the operator console: run detail → stage → envelope view (#/runs/<runId>/stages/<stageId>/envelope).

MCP get_run returns envelope summary and artifact paths without full event streams.

CI consumption {#ci-consumption}

In headless CI, downstream shell steps read envelopes via the CLI instead of querying SQLite or scraping transcripts.

Raw envelope (stage contract as emitted):

sf envelope get --from sf-run.json --stage detect-changes --format envelope --json

Handoff deliverables (normalized shape for GHA scripts — absolute artifact paths, fork skip detection):

sf envelope get --from sf-run.json --stage author-diagrams \
  --detect-stage detect-changes --format handoff --json > envelope.json

When the detect stage emits fork_choice: [], handoff output is { "skipped": true } and downstream deliver/upload steps can no-op.

Typical CI flow:

  1. sf run --json --include stages > sf-run.json
  2. sf envelope get --format handoff …envelope.json
  3. Shell script consumes envelope.json (see examples/archify-on-pr/)

Full recipe: CI / headless · CLI flags: sf envelope get

See also