Skip to main content

Admin API

All endpoints are under /_api/admin/ and require a JWT with the admin role. Unauthenticated requests get 401; authenticated requests without the admin role get 403.

Every state-changing action appends a row to forge_admin_audit. Read-only list/inspect endpoints do not audit.

Authentication

Authorization: Bearer <jwt-with-admin-role>

Error shape

Errors from admin endpoints use a flat envelope, not the RPC envelope:

{ "error": "not_found", "message": "Job not found" }

Endpoint reference

MethodPathPurpose
GET/_api/admin/jobsList jobs
GET/_api/admin/jobs/{id}Get job detail
POST/_api/admin/jobs/{id}/cancelCancel a job
POST/_api/admin/jobs/{id}/retryRetry a failed/cancelled job
POST/_api/admin/jobs/{id}/force-abortForce-abort any non-completed job
GET/_api/admin/workflowsList workflow runs
GET/_api/admin/workflows/{id}Get workflow run detail
POST/_api/admin/workflows/{id}/cancelCancel a workflow run
POST/_api/admin/workflows/{id}/retryRetry a failed/blocked workflow
POST/_api/admin/workflows/{id}/force-abortForce-abort any non-completed workflow
GET/_api/admin/queuesList job queues with stats
POST/_api/admin/queues/{name}/pausePause a queue
POST/_api/admin/queues/{name}/resumeResume a paused queue
GET/_api/admin/nodesList cluster nodes
GET/_api/admin/leadersList current leader elections

Jobs

List jobs

GET /_api/admin/jobs?status=failed&job_type=send_email&limit=50
Authorization: Bearer <jwt>

Query parameters (all optional):

ParameterTypeDescription
statusstringFilter by status (pending, running, failed, dead_letter, cancelled, completed)
job_typestringFilter by job type name
limitintegerMax results, 1–1000. Default 100.

Response:

{
"items": [
{
"id": "550e8400-...",
"job_type": "send_email",
"status": "failed",
"priority": 0,
"attempts": 3,
"max_attempts": 3,
"worker_capability": null,
"owner_subject": "user-uuid",
"scheduled_at": "2026-05-18T10:00:00Z",
"created_at": "2026-05-18T09:59:00Z",
"claimed_at": "2026-05-18T10:00:01Z",
"started_at": "2026-05-18T10:00:02Z",
"completed_at": null,
"failed_at": "2026-05-18T10:00:05Z",
"cancelled_at": null,
"cancel_reason": null,
"last_error": "SMTP timeout"
}
]
}

Get job

GET /_api/admin/jobs/{id}
Authorization: Bearer <jwt>

Returns the full job record, including input, output, progress_percent, progress_message, worker_id, idempotency_key, last_heartbeat, expires_at, and metadata fields not present in the list response. Returns 404 if not found.

Cancel job

POST /_api/admin/jobs/{id}/cancel
Content-Type: application/json
Authorization: Bearer <jwt>

{ "reason": "duplicate submission" }

The body is optional. Running jobs transition to cancel_requested; the worker finalizes them. Non-running, non-terminal jobs go straight to cancelled.

Response:

{
"action": "cancel",
"target_type": "job",
"target_id": "550e8400-...",
"accepted": true
}

accepted: false means the job was already in a terminal state.

Retry job

POST /_api/admin/jobs/{id}/retry
Content-Type: application/json
Authorization: Bearer <jwt>

{ "reason": "transient infra failure" }

Resets the job to pending with attempts = 0. Only valid for jobs in failed, dead_letter, or cancelled status. Wakes the job worker immediately.

Force-abort job

POST /_api/admin/jobs/{id}/force-abort
Content-Type: application/json
Authorization: Bearer <jwt>

{ "reason": "runaway job" }

Forces any non-completed, non-cancelled job — including running — to cancelled. Use when cancel is too slow. The running worker task may still be alive until it checks the cancellation flag.


Workflows

List workflow runs

GET /_api/admin/workflows?status=blocked_signature_mismatch&workflow_name=onboard_user&limit=25
Authorization: Bearer <jwt>

Query parameters (all optional):

ParameterTypeDescription
statusstringFilter by status (see workflow statuses below)
workflow_namestringFilter by workflow name
limitintegerMax results, 1–1000. Default 100.

