Runspool
Local-first CLI workflows for reliable personal automation.
Runspool turns scripts, files, and manual checklists into resumable, observable workflows — with SQLite state, retries, logs, pause/resume controls, approval for steps with side effects, and JSON output for humans, scripts, and AI agents.
It is built from plugins on a small kernel: the store, the step registry, the task lifecycle, even the CLI's extra commands are plugins, and your own steps, workflows and commands plug in the same way.
It runs entirely on your machine. No hosted service, no account, no data leaving your laptop by default.
简体中文 README · Docs · Examples
Why Runspool
Personal automation usually starts as a shell script and slowly turns into a mess: when it dies halfway through, you don't know what ran; re-running redoes work; there's no history; and pausing or retrying means editing the script.
Runspool gives that automation a backbone:
- Resumable — every task is a row in SQLite; a crash or reboot loses nothing.
- Observable — every state change is an event; every step run is timed.
- Controllable — pause, resume, retry, terminate, reprioritize from the CLI.
- Careful — a step marked as having side effects (publish, upload, send) waits for you to approve it, and never runs unapproved.
- Composable — workflows are ordered lists of steps; add steps, workflows and commands as plugins.
- Scriptable —
--jsonon every read command, built for shell and AI agents.
It is not an AI tool, and it is not a cloud workflow platform. It is a small, dependable engine for turning local scripts, files, and checklists into workflows you can trust.
Install
Recommended for CLI use, with uv:
uv tool install runspool
If you don't have uv yet:
curl -LsSf https://astral.sh/uv/install.sh | sh
Then check the command:
runspool --help
Alternatively, install with pip:
pip install runspool
Or from source (for development):
git clone https://github.com/ethan-sun-dev/runspool
cd runspool
uv sync
Requires Python 3.11+. Core dependencies: Typer, Pydantic, PyYAML (SQLite is in the standard library).
Quickstart (about 3 minutes, no setup)
# 1. Create a config and database.
runspool init
# 2. Queue a task. The default `local_file` workflow uses only built-in steps.
echo "Invoice #42 Total amount due: 1320 Payment terms: net 30" > invoice.txt
runspool add ./invoice.txt
# 3. Advance every task to completion, once.
runspool run
# 4. Look at the result.
runspool status
runspool inspect 1
You'll see the task flow through five steps and land its artifacts under
workspace/ready/1/ (normalized Markdown, a summary, a classification, and
metadata). That's a complete workflow with persisted state, logs, and a step
timeline — and it ran with zero external dependencies.
See the result (20 seconds, nothing to install)
Don't want to run it? Real, committed output of the quickstart lives in
sample-output/. Here's what runspool inspect 1 --json
returns after the invoice above completes — a single call that gives a script or
AI agent the whole picture:
{
"id": 1,
"name": "invoice",
"status": "completed",
"current_step": "archive",
"step_runs": [
{ "step": "ingest_file", "status": "ok", "duration_ms": 1 },
{ "step": "classify_text", "status": "ok", "duration_ms": 0 },
{ "step": "normalize_markdown", "status": "ok", "duration_ms": 0 },
{ "step": "summarize_text", "status": "ok", "duration_ms": 0 },
{ "step": "archive", "status": "ok", "duration_ms": 0 }
],
"artifacts": [
"ready/1/classification.json", "ready/1/metadata.json",
"ready/1/normalized.md", "ready/1/summary.md", "..."
],
"available_actions": [],
"suggested_next_action": "Task is complete; no action needed."
}
And the workflow turned a raw invoice.txt into structured artifacts — e.g.
ready/1/classification.json:
{ "category": "invoice", "confidence": 1.0,
"matched_keywords": ["invoice", "amount due", "subtotal", "total", "payment terms"] }
The full snapshot, task list, and every produced file are in
sample-output/. Note step_runs (per-step timing — failures
are attributable to a step, not just the task) and available_actions /
suggested_next_action (the engine tells an agent what it can and should do
next). See docs/design-decisions.md for why it's
shaped this way.
What it looks like
flowchart LR
CLI[runspool CLI] -->|add / run / daemon| ENG
AGENT[AI agent or script] -->|--json| CLI
subgraph ENG[Engine: core plugins on a small kernel]
COORD[Coordinator] --> POOL[Worker pool]
POOL --> RUN[Step runner + approval gate]
RUN --> STEPS[Step registry\nbuilt-in + plugins]
end
ENG <--> DB[(SQLite\ntasks · events · step_runs)]
RUN --> FS[(Workspace\nartifacts)]
A task carries an input through a workflow — an ordered list of
steps. The coordinator claims queued tasks (respecting per-step
concurrency quotas), the worker pool runs each step, and the state
machine records every transition. A step with side effects waits in
awaiting_approval until you runspool approve it. Long jobs run under the
daemon; one-shot runs use run.
Task lifecycle
queued → running → (next step) queued → … → completed | partially_completed
↘ queued, same step (deferred; optionally until a delay passes — `wake` ends it)
↘ failed ──(retry)──↗
↘ manual_required (retries exhausted; needs you)
↘ awaiting_approval ──approve──→ queued (side-effect step)
──reject───→ manual_required
running → pause_pending → paused → (resume) queued
non-terminal → terminated (terminal states refuse further control)
partially_completed means every step ran but one reported it could only do part
of its job. Pause and terminate always take effect at a step boundary, and
terminate wins; see docs/concepts.md.
CLI
runspool init # create config + database
runspool add <input> -w <wf> # queue a task (default workflow: local_file)
# --meta KEY=VALUE, --parent <id>, --name, --force
runspool run # advance all runnable tasks once (great for demos)
runspool daemon # run a resident loop (long-running automation)
runspool daemon-status # report whether a daemon is running
runspool daemon-stop # signal a running daemon to stop
runspool status [<id>] # list tasks, or show one in detail
runspool inspect <id> # agent-friendly snapshot + suggested next action
runspool logs <id> # event history for a task
runspool overview # counts by status
runspool pause|resume|retry|terminate <id>
runspool approve <id> # let a waiting side-effect step run (this attempt)
runspool reject <id> --reason ... # refuse it; the task needs attention
runspool wake <id> # run a deferred task now instead of after its delay
runspool set-priority|set-retries|set-step <id> <value>
runspool workflows # list workflows and their steps
runspool doctor # check the environment, plugins and credentials
Plugins add their own commands, shown by runspool -c <profile> --help — e.g.
the official WeChat plugin adds runspool wechat preview and
runspool wechat token.
Every read command supports --json:
runspool status --json
runspool inspect 1 --json
runspool logs 1 --json
runspool overview --json
runspool workflows --json
runspool doctor --json
See docs/cli.md for the full reference.
Built for AI agents and scripts
runspool inspect <id> --json returns exactly what an automated caller needs to
decide what to do next — current state, the last error, the artifacts produced,
the actions that are valid right now, and a plain-language suggestion:
{
"id": 1,
"status": "manual_required",
"workflow": "client_intel",
"current_step": "collect_sources",
"last_error": "FileNotFoundError: Missing required source(s): requirements.md",
"retry_count": 1,
"max_retries": 0,
"recent_events": [],
"artifacts": [],
"available_actions": ["retry", "set-step", "set-retries", "terminate"],
"suggested_next_action": "FileNotFoundError: Missing required source(s): requirements.md. Resolve the cause, then run `runspool retry 1`."
}
An agent can poll inspect --json, act on available_actions, fix the cause,
and call runspool retry 1 — no screen-scraping required. See
docs/agent-json-output.md.
Examples
Three runnable examples, each with its own README and sample data:
| Example | What it shows |
|---|---|
| local-file-pipeline | The quickstart. Built-in steps only; runs offline in minutes. |
| client-intel-brief | A real consulting workflow: sources → briefing package. Custom steps loaded from config; demonstrates manual_required recovery. |
| creator-publishing-pipeline | A content pipeline that builds a multi-platform draft package (never auto-publishes). Its steps come from a plugin package. |
Write a custom step
A step is a small class. It reads the task, does work, writes artifacts, and returns a result:
from runspool.engine.step import Step, StepContext, StepResult
class GreetStep(Step):
name = "greet"
def run(self, ctx: StepContext) -> StepResult:
ctx.heartbeat("working") # optional progress
name = ctx.task.get("name") or "world"
return StepResult(message=f"hello, {name}")
Load it from config and use it in a workflow:
plugin_paths: [steps] # directories added to sys.path (relative to this config)
steps:
greet:
import: "my_steps:GreetStep"
workflows:
hello:
steps: [greet, archive]
Steps can also raise StepDeferred to wait for a precondition (optionally for
a delay, without counting a failure), return degraded=True when they could only
do part of their job, or raise any exception to fail and retry. A step whose
effects leave your machine sets side_effect = True and runs only after you
approve it. See docs/writing-steps.md.
To ship steps with their own config, default workflow, commands and doctor
checks — installable with pip — package them as a plugin; see
docs/plugins.md. The official
runspool-wechat plugin (lay out Markdown for WeChat
Official Accounts and save it as a draft, after approval) is a complete example.
Concurrency
Runspool runs steps in a bounded thread pool. The daemon keeps the pool busy
across many ticks, so long steps don't block the queue, and a per-step
concurrency quota caps how many of a given step run at once. A crashed or
silent worker's task is reclaimed by heartbeat timeout. See
docs/concepts.md.
Claiming is atomic. A task is claimed with a single conditional
UPDATE ... WHERE task_status = 'queued', and the caller checks rowcount to
learn whether it won — never check-then-act. That makes claiming correct even
when two processes race for the same task (e.g. a run invocation alongside a
live daemon), so exactly one worker ever executes a given step. The state
machine is the only place transitions are decided; steps return data and never
touch the database. The reasoning behind these boundaries is in
docs/design-decisions.md.
Privacy & safety
- Local-first. All state lives under
workspace_rooton your machine. There is no hosted service and nothing is uploaded by default. - No secrets required. The engine and built-in steps need no API keys. A plugin that does (like runspool-wechat) refers to secrets by name only; values come from your environment or an owner-only credentials file, never from config, logs or errors.
- Approval before side effects. A step that publishes, uploads or sends runs only after you approve that attempt; without an approval policy it is refused, never run.
- Drafts, not auto-publish. Content examples and the WeChat plugin produce drafts; publishing is always a deliberate, manual step.
Non-goals
- Not a hosted/cloud workflow platform.
- Not a distributed scheduler or a replacement for heavyweight orchestrators.
- Not an AI product (though it is deliberately AI-agent-friendly).
- No web UI in scope for now — the CLI and JSON are the interface.
Roadmap
runspool watchto follow a task's events live.- Optional structured log export (JSONL).
- Notification plugins that tell you a task awaits approval — and let you approve or reject from the message.
- runspool-wechat: themes, tables and image compression.
Contributing
Contributions are welcome — see CONTRIBUTING.md and the Code of Conduct.
uv sync
uv run ruff check .
uv run pytest
License
MIT.
Metadata
Release files for runspool 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| runspool-0.2.1.tar.gz | 192.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| runspool-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 297.9 kB
Release files / runspool-0.2.1.tar.gz
| Download URL | runspool-0.2.1.tar.gz |
|---|---|
| Size | 192.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
07f2c242ade1e09172a7cd36364350b3d0e099f62a9e2456ddffc137df78d663
|
|
BLAKE2b-256 checksum How to use checksums |
f7f12d126a9be6808b66c434d1dcca76f76d0177825ec326b671adfa0a088cc5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency logRelease files / runspool-0.2.1-py3-none-any.whl
| Download URL | runspool-0.2.1-py3-none-any.whl |
|---|---|
| Size | 105.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f323820b3dd21bfade9510489abe95c8bceb575026c7d17af9af864423fa8234
|
|
BLAKE2b-256 checksum How to use checksums |
9a51098d325fc055c0ee371a0d7fc8ca31fcfafb5691c7d601dcaeb55573342b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency log