Long-Running Job Queue
The job queue lets agents and API clients enqueue work that takes longer than a single HTTP request. Jobs run in the background, track progress from 0–100, and store their result in PostgreSQL when complete. The queue uses FOR UPDATE SKIP LOCKED for safe concurrent worker claiming — multiple workers can run without double-processing.
Job lifecycle
| Status | Meaning | Transitions to |
|---|---|---|
pending | In the queue, not yet claimed by a worker | running, cancelled |
running | Claimed by a worker, actively executing | completed, failed |
completed | Finished successfully; result is available | Terminal |
failed | Errored after all retries exhausted | Terminal |
cancelled | Cancelled by the client before it started running | Terminal |
API
| Endpoint | Description |
|---|---|
POST /jobs | Enqueue a new job. Returns 202 with the job ID immediately. |
GET /jobs | List jobs for the workspace. Filter by status, paginate with limit (max 200). |
GET /jobs/:id | Get a specific job — includes progress, result, and error. |
DELETE /jobs/:id | Cancel a pending job. Returns 404 if the job is already running or complete. |
Enqueuing a job
# Enqueue an agent task as a background job
curl -X POST "http://localhost:3000/jobs" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "agent_run",
"payload": {
"agentId": "research-agent",
"uid": "user_01j...",
"message": "Summarize all Slack threads from last week"
},
"maxRetries": 2,
"timeoutSeconds": 600
}'
# Response 202 Accepted:
# { "id": "job_01j...", "status": "pending" }| Field | Type | Default | Description |
|---|---|---|---|
type | string | required | Job type identifier — matched by worker handlers |
payload | object | {} | Arbitrary JSON passed to the worker |
maxRetries | int 0–10 | 3 | Retry count before status becomes failed |
timeoutSeconds | int 10–86400 | 300 | Worker must complete within this window |
Polling for results
Poll GET /jobs/:id until status is completed or failed. The progress field (0–100) is updated by the worker during execution and can be used to show a progress bar.
# Poll for job status
curl "http://localhost:3000/jobs/job_01j..." \
-H "Authorization: Bearer $TOKEN"
# While running:
# { "job": { "status": "running", "progress": 42, "workerId": "wkr_...", ... } }
# When done:
# { "job": { "status": "completed", "progress": 100, "result": { ... } } }Listing jobs
# List failed jobs
curl "http://localhost:3000/jobs?status=failed&limit=10" \
-H "Authorization: Bearer $TOKEN"Cancelling a job
Only pending jobs can be cancelled. Once a worker has claimed the job (status=running), cancellation is not supported — the worker will run to completion or timeout.
# Cancel a pending job (only works while status=pending)
curl -X DELETE "http://localhost:3000/jobs/job_01j..." \
-H "Authorization: Bearer $TOKEN"Retries
When a job fails, it is retried automatically up to maxRetries times. Each retry resets the status to pending and increments retryCount. After all retries are exhausted, the status becomes failed with the last error message stored in the error field.
Background worker
The background worker starts and stops with the gateway lifecycle. It polls the queue every few seconds using FOR UPDATE SKIP LOCKED to safely claim jobs in a multi-worker deployment.
The built-in job type is agent_run. When the worker claims an agent_run job, it calls the same agent loop as the POST /chat route — passing agentId, uid, and message from the job payload. The full response is stored in the job's result field when the turn completes. This means any agent that works via the chat API also works asynchronously via the job queue with no extra configuration.
| Job type | Required payload fields | What it does |
|---|---|---|
agent_run | agentId, uid, message | Runs one agent turn asynchronously; stores response in result |
Additional job types can be registered in src/jobs/worker.ts by adding a handler to the handlers map.
workspace_id is set automatically from the authenticated session.