Workflow statuses: pending, running, sleeping, waiting, completed, failed, blocked_missing_version, blocked_signature_mismatch, blocked_missing_handler. The DB may also contain legacy strings cancelled_by_operator and compensated, which resolve to failed at the Rust level.

Response:

{
"items": [
{
"id": "550e8400-...",
"workflow_name": "onboard_user",
"workflow_version": "1",
"status": "failed",
"owner_subject": "user-uuid",
"started_at": "2026-05-18T09:00:00Z",
"completed_at": null,
"wake_at": null,
"waiting_for_event": null,
"cancel_requested_at": null,
"cancel_reason": null,
"error": "step send_email: SMTP timeout"
}
]
}

Get workflow run

GET /_api/admin/workflows/{id}
Authorization: Bearer <jwt>

Returns the full run record including input, output, workflow_signature, blocking_reason, resolution_reason, current_step, trace_id, suspended_at, event_timeout_at, tenant_id, and metadata. Returns 404 if not found.

Cancel workflow

POST /_api/admin/workflows/{id}/cancel
Content-Type: application/json
Authorization: Bearer <jwt>

{ "reason": "user requested deletion" }

Sets cancel_requested_at; a DB trigger fires NOTIFY forge_workflow_wakeup so the scheduler picks it up within ~50 ms. Valid for runs in pending, running, sleeping, or waiting status.

Retry workflow

POST /_api/admin/workflows/{id}/retry
Content-Type: application/json
Authorization: Bearer <jwt>

{ "reason": "handler redeployed" }

Resets the run to pending. Valid for terminal or blocked statuses: failed, blocked_missing_version, blocked_signature_mismatch, blocked_missing_handler. This also covers legacy DB strings cancelled_by_operator and compensated, which are treated as failed. Already-completed steps are preserved and skipped on resume.

Force-abort workflow

POST /_api/admin/workflows/{id}/force-abort
Content-Type: application/json
Authorization: Bearer <jwt>

{ "reason": "stuck run" }

Immediately marks the run cancelled_by_operator regardless of current status (except already-completed and already-cancelled_by_operator). Does not wait for in-progress steps to finish.


Queues

List queues

GET /_api/admin/queues
Authorization: Bearer <jwt>

Returns stats for all job queues, including the canonical default, workflows, and cron queues even when empty.

{
"items": [
{
"name": "default",
"pending": 12,
"running": 3,
"paused": false,
"paused_at": null,
"paused_by": null,
"reason": null
},
{
"name": "emails",
"pending": 0,
"running": 0,
"paused": true,
"paused_at": "2026-05-18T08:00:00Z",
"paused_by": "admin-user-uuid",
"reason": "SMTP outage"
}
]
}

Pause queue

POST /_api/admin/queues/{name}/pause
Content-Type: application/json
Authorization: Bearer <jwt>

{ "reason": "SMTP outage" }

Workers stop picking up new jobs from the named queue. In-progress jobs continue to completion. Pausing an already-paused queue updates the paused_by and reason fields.

Resume queue

POST /_api/admin/queues/{name}/resume
Authorization: Bearer <jwt>

No body. Removes the queue from forge_paused_queues and sends a NOTIFY forge_jobs_available to wake workers immediately.

accepted: false means the queue was not paused.


Cluster

List nodes

GET /_api/admin/nodes
Authorization: Bearer <jwt>
{
"items": [
{
"id": "550e8400-...",
"hostname": "forge-1",
"ip_address": "10.0.0.1",
"http_port": 8080,
"grpc_port": null,
"roles": ["gateway", "worker"],
"worker_capabilities": ["default", "emails"],
"status": "healthy",
"version": "0.9.0",
"current_connections": 42,
"current_jobs": 3,
"cpu_usage": 0.23,
"memory_usage": 0.41,
"started_at": "2026-05-18T00:00:00Z",
"last_heartbeat": "2026-05-18T10:05:00Z"
}
]
}

List leaders

GET /_api/admin/leaders
Authorization: Bearer <jwt>

Shows the current advisory-lock leader for each leadership role (cron, cluster coordinator, etc.).

{
"items": [
{
"role": "cron",
"node_id": "550e8400-...",
"acquired_at": "2026-05-18T09:00:00Z",
"lease_until": "2026-05-18T09:01:00Z"
}
]
}