Skip to content
Select themeSelect language

`POST /api/workspaces/{id}/backlog/dispatch` (ADR 0045 Stage 1). The body's `dry_run` defaults to `true` (a bodiless POST is a safe preview).

POST
/api/workspaces/{id}/backlog/dispatch
curl --request POST \
--url https://example.com/api/workspaces/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/backlog/dispatch \
--header 'Content-Type: application/json' \
--cookie supacloud_session=<supacloud_session> \
--data '{ "autonomy_level": 1, "dry_run": true }'
  • dry_run = true — RBAC CreateRun (consistent with the launch path; a preview is a dispatch-shaped action). Returns a [DispatchDryRunReport] WITHOUT launching anything.
  • dry_run = false — RBAC CreateRun. Calls dispatch_workspace_backlog (every gate enforced there) and returns {launched}. Writes an audit_events row (workspace / backlog_dispatch_manual).

A missing/empty body deserializes to the Default (dry_run = true), so a bodiless POST is the preview.

id
required
string format: uuid

Workspace ID to preview or dispatch

Media typeapplication/json

The dispatch body. dry_run defaults to true (a bodiless POST is a safe preview); autonomy_level is an OPTIONAL hypothetical for the dry-run preview (clamped to the resolved ceiling in the service) and is ignored on a real launch.

object
autonomy_level
integer | null format: int32
dry_run
boolean
Examplegenerated
{
"autonomy_level": 1,
"dry_run": true
}

Dry-run preview (DispatchDryRunReport) or the launched count ({launched})

Media typeapplication/json
One of:

ADR 0045 Stage 1 — the manual-dispatch DRY-RUN preview (no writes). Reports the resolved gate state the FE renders BEFORE a real dispatch: how many queued items would launch (an UPPER BOUND — per-project caps/budgets are not simulated), why dispatch is blocked (if it is), and the resolved gates.

autonomy_level_max is the resolved CEILING verbatim — on a dev / enterprise / legacy-grant-all instance it is AUTONOMY_LEVEL_CEILING (100); only a genuine free entitlement resolves it to 0. The preview reports it as-is and never special-cases 0 (Risk-Resolution: dry-run reports the resolved value verbatim).

workspace_ceiling_free is the workspace’s remaining free dispatch slots under max_backlog_concurrent (ceiling − in-flight); None = uncapped.

blocked_reason carries a human string when a gate blocks (global kill-switch off, onboarding mode, ceiling exhausted, autonomy 0, …). onboarding_mode == true MUST surface as a blocked_reason so the FE shows the graduation CTA.

ADR 0045 Stage 2a (GATE 5 — quota): the quota_* fields explain the provider-quota gate the live dispatcher applies after the autonomy gate — which credential REGIME the workspace’s autodev launches under, how much window headroom remains, whether a near-cap effort downshift would apply, and which providers are near exhaustion. All are Option/default so a pre-Stage-2 workspace (no quota snapshots) reports them as absent and the FE .passthrough() schema tolerates it.

object
autonomy_level_max
required
integer format: int64
blocked_reason
string | null
dispatched_recent

Backlog runs dispatched in the recent throughput window for THIS workspace — the owner-visible launch rate (the per-workspace counterpart to the operator console’s cross-tenant throughput gauge).

integer format: int64
downshifted_effort

The effort ceiling a near-cap window would impose on the next dispatch ('minimal' | 'low'); None when no downshift applies (healthy window).

string | null
fill_target

The operator’s target window saturation in [0.0, 1.0] (e.g. 0.85) the gate reserves against; None → the surface falls back to the Balanced default.

number | null format: double
global_dispatch_enabled
required
boolean
in_flight

In-flight backlog runs across the whole workspace right now (the workspace-ceiling denominator: backlog_item_id IS NOT NULL runs in an active status). The same figure the live dispatcher reads, so the panel’s “In flight” matches what occupies the concurrency slots.

integer format: int64
in_quiet_hours

The workspace is inside its configured daily quiet-hours window (workspace_settings.backlog_quiet_hours_*, UTC) during which DISPATCH is suppressed (scan/classify keep running). true always surfaces as a blocked_reason, mirroring the live dispatcher’s quiet-hours gate. false when no window is configured or the current time is outside it.

boolean
onboarding_mode
required
boolean
per_project

Per-project dispatch snapshot: cap / in-flight / queued / blocked-reason for each backlog-enabled project of the workspace, so the owner sees WHICH project is at its cap and WHY a queue is not draining. Built from the same effective_cap + in-flight + queued reads the dispatcher uses.

Array<object>

#532 A1 — one project’s slice of the owner dispatch observability panel: the effective fan-out cap, the current in_flight backlog-run count, the queued items waiting, and a blocked_reason when the project cannot dispatch this tick (cap reached, or a 0 cap = paused). cap/in_flight reuse the EXACT reads the live dispatcher uses (dispatch::effective_cap + count_in_progress_backlog_runs), so the panel never disagrees with the launcher. title is the project name (projects.name).

object
blocked_reason

