Skip to content

Tasks API

Task reads (list, detail, tree) live under /projects/{slug}/tasks. Most agent-executed mutations (retry, cancel, bulk retry, queue purge, and worker control) go through /projects/{slug}/commands so the brain can mint an audit-chained command record. Bulk delete is the brain-local exception at /projects/{slug}/tasks/bulk-delete.

GET /api/v1/projects/{slug}/tasks

Role: viewer.

Query param Type Notes
state string One of pending, received, started, success, failure, retry, revoked, rejected, unknown. Invalid and empty values return 422.
priority string Comma-separated, e.g. critical,high.
name string Literal task-name substring.
search string Literal, case-insensitive substring across name, queue, worker, and task ID.
queue string Exact match.
worker string Exact match on worker_name.
since / until RFC 3339 Bound on received_at.
cursor opaque Pagination cursor from the prior page.
limit int Request validation accepts 1..5000, then normal JSON listing clamps it to Z4J_REST_MAX_PAGE_SIZE (default 500).
format string csv, xlsx, or json. Export mode ignores cursor pagination but returns at most Z4J_TASKS_EXPORT_MAX_ROWS; CSV/JSON silently stop at that cap, while XLSX fails above its separate 25,000-row memory cap.
fields string Comma-separated projection for export.

Response (JSON mode):

{
"items": [
{
"id": "01H...",
"project_id": "...",
"engine": "celery",
"task_id": "9d2c...",
"name": "email.send",
"queue": "default",
"state": "failure",
"priority": "normal",
"args": ["<redacted>"],
"kwargs": {"to": "<email>"},
"result": null,
"exception": "ValueError: ...",
"traceback": "...",
"retry_count": 0,
"eta": null,
"received_at": "...",
"started_at": "...",
"finished_at": "...",
"runtime_ms": 142,
"worker_name": "worker-1@host",
"parent_task_id": null,
"root_task_id": null,
"tags": [],
"created_at": "...",
"updated_at": "..."
}
],
"next_cursor": "..."
}
GET /api/v1/projects/{slug}/tasks/{engine}/{task_id}

Role: viewer. Same TaskPublic shape as the list endpoint.

GET /api/v1/projects/{slug}/tasks/{engine}/{task_id}/tree

Role: viewer. Walks to the task's root via root_task_id (or treats the task itself as root if the field is null) and returns every task in the project sharing that root. Capped at 500 nodes; the response carries truncated: true when the cap kicks in.

POST /api/v1/projects/{slug}/tasks/bulk-delete

Role: admin. CSRF-protected. Throttled by the shared bulk-action bucket used by other destructive and expensive routes.

{
"filter_state": "failure",
"filter_priority": ["critical", "high"],
"filter_search": "email_%"
}

The request must select exactly one mode: a non-empty task_ids list of at most 1000 task-row UUIDs, or at least one validated filter_* field. Empty or all-null bodies, empty ID/priority lists, blank text filters, unknown state or priority values, duplicate priorities, and mixed ID/filter requests return 422. Every mode is scoped to the path project.

Filter mode accepts filter_state, filter_priority, filter_search, filter_name, filter_queue, filter_worker, filter_since, and filter_until. Terms are intersected. filter_search mirrors list-search semantics across task name, queue, worker, and task ID; filter_name searches only task names. Both are literal substring filters, so %, _, and escape characters do not broaden the selection. Filter mode deletes at most 10,000 rows in stable task-UUID order. Returns {"deleted_count": N}.

Commands (retry, cancel, bulk-retry, purge-queue)

Section titled “Commands (retry, cancel, bulk-retry, purge-queue)”

Every action that needs an agent round-trip is issued as a command. The brain inserts a row in commands, signs it, and delivers it over WebSocket or the agent long-poll transport. The agent returns signed command_ack and command_result frames over that same transport; there is no result POST endpoint.

GET /api/v1/projects/{slug}/commands?status=&cursor=&limit=

Role: viewer. status is one of pending, dispatched, completed, failed, timeout, or cancelled. An unknown filter value is currently ignored and therefore returns all statuses.

GET /api/v1/projects/{slug}/commands/{command_id}

Role: viewer.

POST /api/v1/projects/{slug}/commands/retry-task

Role: operator.

{
"agent_id": "...",
"engine": "celery",
"task_id": "9d2c...",
"idempotency_key": "optional-string",
"override_args": [],
"override_kwargs": {"foo": "bar"}
}

