This walks from a signed-in account to a published Worker and a cost estimate. Every request below was checked against the shipped router in apps/control/app/api/workers.py.
The last step — starting the run — does not complete as of 2026-09-10. The orchestration service now has an image and Compose services, but the joins that would let it accept a run are still being built, so a start reaches the hand-off and is refused. See Overview. Everything before it works today.

Before you start

Workers routes are operator routes on the control plane. They authenticate a human’s dashboard session; a virtual key (ob_live_…) authenticates an agent at the gateway and is never accepted here. See Control plane API for how to obtain a session token.
Environment
1

Check what your workspace can actually run

Trimmed
runnable is the answer, not availability. capabilities tells you what your own role permits — render against it rather than guessing.That response is one deployment’s answer, not the shape you should expect. Control asserts no backend by default, so a deployment that has configured nothing reports "backend": "none", "backend_readiness": "not_configured" and "runnable": false on every class, including code and browser. isolation_claim is derived from whichever backend resolved — vendor_attested_microvm for the Obol-operated E2B fleet, development_only for the Docker development backend and the demo backend — and a deployment never gets to state its own. Read what that claim does and does not cover, in full, on Environments.
2

Pick a template

Each entry is {"template": …, "availability": …}. A template gated by an environment class your deployment cannot run is returned with its gate, not hidden — render the gate instead of a start button. A coding template that must run tests, builds or installs is startable when this deployment’s code class is runnable; it is no longer blocked merely because it needs sandbox commands. Control’s availability.runnable is still the only field a start button may read.
3

Create the Worker and its Draft v1

A Worker runs as an existing Agent Passport, so agent_passport_id is required.
201
That response really is all nulls today: the router projects an operation outcome that the definitions service does not return. Recover the Worker by listing and matching on your slug — the next step does exactly that.
4

Read the draft revision

The draft arrives pre-filled with the safe defaults: a 10-minute run timeout, a 15-minute approval window, two infrastructure retries and zero ambiguous-write retries, 7-day artifact retention, concurrency of one, an allowlist network policy, an ephemeral browser session, and production writes requiring approval.
5

Publish v1

Publishing pins three things, because three different races are being refused: which draft (revision_id), its exact content (revision_digest), and the published pointer somebody else may have advanced (expected_published_revision_id, null for a first publish).revision_digest is the governed digest control computed for the draft you read. The versions route returns it as draft_governed_digest, and every revision in the list carries its own governed_digest. Send it back unchanged. Never reconstruct it: an edit made after you read the draft changes the digest, so the publish is refused rather than shipping content nobody reviewed (ADR-0130).
Test before you publish. A Worker still in draft can start a test run of its draft revision: test_mode: true on the run start. The test run’s key carries test mode, and the gateway refuses every tool call that is not read-only, so a test never makes a vendor-side write (ADR-0128). The outcome is recorded as the revision’s test_status. Publishing an untested revision succeeds, but returns the warning revision_untested.
201
The SDK reads the digest for you: see below.
6

Read the cost card

Trimmed
An unknown amount has no number in it at all — that is the contract refusing to let a missing estimate render as $0.00. The hard stop is always exact, because a run cannot be cancelled at an unknown ceiling. See Control costs.
7

Start the run

acknowledged_hard_stop must be exactly the acknowledgement you were just handed.
If the estimate moved in between, control answers 409 with the current card rather than starting a run against a ceiling you did not see:
409
The run starts against the pinned revision. Follow it with GET $WS/worker-runs/{run_id}, which returns its state, its evidence_summary (what the Worker reported, kept apart from what its receipts show) and, if it fails, a typed failure with a fault_domain and a fix action. See Debug a failed run.

The same thing with the SDK

run() fetches the estimate, calls confirm, and starts the run with the acknowledgement control just produced. On 409 estimate_stale it calls confirm again with the new card before retrying — an approval of the old ceiling is never reused for a new one. See the SDK reference.

Try it without connecting anything

A demo run uses Obol-owned demo targets: no OAuth, no card, never billed, and labelled a demo everywhere it appears. demo is set by control; a request body has no field for it.
You do not have to do steps 3 to 5 first. A demo still runs a pinned revision — a demo whose behaviour is not pinned is one nobody can reproduce — so if your workspace has no published Worker for that template, the route seeds one from the reviewed template before it starts anything, idempotently per template. The entitlement gate runs before the seed, so a workspace that may not run the template’s environment class is told so without acquiring a Worker for it. 409 demo_revision_missing remains as the backstop for a caller that reached the run path another way. The seed also creates one reserved passport per workspace, obol-workers-demo, because a Worker runs as an Agent Passport and a fresh workspace has none. It denies everything — no tools, no models, no scopes, destructive actions denied, in every environment — because a demo reaches Obol-owned fixture targets and no system of yours.