`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).
const url = 'https://example.com/api/workspaces/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/backlog/dispatch';const options = { method: 'POST', headers: { cookie: 'supacloud_session=<supacloud_session>', 'Content-Type': 'application/json' }, body: '{"autonomy_level":1,"dry_run":true}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}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— RBACCreateRun(consistent with the launch path; a preview is a dispatch-shaped action). Returns a [DispatchDryRunReport] WITHOUT launching anything.dry_run = false— RBACCreateRun. Callsdispatch_workspace_backlog(every gate enforced there) and returns{launched}. Writes anaudit_eventsrow (workspace/backlog_dispatch_manual).
A missing/empty body deserializes to the Default (dry_run = true), so a
bodiless POST is the preview.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Workspace ID to preview or dispatch
Request Bodyrequired
Section titled “Request Bodyrequired”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
Examplegenerated
{ "autonomy_level": 1, "dry_run": true}Responses
Section titled “Responses”Dry-run preview (DispatchDryRunReport) or the launched count ({launched})
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
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).
The effort ceiling a near-cap window would impose on the next dispatch
('minimal' | 'low'); None when no downshift applies (healthy window).
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.
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.
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.
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.
#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
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).
Providers whose chosen credential is at/over its fill target (near exhaustion) — surfaced so the usage panel can name them. Empty when none.
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.
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).
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).
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
object
The resolved credential regime the workspace’s autodev launches under
('subscription' | 'api_key' | 'system'); None when no project/credential
resolves (an empty workspace).
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.
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.
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).
Weekly window fill fraction [0.0, 1.0] for the chosen subscription
credential; None for an API-key/system regime or an unmetered credential.
The real-dispatch response: how many runs were launched by
dispatch_workspace_backlog.
object
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
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
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Authentication required
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
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Permission denied
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
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Structured server error
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
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}