Agents API
The agents API is project-scoped: list, inspect health, mint and revoke. There is no token rotation endpoint and no rotate-in-place operation. Replacing a credential means revoking the old agent and minting a new row, ID, and token.
Revocation uses the DELETE route but retains the agent row as a durable tombstone.
It sets revoked_at and overwrites the token hash. The database change commits
before the brain makes a best-effort attempt to kick active
sockets locally and across replicas. Subsequent authentication with the old
token fails even if a kick is delayed or lost. Worker control happens through the commands API (restart-worker, pool-resize, etc.).
Archiving the agent's project (DELETE /api/v1/projects/{slug}) disconnects
its agents through the same kick, with the same 4003 close, but leaves the
agent rows and tokens intact. While the project is archived a hello from one of
its agents is refused with 4401 and the long-poll routes answer 403 with
error: project_inactive; once the project is active again the agents
reconnect with their existing tokens.
List agents
Section titled “List agents”GET /api/v1/projects/{slug}/agentsRole: viewer. Returns a list of AgentPublic:
[ { "id": "...", "project_id": "...", "name": "web-01", "state": "online", "protocol_version": "2", "framework_adapter": "django", "engine_adapters": ["celery"], "scheduler_adapters": ["celery-beat"], "capabilities": {}, "last_seen_at": "...", "last_connect_at": "...", "created_at": "...", "is_outdated": false, "host_name": "web-01.internal", "agent_version": "...", "version_status": "current" }]is_outdated is true when the agent connected at least once and its last advertised protocol_version is older than the brain's CURRENT_PROTOCOL. Never-connected agents report false.
host_name is the operator-supplied label the agent sends in its hello frame (Z4J_AGENT_NAME), distinct from the mint-time name; null when the agent never set one. agent_version is the z4j-core version the agent advertised in its hello frame; null when it has never connected or did not report one. version_status compares agent_version with the brain's versions snapshot: current, outdated (older, same major), newer_than_known (the brain's snapshot is stale; refresh from Settings, Check for updates), incompatible (major version mismatch) or unknown (no version reported, or the package is missing from the snapshot); null when agent_version is null.
Mint agent (returns token + HMAC secret)
Section titled “Mint agent (returns token + HMAC secret)”POST /api/v1/projects/{slug}/agentsRole: admin. CSRF-protected and requires a fresh MFA verification. The fresh
MFA gate makes minting browser-session-only; an API key is rejected.
{"name": "billing-worker-02"}(project_id, name) is unique among live agents; a duplicate returns 409
with "error": "conflict". Response (shown once -- save both):
{ "agent": { /* AgentPublic */ }, "token": "<43 URL-safe base64 characters; no prefix>", "hmac_secret": "<urlsafe-base64, 32 raw bytes>"}The hmac_secret is the per-project signing key. It is HMAC-derived from the
current brain master secret and is not persisted by the brain. Consequently,
rotating Z4J_SECRET requires re-credentialing every agent. Adding the old
master to Z4J_PREVIOUS_SECRETS keeps the old bearer token acceptable during
the rotation window, but does not make an old hmac_secret valid: the
handshake can succeed and the first signed data frame then fails HMAC.
Operators paste both values into the agent configuration; the agent refuses to
start without hmac_secret.
Revoke agent
Section titled “Revoke agent”DELETE /api/v1/projects/{slug}/agents/{agent_id}Role: admin. CSRF-protected and requires fresh MFA. Soft-revokes the token
and retains the agent row because events.agent_id is non-null and uses
ON DELETE RESTRICT; tasks have no agent foreign key. The tombstone is hidden
from the agent list and cannot reconnect or receive new work. Revocation keeps
the original name initially. Minting the same name later moves that tombstone
into a reserved namespace under its row lock, then inserts a new agent row.
The replacement does not reuse the revoked row or its ID.
Agent health
Section titled “Agent health”GET /api/v1/projects/{slug}/agents/{agent_id}/healthRole: viewer. Returns at most 100 retained status samples, newest first, with
id, captured_at, worker_id and telemetry_loss. captured_at is the time
the agent signed the sample for sending, not the time its counters were read, so
a sample buffered during an outage carries its delivery time. Revoked or foreign
agents return 404. telemetry_loss: null means that sample does not provide supported
loss accounting. An empty list is not a zero-loss measurement.
Counters are cumulative within their buffer_id or runtime_id scope. Do not
sum samples or treat the bounded history as a complete fleet inventory. See
agent telemetry loss for counter
units, retention and operational interpretation.
List agent workers
Section titled “List agent workers”GET /api/v1/projects/{slug}/agent-workersRole: viewer. Lists the z4j agent processes (web, task, scheduler, beat)
registered through the WebSocket handshake; this is distinct from the
engine-native workers at GET /api/v1/projects/{slug}/workers, and long-poll
agents do not register here. Query: ?state= (online or offline; default
all; other values fail request validation with 422), ?role= (web, task,
scheduler, beat or other; default all; other values fail request
validation with 422) and ?limit= (default 200, 1 to 500). Rows come online
before offline, then by last_seen_at descending. This route is
session-cookie only: its tag has no API-key scope mapping, so every API key
gets 403, even one granted admin:*.
Returns a list of AgentWorkerPublic:
[ { "id": "...", "agent_id": "...", "project_id": "...", "worker_id": "...", "role": "task", "framework": "django", "pid": 12400, "started_at": "...", "state": "online", "last_seen_at": "...", "last_connect_at": "...", "created_at": "...", "updated_at": "..." }]worker_id, role, framework, pid, started_at, last_seen_at and
last_connect_at can be null. id stays stable while the same
(agent_id, worker_id) pair is refreshed; a regenerated worker_id creates a
new row and the previous row remains as historical state.