Stateless Pipeline Runner

July 3, 2026 · View on GitHub

Execute a skillflow pipeline through a stateless protocol: state lives in SQLite + the workspace, never in the process. You get an instruction, do the work, hand in the result, repeat. You never see the graph — the runner tells you what to do next.

Two transports, same protocol:

  • MCP (preferred when your host supports it)pip install skillflow-py[mcp], then register skillflow-mcp as a stdio MCP server. You get typed tools (runner_start, runner_next, runner_status, runner_submit, runner_approve, runner_reject, skillflow_tool) — no shell quoting of documents.
  • CLI — the skillflow-run commands below; works with nothing but a shell.

The rest of this document describes the protocol in CLI terms; MCP tool parameters map 1:1 (--result '{"k":"v"}'result={"k": "v"}).

CLI reference

# Start a new run (--graph only on start)
skillflow-run --graph pipeline.yaml --action start

# All subsequent calls use --run-id to reconnect state
skillflow-run --action submit --run-id <id> --result '{"key":"val"}'
skillflow-run --action approve --run-id <id>
skillflow-run --action reject --run-id <id> --feedback "reason"
skillflow-run --action reject --run-id <id> --feedback "reason" --redirect-to <step>  # loop back to a different step
skillflow-run --action abort --run-id <id>

Every call prints one JSON line to stdout. Parse it, act, call again.

Interaction Protocol

Agent                         skillflow-run
  │                                │
  │── --action start ──────→      │  creates run, claims first step
  │    --graph pipeline.yaml       │
  │←── JSON ──────────────────    │  {status: "in_progress", step, instruction, tools}
  │                                │
  │  [do the work]                 │
  │                                │
  │── --action submit ──────→     │  confirms step, advances graph
  │    --run-id abc123             │  (auto-resolves gates, loops)
  │    --result '{"key":"val"}'    │
  │←── JSON ──────────────────    │  {status, step, instruction, tools}
  │                                │
  │  ... repeat ...                │
  │                                │
  │←── JSON ──────────────────    │  {status: "completed", outputs: {...}}

SkillResponse Format

In progress (work to do)

{
  "status": "in_progress",
  "step": "analyze_diff",
  "instruction": "## Task\nExecute step `analyze_diff`.\nWrite output files to the output directory:\n- `findings.json`",
  "tools": {
    "write_findings": {"name": "write_findings", "description": "Replace findings.json...", "parameters": {"content": {"type": "string", "required": true}}}
  },
  "output_dir": "/path/to/analyze_diff.tmp",
  "expected_files": ["findings.json"],
  "validation_error": ""
}

Deliver each expected output EITHER by passing it in submit--result '{"<slot>": "<content>"}' / result={"<slot>": <content>} — OR, on MCP, by writing it first via the skillflow_tool proxy (skillflow_tool(run_id, step_id, name="write_<slot>", params={"content": ...})) and then submitting with an empty result. Do NOT write these files with your own file tools — the staging directory belongs to skillflow. The instruction's "Expected outputs" section lists each slot and its format.

If validation_error is non-empty, the previous submit was rejected. Fix the issue described and re-submit.

Paused at checkpoint

{
  "status": "paused",
  "step": "summarize",
  "checkpoint_label": "Review Summary",
  "instruction": "Pipeline paused. Call approve or reject."
}

Call --action approve to continue, or --action reject with --feedback to redo the step. Add --redirect-to <step> to loop the run back to a different earlier step instead of redoing this one — the feedback is carried to that target (e.g. reject a final review back to the planning step).

Completed

{
  "status": "completed",
  "outputs": {
    "analyze_diff": {"findings": [...]},
    "summarize": {"review": "..."}
  },
  "steps_completed": 5
}

The pipeline is done. Present outputs to the user.

Failed

{
  "status": "failed",
  "error": "No matching transition from 'review' with flags {...}"
}

Report the error to the user.

Rules

  1. Start with --action start --graph pipeline.yaml (no --run-id) — save run_id from the response
  2. Always pass --run-id back on every subsequent call to resume the session
  3. On status="in_progress": if expected_files is non-empty, write those files to output_dir before submitting. Then --action submit with --run-id and --result
  4. On status="paused": decide — --action approve or --action reject with --run-id and --feedback
  5. On status="completed": done — present outputs
  6. On status="failed": report error
  7. Never submit twice in a row — wait for a new in_progress
  8. If validation_error is set on the response, fix the issue and re-submit (the step repeats)
  9. If you lose state, call --action next --run-id <id> with the last known run_id to reconnect

Tool nodes

Tool nodes are always delegated to the agent. They're presented as regular steps with tool_name and tool_params:

{
  "status": "in_progress",
  "step": "validate_design",
  "tool_name": "skillflow_lint",
  "tool_params": {"path": "/workspace/design/skill_pipeline.yaml"},
  "instruction": "Execute tool: skillflow_lint"
}

You execute the tool (using your own tool infrastructure), then submit the result. The runner stores it and advances the graph.

Native tools (under src/skillflow/tools/) are auto-executed — you never see them.

Variable substitution

You may encounter $CONFIG_DIR, $STEP_DIR, $STEP_TMP_DIR, $PROJECT_ROOT, or $TASK_DIR in tool_params. These are path variables resolved at runtime:

VariableResolves to
$CONFIG_DIRThe graph's per-config workspace directory
$STEP_DIRThe promoted output directory of the current step
$STEP_TMP_DIRThe temporary staging directory for step output
$PROJECT_ROOTThe project root directory on disk
$TASK_DIRThe project's tasks subdirectory

You do not need to expand these yourself. They are resolved before the tool executes. Example: "$CONFIG_DIR/design_graph/skill_pipeline.yaml" points to the skill_pipeline.yaml output of the design_graph step.

Checkpoints are for your user, not you

When the runner returns {status: "paused"}, present the checkpoint to the human user behind you. Do NOT auto-approve or reject.

{
  "status": "paused",
  "step": "summarize",
  "checkpoint_label": "Review Summary — approve to commit, reject to revise",
  "instruction": "Pipeline paused at checkpoint. Call approve or reject."
}

Your job:

  1. Show the checkpoint label and outputs to the user
  2. Ask if they approve
  3. If yes → --action approve
  4. If no → --action reject --feedback "reason"

What you don't need to worry about

  • Gates — auto-resolved, never shown to you
  • Native tools — auto-executed inline, never shown
  • Loop steps — auto-iterated, each iteration appears as a regular agent step
  • Error handlers — routed automatically on retry exhaustion
  • Stale claims — auto-recovered by advance_run