superagentic
Spawn ten agents on one job and they all start on page one — then each invents its own idea of what "done" means.
Two problems, and they have the same cause: a freshly spawned agent has no context. It did not read your orchestration code, it cannot see the other nine, and it will not remember any of this next session. So it needs two things it can only get by asking:
- which unit is mine — nobody else is on it, and if I die it comes back;
- what am I supposed to do with it — the task, what finished looks like, what to hand back.
superagentic is where both live. The orchestrator defines the work and
enqueues the units; every worker claims one and is handed the assignment
with it. A library, a CLI and an MCP server, in one SQLite file, with no
dependencies at all.
uv tool install superagentic # the CLI, on your PATH
uvx superagentic demo # or run it without installing
uv pip install superagentic # or as a library
brew install narimannemo/tap/superagentic # or via Homebrew
The shape of it
import superagentic as sa
conn = sa.connect("work.db")
# One execution of a fleet. Everything below belongs to it, so afterwards you
# can ask what THIS run did rather than what the database contains.
run = sa.start_run(conn, label="Tomus II extraction")
# The orchestrator, once. This is the part a prompt cannot do:
# the ninth worker, spawned an hour from now, reads the same thing.
sa.define(conn, "extract",
instructions="Read $path. Record every claim it makes, quoting verbatim.",
done_when="every claim on the page is recorded, or you have established "
"there are none",
returns='{"claims": <int>, "notes": "<string>"}',
tools="the `xrad` MCP server: record_claim, check_quote")
sa.add(conn, "extract", pages, run=run, meta={"path": "scans/$name.png"})
A kind can also say what a worker must have — separate from what it must do, because a skill it never loaded is not something it can discover halfway through a unit:
sa.define(conn, "extract", instructions=...,
skills=["xrad-extraction"],
mcp={"xrad": "xrad serve --db graph.db"},
context=Path("ontology/glossary.md").read_text())
Then superagentic prompt extract -n 4 generates the spawn prompt from the
kind, already telling each worker which skills to load and to fail rather
than improvise if it cannot.
Then spawn ten agents with one instruction — claim work and do it — and each of them is handed this:
UNIT: p0189 (kind: extract, id: extract:p0189)
WHAT TO DO
Read scans/p0189.png. Record every claim it makes, quoting verbatim.
USE
the `xrad` MCP server: record_claim, check_quote
DONE WHEN
every claim on the page is recorded, or you have established there are none
HAND BACK
{"claims": <int>, "notes": "<string>"}
Call finish (unit_id=extract:p0189) when done, or fail with a reason.
Do not start any other unit.
When they are finished, sa.results(conn, "extract", run=run) is what they
produced, and sa.runs(conn) is every fleet you have ever run:
run label units done failed workers elapsed parallel
20260804-165708-410a * Tomus II extraction 40 0 0 0 30s —
20260804-165705-13f3 Tomus I extraction, new prompt 70 70 0 6 4s 5.1x
20260804-165650-24a8 Tomus I extraction 73 70 3 4 9s 0.8x
parallel is worker-seconds divided by wall-clock — how much concurrency you
actually got. 0.8x on a four-worker run means three of them were idle most of
the time and the units were too few or too uneven to fill them.
In sixty seconds
uvx superagentic demo
-- 2. three workers claim, and never collide ---------------
worker-a: page-1, page-2
worker-b: page-3, page-4
worker-c: page-5, page-6
6 units handed out, 6 distinct -- nobody got the same page
-- 3. two finish. the third crashes, holding its work ------
worker-c: [process dies without reporting anything]
-- 4. its lease expires, and the work comes back -----------
another worker asks immediately:
nothing -- still leased
...one second later, after the lease expired:
worker-d picked up page-5 (attempt 2)
No daemon ran. reclaim() happens on the way into claim().
-- 5. and the dead worker cannot close what it lost --------
worker-c calls finish on page-5: False
worker-d calls finish on page-5: True
A lease, not a lock
This is the only hard part of the problem, and every other decision follows from it.
A lock held by a crashed worker is worse than no lock at all. The unit is neither being worked nor available, and nothing in the system can tell a busy worker from a dead one. A lease makes that distinction the passage of time: renew it and you keep the unit, stop renewing and it returns to the pool.
There is no daemon and no cron. reclaim() runs at the top of every claim(),
so the next worker asking for work does the cleanup on its way in.
At-least-once, and nothing can do better
Said here rather than in a footnote, because the alternative is you finding out in production:
A worker that is slow rather than dead will have its lease expire, another worker will take the unit, and both will finish it.
No timeout distinguishes those two cases. Two defences, and you want both: heartbeat while you work, so only genuinely stalled units are reclaimed; and make the write at the end idempotent, so a unit done twice converges.
When a lease is lost, finish returns False rather than raising. Handle it —
this worker no longer owns the unit and should claim a different one.
The worker loop
while units := sa.claim(conn, "translate", lease=1800):
for u in units:
try:
out = do_the_work(u.name, u.instructions)
sa.finish(conn, u.unit_id, result=out)
except Exception as e:
sa.fail(conn, u.unit_id, note=str(e))
Stages compose without this becoming a scheduler — a finishing worker hands the next stage its units:
sa.finish(conn, u.unit_id, result={"claims": 12},
then={"audit": [f"claim-{i}" for i in ids]})
Nothing is enqueued if the close failed, so a worker that lost its lease cannot inject work off the back of a unit it no longer owns.
From the shell
claim exits 1 with no output when the queue is dry, so a loop ends by itself.
Eight workers, no coordinator:
superagentic add extract --from-file pages.txt
for i in $(seq 1 8); do
( while unit=$(superagentic claim extract --json --lease 1800); do
id=$(echo "$unit" | jq -r '.[0].unit_id')
name=$(echo "$unit" | jq -r '.[0].name')
if my-extractor "$name"; then
superagentic done "$id"
else
superagentic fail "$id" --note "extractor exited $?"
fi
done ) &
done
wait
superagentic status --who
From an agent
Nine MCP tools, split by who uses them.
The orchestrator — the agent that spawns the fleet — uses define_kind,
add_jobs and job_results. It can set up an entire fleet without touching a
shell.
Each worker uses claim_job, finish_job, release_job, fail_job,
heartbeat_job and job_status.
The tool descriptions carry the protocol, because that is all a worker reads:
claim before starting, do what the unit's brief says rather than what you
assume the task is, and stop when the queue is empty rather than invent
work — which is the failure mode worth designing against, since an agent with
nothing to do will reliably find something, and what it finds is usually a unit
somebody else has.
{"mcpServers": {"work": {"command": "superagentic",
"args": ["serve", "--db", "work.db"]}}}
See MCP.
Watching it run
superagentic dashboard --db work.db # http://127.0.0.1:8787
superagentic dashboard --out fleet.html # a static snapshot
Every run, newest first, with what it did — click one to scope every panel to
it. 14 left is the same number whether four workers are moving through it
steadily or three have died and one is stuck on a poison unit. The dashboard is the
difference: throughput over time, what every worker is holding right now and
for how long, duration p50 against p95, and a stripe on any unit held past
three times the p95 — because "is anyone stuck?" is the question, and a raw
duration column does not answer it.
A sidebar switches between projects (each is a database) and runs;
selecting one scopes every panel. Served from http.server, CSS and JS inline,
SVG drawn by hand. No framework, no build step, nothing fetched.
Read-only, so pointing it at a live run cannot disturb the run. There is an optional access token with sign in and sign out — and because there is no TLS, the server refuses to bind off-loopback unless one is set.
Documentation
| Concepts | leases, attempts, and what this deliberately is not |
| MCP | wiring it to Claude Code, Cursor, or your own agent |
| Dashboard | what each panel answers, and why percentiles not averages |
| Reference | every command and the Python API |
| Packaging | uv, Homebrew, pip — and which to use |
| Skill | drop-in Claude Code skill, so an agent knows how to run a fleet |
What it is not
Not a scheduler. No dependencies between units, no backoff, no cron, one integer of priority. If you need those, run a real queue and keep this for the hand-out.
Not a broker. One SQLite file on one filesystem. Many processes, one box. SQLite over NFS is not safe and this does not pretend otherwise.
Not exactly-once. See above. Nothing is.
It does not do your work or check it. It hands out units and carries your instructions verbatim. Whether the agent followed them is between you and the agent.
Apache-2.0. Contributions welcome — CONTRIBUTING.md says what will and will not be accepted before you spend an evening.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file superagentic-0.4.2.tar.gz.
File metadata
- Download URL: superagentic-0.4.2.tar.gz
- Upload date:
- Size: 80.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ddc0f029f98573e23e3754f5f378f04ead22fd9a9dcb5780db0488b3ef49ed8
|
|
| MD5 |
7becbb1697dd26102104f14b54a4672a
|
|
| BLAKE2b-256 |
5e3ce4b2c6d572cbe1918bee7888134be168c7a8c53b272df53af5bc054aee1d
|
Provenance
The following attestation bundles were made for superagentic-0.4.2.tar.gz:
Publisher:
release.yml on narimannemo/superagentic
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
superagentic-0.4.2.tar.gz -
Subject digest:
8ddc0f029f98573e23e3754f5f378f04ead22fd9a9dcb5780db0488b3ef49ed8 - Sigstore transparency entry: 2340934379
- Sigstore integration time:
-
Permalink:
narimannemo/superagentic@80cef6b2193821d14e85eb0abaa192730574272c -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/narimannemo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@80cef6b2193821d14e85eb0abaa192730574272c -
Trigger Event:
push
-
Statement type:
File details
Details for the file superagentic-0.4.2-py3-none-any.whl.
File metadata
- Download URL: superagentic-0.4.2-py3-none-any.whl
- Upload date:
- Size: 51.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2abb079e361e7fac8046a9c08ca8876c36980286e2299c37b3e19f1c790bac7
|
|
| MD5 |
7cc9cc8f157eafec5ef556d15d9b9d56
|
|
| BLAKE2b-256 |
a08f686e510b6a6da8a12965f25b0e90a177be658b3497965b6c7131fafaeaca
|
Provenance
The following attestation bundles were made for superagentic-0.4.2-py3-none-any.whl:
Publisher:
release.yml on narimannemo/superagentic
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
superagentic-0.4.2-py3-none-any.whl -
Subject digest:
c2abb079e361e7fac8046a9c08ca8876c36980286e2299c37b3e19f1c790bac7 - Sigstore transparency entry: 2340934399
- Sigstore integration time:
-
Permalink:
narimannemo/superagentic@80cef6b2193821d14e85eb0abaa192730574272c -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/narimannemo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@80cef6b2193821d14e85eb0abaa192730574272c -
Trigger Event:
push
-
Statement type: