HTTP API
The application API is rooted at /api/v2. A running Server's
/openapi.json is the authoritative machine-readable schema for request and
response bodies. This page records the behavioral contract that an independent
Client or Worker must preserve.
curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/openapi.json > openapi.json
FastAPI's interactive Swagger and ReDoc pages are intentionally disabled.
Common contract
/healthand/openapi.jsonare unauthenticated. Every/api/v2endpoint uses the one Server-wide Bearer token when authentication is configured.- Request schemas reject unknown fields. Response schemas may gain optional fields within v2; existing fields, types, defaults, meanings, and Task states do not change without a new API prefix.
- Request bodies and complete stored Task data are each limited to 1 MiB. Large artifacts do not belong in Task JSON.
- Public timestamps are UTC RFC 3339 strings. Task states are exactly
pending,running,succeeded,failed, andcancelled. - Successful
204responses have no body. Clients must not invent receipt objects for them.
When enabled, authenticate application requests with:
Authorization: Bearer <token>
Application responses under /api/ include Labtasker-Server-Version with the
Server package version, including empty successes and handled errors. With
authentication enabled, only requests carrying the valid token receive it;
otherwise it is public. /health and /openapi.json do not include it.
Clients can observe this header without a preflight request. Older Servers or proxies may omit it; absence is not proof of incompatibility. The Python Client warns on stderr when a known Server version is older, while preserving the operation's result or error. Package version differences do not themselves establish protocol incompatibility.
Discovery
| Method and path | Success |
|---|---|
GET /health |
200 with {"status":"ok","api_version":"2","database":"ok"} after a real database check. Database failure returns 503 with status and database set to error. |
GET /openapi.json |
200 with the generated OpenAPI schema. |
Queue endpoints
| Method and path | Input | Success and constraints |
|---|---|---|
PUT /api/v2/queues/{queue} |
No body | 201 when created, 200 when already present; returns {"name": ...}. |
GET /api/v2/queues |
None | 200 with the complete unpaginated Queue array. |
DELETE /api/v2/queues/{queue}?cascade=false |
Boolean query parameter | 204. A non-empty Queue requires cascade=true; any running Task still blocks deletion. Successful cascade atomically deletes the Queue and all its Tasks. |
Queue names are explicit path components and are not authentication identities.
Task resource endpoints
| Method and path | Input | Success and constraints |
|---|---|---|
PUT /api/v2/queues/{queue}/tasks/{task_id} |
Task creation object | 201 on creation; 200 for an identical replay at the same ID; 409 task_id_conflict for a different definition. |
GET /api/v2/queues/{queue}/tasks/{task_id} |
None | 200 with one Task; 404 task_not_found when absent. |
GET /api/v2/queues/{queue}/tasks |
Selection and pagination query parameters | 200 with {"items":[...],"next_cursor":...}. Returns one page only. |
GET /api/v2/queues/{queue}/tasks/count |
Selection query parameters | 200 with {"count":N}, or a grouped page when group_by is supplied. |
PATCH /api/v2/queues/{queue}/tasks/{task_id} |
Non-empty Task update object | 200 with the Task. Running Tasks reject updates. Object/list fields are complete replacements. |
PATCH /api/v2/queues/{queue}/tasks |
{"filter":...,"changes":...} |
200 with {"matched":N,"updated":M}. The filter is required; the update is atomic across matching non-running Tasks. |
POST /api/v2/queues/{queue}/tasks/{task_id}/cancel |
No body | 200 with the cancelled Task. Accepts pending/running and is idempotent for cancelled. |
POST /api/v2/queues/{queue}/tasks/{task_id}/requeue |
No body | 200 with the pending Task. Accepts pending/failed/cancelled, resets attempt and last error. |
DELETE /api/v2/queues/{queue}/tasks/{task_id} |
None | 204; idempotent when absent. Running Tasks reject deletion. |
Creation body
Every field is optional because the Server expands the same defaults as the Python API:
{
"name": null,
"args": {},
"metadata": {},
"priority": 0,
"max_attempts": 3,
"routes": ["default"]
}
The Task ID is the resource identity and must match
t_[A-Za-z0-9_-]{12}. Replaying the same normalized definition returns the
Task's current representation. JSON object-key order, route input order, and an
omitted default versus the same explicit default do not change that identity.
List and count queries
Task listing accepts:
status, name, name_fuzzy, filter, order_by, descending, limit, cursor
Count accepts status, name, name_fuzzy, filter, and optional group_by
with grouped limit/cursor. Selectors are combined with AND.
limit is 1 to 1000 and defaults to 100. A non-null next_cursor must be reused
with the same Queue, selectors, filter, order field, and direction. The cursor is
opaque. See Query language for filter syntax.
name_fuzzy searches names using case-insensitive subsequences. Each
whitespace-separated word must match; words may occur in any order. Empty or
whitespace-only search adds no restriction. Non-empty search excludes unnamed
Tasks. Matching happens on the Server before pagination, uses literal punctuation,
and preserves ordering. Exact name and filter equality remain available.
Grouped counts
GET /api/v2/queues/{queue}/tasks/count?status=pending&group_by=routes,status
returns, for example:
{
"group_by": ["routes", "status"],
"count": 2,
"items": [
{"key": {"routes": "a", "status": "pending"}, "count": 2},
{"key": {"routes": "b", "status": "pending"}, "count": 1}
],
"next_cursor": null
}
One selected Task in this example supports both a and b. Selection applies
before route expansion. count is the deduplicated complete Task total; groups
can overlap. Task grouping supports only routes, status, or their combination
in either order, with one comma-separated group_by parameter and no spaces.
No arbitrary aggregation expressions or separate route records are introduced.
Groups sort lexically by values in the requested dimension order. Only nonzero
groups appear. limit defaults to 100 groups, with range 1–1000. Cursors bind the
resource, Queue, complete selection and ordered dimensions; page size may change.
Malformed or mismatched cursors return 422 invalid_cursor. Count and items
share a read transaction, but subsequent pages read current data.
Empty grouping, whitespace, unsupported or repeated dimensions, repeated
group_by parameters, and pagination without grouping return 422 invalid_request.
Ungrouped responses remain {"count":N}. A Client requesting grouping must reject
an old Server's scalar response rather than treating it as grouped data.
Update body
A Task update contains at least one of:
{
"name": "new name",
"args": {},
"metadata": {},
"priority": 10,
"max_attempts": 5,
"routes": ["robotwin"],
"result": {}
}
Every supplied object/list is a complete replacement. Identity, status, attempt, last error, run ownership, and timestamps are Server-owned. Bulk update counts only rows that match and remain non-running; a concurrent claim either sees all new values or excludes that Task. Validation failure for one matched non-running Task rolls back the complete batch.
Worker observations
All paths below require an existing Queue and the ordinary application token.
| Method and path | Input | Success |
|---|---|---|
GET /api/v2/queues/{queue}/workers |
filter, limit, cursor |
200 with {"items":[...],"next_cursor":...}, ID ascending, default 100/max 1000. |
GET /api/v2/queues/{queue}/workers/count |
filter, optional group_by, grouped limit/cursor |
200 with scalar or grouped counts. Dimensions: route, status, or both in either order. |
PUT /api/v2/queues/{queue}/workers/{id} |
Complete {"route":"sdxl","status":"busy","task_id":"t_ABCDEFGHIJKL"} |
204; creates or renews the observation. |
DELETE /api/v2/queues/{queue}/workers/{id} |
None | 204, including an absent Worker in an existing Queue. |
Each observation exposes id, queue, route, status, nullable task_id,
last_seen_at, and expires_at. IDs match w_[A-Za-z0-9_-]{12} and identify one
loop invocation across successive Tasks. All seven fields support the existing
filter grammar with their own types; Task-only paths are rejected. Worker count
pagination and filtering follow the grouped-count rules above. List cursors bind
the Queue and exact filter.
A report requires all three fields; task_id accepts null or a valid Task ID
without a Task lookup. Updating an existing instance to another route returns
409 worker_route_conflict. idle means waiting for work; busy begins at
confirmed claim and covers execution, reporting and cleanup, even after Task
completion. The bundled Worker reports null when idle and its Task ID when busy.
The Server assigns both timestamps and expires each accepted observation after 300 seconds. Bundled Workers report every 60 seconds and on activity changes through an independent reporter. Only unexpired rows appear in reads; expired rows are deleted at startup and on the lease-scan cadence. These are latest observations, with no history, process-control or Task-ownership effect. An advisory Task reference can be terminal, deleted or stale. Counts are approximate.
Observation failures never stop or block the Task loop or affect its failure guard. Once the loop has independently decided to exit, it waits at most one second for reporter shutdown and best-effort withdrawal. Crashes and failed withdrawals fall back to expiry. A delayed PUT can recreate a withdrawn row; there are no tombstones. Worker observations do not block Queue deletion and are removed with the Queue. Reports never recreate a missing Queue.
Worker protocol
These endpoints let an independent executor implement the same lease/fencing
contract as the bundled Workers. They are not public convenience methods on the
Python Client.
| Method and path | Body | Success |
|---|---|---|
POST /api/v2/queues/{queue}/tasks/claim |
{"route":...,"run_id":...} |
200 with Task, run_id, and lease_expires_at; 204 when no compatible pending Task exists. |
POST /api/v2/queues/{queue}/tasks/{task_id}/heartbeat |
{"run_id":...} |
200 with the renewed lease_expires_at. |
POST /api/v2/queues/{queue}/tasks/{task_id}/complete |
{"run_id":...,"result":{...}} |
204; completes the matching active run as succeeded. |
POST /api/v2/queues/{queue}/tasks/{task_id}/fail |
{"run_id":...,"error":{"type":...,"message":...,"traceback":...}} |
204; charges the failure and retries or fails according to the Task budget. |
POST /api/v2/queues/{queue}/tasks/{task_id}/unclaim |
{"run_id":...} |
204; returns the matching run to pending without an error payload. |
run_id matches r_[A-Za-z0-9_-]{12} and is a private per-claim ownership
token, not a Run resource. Heartbeat and terminal actions require the currently
active ID. A stale process cannot mutate a newer claim.
Claim matches the supplied route exactly and case-sensitively. Among compatible pending Tasks, higher priority is claimed first, followed by stable pending order. A healthy run renews its five-minute lease with heartbeat; the lease is not an execution timeout.
Terminal actions are effect-idempotent for the most recently finalized run and
same action. A contradictory action or stale run conflicts. The Server exposes
no /runs collection or execution-history API.
Task representation
Ordinary get/list/action responses contain exactly these required v2 fields:
id, queue, status, name, args, metadata, priority, attempt, max_attempts,
routes, result, last_error, last_route, created_at, updated_at, started_at,
finished_at
Active run_id and lease expiry appear only in Worker protocol responses, never
in the ordinary Task resource.
Errors
Every documented application error uses one envelope:
{
"error": {
"code": "stale_run",
"message": "This run is no longer active.",
"details": {}
}
}
Branch on error.code, not message. The stable status classes are:
| HTTP status | Meaning and representative codes |
|---|---|
401 |
Missing or incorrect token: unauthorized. |
404 |
Missing Queue or Task: queue_not_found, task_not_found. |
409 |
State or ownership conflict: task_id_conflict, task_running, stale_run, run_finalized, lifecycle/update conflicts. |
413 |
Body exceeds 1 MiB: request_too_large. |
422 |
Invalid request, Task, filter, update, identifier, or complete stored data: for example invalid_task, invalid_filter, invalid_update, task_data_too_large. |
503 |
Retryable Server-side operational failure such as database_busy; still uses the same envelope. |
Malformed FastAPI/Pydantic native errors are converted to this envelope; Server tracebacks and database details are never part of the public response.
Client retry boundary
Safe reads and representation-idempotent Task creation can be retried with the same inputs. Lifecycle, update, and delete calls should not be retried blindly after an uncertain response because another actor may have changed the resource. Inspect current state before deciding.
Workers use a separate reliability policy: they keep heartbeat active while
retrying one terminal report, and run_id fencing prevents a stale retry from
overwriting a newer execution.