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 registerskillflow-mcpas 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-runcommands 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
- Start with
--action start --graph pipeline.yaml(no--run-id) — saverun_idfrom the response - Always pass
--run-idback on every subsequent call to resume the session - On
status="in_progress": ifexpected_filesis non-empty, write those files tooutput_dirbefore submitting. Then--action submitwith--run-idand--result - On
status="paused": decide —--action approveor--action rejectwith--run-idand--feedback - On
status="completed": done — present outputs - On
status="failed": report error - Never
submittwice in a row — wait for a newin_progress - If
validation_erroris set on the response, fix the issue and re-submit (the step repeats) - If you lose state, call
--action next --run-id <id>with the last knownrun_idto 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:
| Variable | Resolves to |
|---|---|
$CONFIG_DIR | The graph's per-config workspace directory |
$STEP_DIR | The promoted output directory of the current step |
$STEP_TMP_DIR | The temporary staging directory for step output |
$PROJECT_ROOT | The project root directory on disk |
$TASK_DIR | The 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:
- Show the checkpoint label and outputs to the user
- Ask if they approve
- If yes →
--action approve - 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