A human reason this project cannot dispatch right now, or None when it has free slots and queued work. Distinct from the workspace-level blocked_reason — this is per-project (cap reached / paused at a 0 cap).

string | null
cap
required
integer format: int64
in_flight
required
integer format: int64
project_id
required
string format: uuid
queued
required
integer format: int64
title
required
string
providers_near_exhaustion

Providers whose chosen credential is at/over its fill target (near exhaustion) — surfaced so the usage panel can name them. Empty when none.

Array<string>
queued_items
required
integer format: int64
quota_blocked

The quota gate WOULD block the next dispatch (the window is past its fill target). Distinct from would_dispatch (which folds in every gate) so the FE can show a quota-specific “throttled” chip even when another gate is the reported blocked_reason.

boolean
quota_headroom

The minimum window headroom across the gated windows in [0.0, 1.0] — the figure the quota gate compares against the fill-target reserve. None when no regime gate applies (system / no credential).

number | null format: double
recent_routing_decisions

ADR 0045 D1 — the most-recent routing decisions for this workspace, each carrying its decision_factors JSON (why dispatched / which model chosen / why escalated). The SAME explainability the operator /routing decision-log exposes, surfaced inline in the dry-run so a user previewing a dispatch sees why the queued items routed as they did. Empty for a pre-Stage-2b workspace (no decisions logged yet).

Array<object>

ADR 0045 D1 — one routing decision surfaced in the dry-run. A trimmed, FE-stable projection of the backlog_routing_decisions log row (mig 212): the item it routed, the source path (classify|escalate|fallback|manual) and the verbatim decision_factors JSON. Mirrors the operator /routing entry so the two explainability surfaces agree on field names.

object
created_at
required
string format: date-time
decision_factors
required
object
key
additional properties
item_id
required
string format: uuid
item_title
string | null
routing_source
required
string
regime

The resolved credential regime the workspace’s autodev launches under ('subscription' | 'api_key' | 'system'); None when no project/credential resolves (an empty workspace).

string | null
rolling_5h_fill

Rolling-5h window fill fraction [0.0, 1.0] for the chosen subscription credential; None for an API-key/system regime or an unmetered credential.

number | null format: double
seconds_since_tick

Seconds since the last backlog dispatch sweep (the BACKLOG_SCAN_TICK_SECONDS cadence, distinct from the faster due-schedules loop). None before the first sweep of a just-booted process — the FE renders that as “not yet”. Lets the owner see the dispatch engine is alive and how stale the last sweep is.

integer | null format: int64
tick_interval_seconds

The scheduler tick interval in seconds (services::backlog::BACKLOG_SCAN_TICK_SECONDS) — the cadence at which dispatch is re-evaluated. Surfaced so the FE renders the real interval instead of hardcoding it (NO magic numbers on the FE).

integer format: int64
weekly_fill

Weekly window fill fraction [0.0, 1.0] for the chosen subscription credential; None for an API-key/system regime or an unmetered credential.

number | null format: double
workspace_ceiling_free
integer | null format: int64
would_dispatch
required
boolean
Examplegenerated
{
"autonomy_level_max": 1,
"blocked_reason": "example",
"dispatched_recent": 1,
"downshifted_effort": "example",
"fill_target": 1,
"global_dispatch_enabled": true,
"in_flight": 1,
"in_quiet_hours": true,
"onboarding_mode": true,
"per_project": [
{
"blocked_reason": "example",
"cap": 1,
"in_flight": 1,
"project_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"queued": 1,
"title": "example"
}
],
"providers_near_exhaustion": [
"example"
],
"queued_items": 1,
"quota_blocked": true,
"quota_headroom": 1,
"recent_routing_decisions": [
{
"created_at": "2026-04-15T12:00:00Z",
"decision_factors": {
"additionalProperty": "example"
},
"item_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"item_title": "example",
"routing_source": "example"
}
],
"regime": "example",
"rolling_5h_fill": 1,
"seconds_since_tick": 1,
"tick_interval_seconds": 1,
"weekly_fill": 1,
"workspace_ceiling_free": 1,
"would_dispatch": true
}

Structured client error

Media typeapplication/json

The canonical JSON body of every error response — the single source of truth the frontend binds to. Every AppError serializes as this exact shape, and the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is what the frontend error schema is generated from, so there is no hand-written error schema on either end.

object
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}

Authentication required

Media typeapplication/json

The canonical JSON body of every error response — the single source of truth the frontend binds to. Every AppError serializes as this exact shape, and the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is what the frontend error schema is generated from, so there is no hand-written error schema on either end.

object
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}

Permission denied

Media typeapplication/json

The canonical JSON body of every error response — the single source of truth the frontend binds to. Every AppError serializes as this exact shape, and the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is what the frontend error schema is generated from, so there is no hand-written error schema on either end.

object
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}

Structured server error

Media typeapplication/json

The canonical JSON body of every error response — the single source of truth the frontend binds to. Every AppError serializes as this exact shape, and the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is what the frontend error schema is generated from, so there is no hand-written error schema on either end.

object
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}