Supply neither override field to retry by reference, or supply both override_args and override_kwargs to replace the original inputs; a partial override is rejected. Each field has a 64 KiB serialized cap, but the whole request is also subject to the lower default HTTP body limit.

POST /api/v1/projects/{slug}/commands/cancel-task

Role: operator.

{
"agent_id": "...",
"engine": "celery",
"task_id": "9d2c...",
"idempotency_key": "optional-string"
}
POST /api/v1/projects/{slug}/commands/bulk-retry

Role: operator.

{
"agent_id": "...",
"filter": {"task_ids": ["9d2c..."], "engine": "celery"},
"max": 1000,
"idempotency_key": "optional-string"
}

This compatibility command route accepts only an explicit, non-empty task_ids selection and requires a known engine; every selected ID must belong to this project and engine. max is bounded 1..10000 and truncates the deduplicated selection to its first max IDs. Filter-only/all-matching retries return 410 and must use /api/v1/projects/{slug}/bulk-retry-requests.

POST /api/v1/projects/{slug}/commands/purge-queue

Role: admin.

{
"agent_id": "...",
"queue": "default",
"confirm_token": "<hmac of (queue_name, observed_depth)>",
"force": false,
"idempotency_key": "optional-string"
}

Clients may pass a precomputed confirm_token, or observed_depth so the brain computes the keyed token, or force=true. The HTTP route also accepts none of them and returns 202; in that case the agent refuses the command asynchronously. force=true bypasses the token/depth guards. The Celery adapter logs that bypass at CRITICAL; the RQ and Dramatiq paths do not currently make the same logging guarantee.

The same /commands router exposes five worker-control routes. Each accepted command is audit-logged and signed, then delivered to the named agent over WebSocket or long-poll.

Worker control is an operator capability, not an admin one. Only queue purge is raised to admin, because it destroys queued work. Assign operator with that in mind.

POST /api/v1/projects/{slug}/commands/restart-worker

Role: operator.

{
"agent_id": "...",
"worker_name": "celery@worker-1",
"idempotency_key": "optional-string"
}

For Celery this broadcasts pool_restart(reload=True): the parent worker stays alive while its child pool is respawned. The broadcast is fire-and-forget and requires Celery's pool-restart support; it is not proof that in-flight work was preserved. Other adapters may refuse the operation or use the guarded supervisor-based self-exit fallback.

POST /api/v1/projects/{slug}/commands/pool-resize

Role: operator.

{
"agent_id": "...",
"worker_name": "celery@worker-1",
"delta": 2,
"idempotency_key": "optional-string"
}

delta is bounded -100..100. Positive grows the pool, negative shrinks it. The agent's engine adapter translates the delta into the engine-native call (pool_grow / pool_shrink). Unsupported adapters still leave the HTTP call at 202; the command later reaches status="failed" with an error string. The wire result has status, not an ok field.

POST /api/v1/projects/{slug}/commands/add-consumer

Role: operator.

{
"agent_id": "...",
"worker_name": "celery@worker-1",
"queue": "billing",
"idempotency_key": "optional-string"
}

Tells the worker to start consuming from the named queue.

POST /api/v1/projects/{slug}/commands/cancel-consumer

Role: operator. Same body shape as add-consumer. The worker stops consuming from queue but keeps running on its remaining queues.

POST /api/v1/projects/{slug}/commands/rate-limit

Role: operator.

{
"agent_id": "...",
"task_name": "myapp.tasks.send_email",
"rate": "100/m",
"worker_name": "celery@worker-1",
"idempotency_key": "optional-string"
}

rate follows Celery's grammar: integer optionally suffixed with /s, /m, or /h; "0" clears the limit. The pattern is enforced server-side. worker_name is optional in the request schema, but omission does not currently broadcast: the dispatcher falls back to the command target id, which for this route is the task name, and sends the control command to a worker with that name. The command can therefore report success without changing the intended fleet. No CRITICAL global-throttle log or audit flag is produced on this path; pass an explicit worker name.

Every worker-control command requires an agent_id, which the agents API returns. Discover engine-native worker identifiers (for example Celery's worker@host) from GET /api/v1/projects/{slug}/workers. The separate /api/v1/projects/{slug}/agent-workers collection describes z4j agent processes, not engine-native workers; neither collection is nested under /agents.