Background tasks
Werbos's background work queue: SQLite-backed task store, asyncio runner that yields to foreground turns, /tasks slash command. The plumbing that lets agents run while you're not looking.
Werbos has a background work queue. Today it runs one stub executor
(stats-refresh) — the surface exists for the dogfood-replay agent,
inbox janitor, and continuity-of-intent agent that are queued in the
roadmap.
The defining property of every background agent here: it yields to foreground turns. When you’re mid-chat, the runner stays out of the way. When the AI turn-lock releases, the runner picks up where it left off. No 5-second stalls when you start a new chat.
The pieces
werbos.tasks.TaskStore
SQLite-backed, optimistic-lock CAS via a version column. Status
transition rules: pending → running → done | failed | cancelled.
UUIDv7 ids (time-sortable). tenant_id and user_id columns from
day 1, defaulting to "default" / "local" in single-user mode —
the gatekeep multi-tenant seam is baked in so a future hosted SKU
is a default-value change, not a refactor.
werbos.tasks.TaskRunner
asyncio loop that lives in the Starlette lifespan. Each iteration:
- Check if the foreground is busy via observe’s foreground-busy callable. If yes, sleep.
- Pull the next pending task from the store (FIFO by created_at).
- Dispatch to the registered Executor for that task’s
kind. - Capture exception type + message into the row on failure; never crashes the loop.
Executors
A registered handler for one task kind. Today there’s one:
- stats-refresh — rebuilds
werbos stats-style aggregates off-thread when a chat archive lands. Lightweight enough to prove the runner end-to-end.
The dogfood-replay executor is the next slot — it’ll watch the
chat archive directory, pick recently-bad-labeled turns, and
replay them through the latest model / persona / LoRA, writing
per-turn rows to ~/werbos/regressions.jsonl.
HTTP surface
GET /api/tasks # list (paginated; observe-shaped)
POST /api/tasks # create — body: {kind, payload}
GET /api/tasks/{id} # one task with status + result
Every error response uses observe’s {error, detail} envelope and
ISO-8601 UTC Z-suffix timestamps so a watchtower runtime polling
this endpoint sees a consistent shape.
Tenant scoping is server-side: the request’s resolved
tenant_id filters reads and stamps writes. Single-user mode is
transparent — every request resolves to "default" / "local".
/tasks slash command
Inside a chat, you can enqueue and inspect tasks without leaving the conversation:
/tasks # list your pending + recent tasks
/tasks <kind> # enqueue a task of that kind
/cancel <id> # mark a pending task cancelled
The slash dispatch routes through the chat surface to the same HTTP endpoints. The chat turn that enqueued the task gets the task id back inline; the runner picks it up next idle moment; future chat turns can ask for status by id.
Foreground-busy
Werbos’s foreground state is exposed two ways for orchestration consumers:
In-process
from werbos.observe import make_foreground_busy_callable
is_busy = make_foreground_busy_callable(session_registry)
if is_busy():
# AI turn in flight; defer the agent dispatch
...
The callable matches observe SPEC v1 § 4 exactly:
Callable[[], bool], synchronous, side-effect-free, returns in
microseconds.
HTTP
GET /api/observe/foreground-busy
{
"protocol": "OBSERVE-PROTO-v1",
"foregroundBusy": false,
"asOf": "2026-05-08T07:00:00.000Z"
}
A separate-process watchtower runtime, a cron-driven warden, or a Prometheus shim can poll this without coupling to werbos’s Python surface. Round-trip is <5 ms with no active sessions.
What’s queued
- Watchtower Dispatch consumer — observe SPEC § 2.4.1 webhook
with HMAC verification. Will land at
/api/observe/dispatchso triggers fired by a separate-process watchtower can drive werbos agents. - Pending-actions endpoint —
/api/pendingwith/approve//denyslash commands. Lets background agents propose work that the user must explicitly green-light before it runs. - Dogfood-replay agent. The first real (non-stub) executor.
Watches
~/werbos/chats/, picks recently-bad-labeled turns, re-runs them through the latest model, writes regression rows. - Inbox janitor / dispatcher. Watches
~/werbos/inbox/, sorts easy stuff locally, escalates hard stuff to Claude for triage. - Daily Claude budget — cap on background-agent escalations (e.g. 100 calls / $5/day). Logged usage, “refuse + queue for tomorrow” when capped.