Skip to the content.

CI / headless

Stageflow is designed to run the same YAML catalog locally, in the operator console, and in CI. The guest actor in CI is the CLIsf ui and MCP are not required in the job.

Run validate and pipeline commands from the repository root ($GITHUB_WORKSPACE) with path arguments:

sf validate --strict --json
sf run --task examples/hello-world/my-task.task.yaml --pipeline examples/hello-world/hello.pipeline.yaml --json

Validate in CI

Check catalog shape before running agents:

sf validate --strict --json
Exit Meaning
0 No errors
1 Validation errors (warnings alone pass unless --strict promotes manifest warnings)

With no flags, sf validate validates all pipelines and tasks declared in stageflow.yaml (manifest-all), including each pipeline’s stages. --pipeline validates that pipeline and its stages (uses: / include:), not all tasks. --task validates that task file. The CLI rejects both --pipeline and --task. --strict promotes catalog.manifest_missing and catalog.empty_catalog warnings to errors.

Does not prove provider auth or checkout paths.

JSON output includes ok, scope, checks, summary, and findings[] with severity, code, file, message, category. The CLI remaps each finding’s path to file; MCP validate keeps path.

Run in CI

sf providers login anthropic --type api_key --api-key-env ANTHROPIC_API_KEY
sf run --task examples/hello-world/my-task.task.yaml --pipeline examples/hello-world/hello.pipeline.yaml --json

Provider login stores credentials in the job environment (prefer --api-key-env over prompts).

Exit codes

Code outcome When
0 succeeded Pipeline completed
1 failed or busy Stage error, validation at start, concurrency conflict
2 waiting Stage blocked on HITL

Unchosen branches in fork pipelines are skipped, not failed; a run where all non-failed stages are succeeded or skipped exits 0. A parent that failed in a state a generic fan-in join explicitly accepts does not independently fail the run.

For unattended CI, either use pipelines without ask_operator, or pass --skip-gates (fails the stage with exit 1 instead of parking). See HITL. The CI guest uses sf run --json / --skip-gates only — it does not wait or answer with sf runs. Outside CI, humans and agents can continue a parked run with sf runs.

JSON stdout

One document per invocation with --json:

Success:

{
  "ok": true,
  "outcome": "succeeded",
  "runId": "…",
  "runDir": ".stageflow/runs/…"
}

Waiting:

{
  "ok": false,
  "outcome": "waiting",
  "runId": "…",
  "runDir": "…"
}

Failed (run created):

{
  "ok": false,
  "outcome": "failed",
  "runId": "…",
  "runDir": "…",
  "reason": "…"
}

Busy (outcome: "busy", no runId):

{
  "ok": false,
  "outcome": "busy",
  "code": "busy_capacity",
  "reason": "…",
  "activeCount": 3,
  "maxConcurrent": 3,
  "activeRunIds": ["…"]
}

code is busy_capacity or busy_checkout. Capacity includes activeCount / maxConcurrent / activeRunIds; checkout conflict includes conflictingRunId / conflictingCheckout.

Start failed without a run (outcome: "failed", no runId):

{
  "ok": false,
  "outcome": "failed",
  "reason": "…"
}

Optional code when the start failure reports one.

Validation failure during sf run --json: stdout is validate-shaped JSON (ok, scope, checks, findings…) with no outcome / runId. Exit 1. Distinct from outcome: "failed".

ok is true only for succeeded.

Including stage projections {#including-stage-projections}

Pass --include stages with --json to append a stages[] array to the completion document. Each item is a StageProjection (snake_case): stage_id, status, envelope, artifacts, and optional last_at, pending_prompt. That --include stages schema is unchanged for diamond runs — it does not add pipeline_track or join-input fields. The multi-edge graph (a diamond join has two inbound pipeline_track edges; blocked_by lists unresolved parents) is on sf runs show --json, MCP get_run, and sf export-run. --include stages without --json exits 1. After clonable fan-out, --stage and stages[] ids are instance ids (work~1), not the catalog id; run-once stays the catalog id. See YAML catalog — instance ids.

sf run --task examples/hello-world/my-task.task.yaml \
  --pipeline examples/hello-world/hello.pipeline.yaml \
  --json --include stages > sf-run.json

On success the document adds stages[] alongside the usual root keys (ok, outcome, runId, runDir). Without --include, the baseline shape is unchanged — no stages key.

If the run store cannot be read after completion (for example, a locked database), the command exits non-zero and prints an error to stderr instead of emitting JSON without stages[].

Run records store optional pipeline_path and task_path catalog locators (for resume and triage). These appear on MCP get_run and console run detail — not in CLI --json stdout.

Handoff envelope extraction {#handoff-envelope-extraction}

After sf run --json, read stage deliverables without querying SQLite:

sf envelope get --from sf-run.json --stage author-diagrams \
  --detect-stage detect-changes --format handoff --json > envelope.json
--format Output
envelope (default) Raw stage envelope JSON
handoff CI deliverables shape: { skipped: true } when detect emits fork_choice: [], otherwise { skipped: false, runId, runDir, stageId, diagrams: [{ diagram_type, spec_path, summary }] } with absolute spec_path values

Use --from sf-run.json to read runId and runDir from a prior sf run --json output file. --detect-stage is optional; when set, an empty fork_choice on that stage emits { skipped: true } and exits 0.

Composite action

This repo ships .github/actions/sf-run for same-repo workflows only. It invokes node dist/cli.js (the repo must be built with npm run build), not the global sf from npm i -g stageflow used in the recipe below. It runs sf run --json --include stages, optionally extracts a handoff envelope, and optionally writes run-export.json.

The action propagates any non-zero exit from sf run, including exit 2 (HITL waiting fails the GHA step). For unattended HITL pipelines pass extra-args: --skip-gates.

- id: sf-run
  uses: ./.github/actions/sf-run
  with:
    pipeline: examples/archify-on-pr/archify-on-pr.pipeline.yaml
    task: examples/archify-on-pr/archify-on-pr.task.yaml
    checkout: $
    extra-args: --skip-gates --git-sha $
    detect-stage: detect-changes
    stage: author-diagrams
Input Description
pipeline, task Required pipeline and task YAML paths
checkout Checkout path for sf run (default: $)
extra-args Additional sf run flags
detect-stage Stage id for fork skip detection in handoff extraction
stage Stage id for handoff extraction (writes envelope.json)
export-run Set to true to write run-export.json via sf export-run
Output Description
run-id, run-dir From sf-run.json
skipped "true" when handoff JSON has skipped: true
envelope-path Path to envelope.json when stage is set
run-export-path Path to run-export.json when export-run is true

CI identity metadata

Optional flags on sf run (auto-detected on GitHub Actions when omitted):

Flag / env Description
--git-sha / GITHUB_SHA Git commit
--ci-pr-url Pull request URL (derived from GITHUB_REF when possible)
--ci-job-url Job URL (derived from GITHUB_SERVER_URL, GITHUB_RUN_ID, etc.)

Recorded on the run for operator triage in the console.

Skills in CI {#skills-in-ci}

Stages can reference installed skills via the skill: field in stage YAML. Skills resolve from the operator checkout { cwd, agentDir }:

The guest CLI defaults to { cwd: process.cwd(), agentDir: getAgentDir() }. Override when the job checkout is not the skill tree root or when skills live in a shared agent dir on the runner.

Extensions in CI

Only provider-level hooks are supported today — for example STAGEFLOW_CURSOR_EXTENSION for Cursor-backed models. Per-stage extension YAML in the catalog is not supported in headless CI yet.

GitHub Actions recipe

name: Stageflow

on:
  pull_request:
  push:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm i -g stageflow
      - run: sf validate --strict --json

  run-pipeline:
    runs-on: ubuntu-latest
    needs: validate
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm i -g stageflow
      - name: Provider auth
        env:
          ANTHROPIC_API_KEY: $
        run: sf providers login anthropic --type api_key --api-key-env ANTHROPIC_API_KEY
      - name: Run pipeline
        run: sf run --task examples/hello-world/my-task.task.yaml --pipeline examples/hello-world/hello.pipeline.yaml --json --skip-gates
        # When skills live outside the repo checkout, add:
        # --operator-cwd path/to/checkout

Adjust task, pipeline, and secrets for your project. Dogfood release automation lives in examples/github-release/. To rewrite notes on an already-published GitHub Release, run Repair GitHub Release notes from the Actions tab.

Concurrency env vars

Variable Effect
STAGEFLOW_MAX_CONCURRENT_RUNS Soft cap on parallel runs (busy exit if full)
STAGEFLOW_MAX_ACTIVE_STAGES_PER_RUN Parallel stages within one run
STAGEFLOW_MAX_ACTIVE_STAGE_PROCESSES Stage worker process cap
STAGEFLOW_OPERATOR_CWD Operator checkout root for skill resolution (see Skills in CI)
STAGEFLOW_OPERATOR_AGENT_DIR Pi agent directory for user/runner skills

State in CI

Runs write under <repo>/.stageflow/ at the git root. Cache or artifact this directory if you need post-job inspection; ephemeral runners can discard it.

PR diagrams (Archify) {#pr-diagrams-archify}

This repo dogfoods examples/archify-on-pr/ in .github/workflows/archify-pr-diagrams.yml.

The workflow is manually triggered via workflow_dispatch (not automatic on every PR). Inputs: pr_number and/or head_ref (provide one), plus optional base_ref (default main). Sticky PR comments run only when pr_number is set.

Provider auth uses OpenRouter (OPENROUTER_API_KEY), not OpenAI. scripts/prepare-ci-context.sh writes full changed_files, filtered relevant_files, and deterministic diagram_types / change_summary / expected_fork_choice from path rules; when the relevant set is empty, GHA skips the pipeline early. detect-changes copies that context into changes.json and the envelope (no type-selection heuristics); pipeline completion checks the handoff against ci-context via scripts/validate-detect-envelope.mjs. author-diagrams then writes {type}.spec.json per type. The workflow uses .github/actions/sf-run with export-run: true, then runs Archify deliver for each spec via scripts/deliver-diagrams.sh, uploads per-type HTML (unzipped for in-browser viewing) plus a diagrams/ bundle, and updates a sticky PR comment when applicable. Skill provisioning uses sf skills install --from-zip; agents do not install Archify or post comments.

When detect emits fork_choice: [], GHA skips deliver, upload, and comment. Fork PRs cannot receive bot comments with the default token; see the example README.

See also