plans
The planner (M16) — a frontier model orchestrates the three layers for one task on one device through a closed step vocabulary; it can only request approvals (a human signs them), never opens a session, never names a joint. Recorded as an orchestration run.
GET /v1/plans
List plans
Plans visible to the caller (owner, creator, or org member), newest first. No plan is public.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status | query | PlanStatus | |
device_id | query | Id | |
run_id | query | Id | The plan recorded as this |
limit | query | integer | Page size. |
cursor | query | string | Opaque cursor from the previous page's |
Responses
POST /v1/plans
Plan a task for a robot (the planner makes its first decision)
{task, device, budget_usd, max_minutes} → a plan. The hub records an orchestration run (with parent_run_id lineage when given), asks the planner model for its first decision synchronously, executes what it can (a request_approval step files an approval request on the device at once) and returns the plan with its steps and timeline.
The planner's vocabulary is closed — request_approval, await_started, open_session, wait, evaluate, hand_back — and six guarantees are enforced in code on every decision: only those kinds; no step text names a joint, a torque, a velocity, a gain or a raw action; a request_approval may only name an endpoint or model the device's registered contracts allow (checked before any request is filed); nothing runs on the device's behalf before the device's own signed started; every step expires inside the plan's window; every step's cumulative cost cap lies within the budget. A violating decision is retried once with the errors quoted back, then the plan is failed and the response is 502 carrying them.
The plan never approves anything (createApproval is session-only), never opens a session (the device does) and never touches an endpoint beyond reading it. It acts as the credential that created it, narrowed to write, for the one write it makes: filing requests. Spends model dollars: key:train, and every decision — this one and each later one — counts against the coach's daily quota (429 with X-Coach-Quota-* headers when the first cannot be made).
Request body
| Field | Type | Description |
|---|---|---|
task | string | What the robot should do, in plain words. |
device | string | A device id ( |
budget_usd | number | Cap on the plan's model spend in USD; the plan ends |
max_minutes | integer | The plan's window; it hands back when it passes. Every step and every approval fits inside it. |
parent_run_id | string | null | Lineage — the run (a training run or another plan's run) this plan follows from. An unreadable one is |
Responses
| Status | Description | Body |
|---|---|---|
201 | Plan created; its first decision was made and executed as far as it could go. | Plan |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
422 | Request failed validation. | Problem |
502 | The planner's first decision failed validation twice. The plan exists, is | Problem |
503 | A service this endpoint needs is not configured in this deployment (M0's boot guarantee: the API boots with zero secrets, and an endpoint that needs one says so instead of returning a stack trace). | Problem |
GET /v1/plans/{plan_id}
Get a plan (steps, timeline, what it is waiting on)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
plan_id | path | Id | Plan id ( |
Responses
POST /v1/plans/{plan_id}/advance
Run one orchestrator tick on a plan (idempotent)
The orchestrator's beat, callable by hand. Re-reads the device's events, the endpoint session metering and the approval state; expires a step whose window passed; executes the current step when it can; and asks the model for the next decision only when no step is pending or the last one failed or expired. Takes a compare-and-set lock on the plan, so two ticks at once cannot both decide, and a tick with nothing due changes nothing. The same function runs on a schedule (the arq cron, or the API's in-process ticker when PLAN_TICK_INTERVAL_S is set), so calling this is never required. A decision made here counts against the coach's daily quota; when the account's quota is exhausted the plan hands back rather than answering 429.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
plan_id | path | Id | Plan id ( |
Responses
| Status | Description | Body |
|---|---|---|
200 | The plan after the tick (unchanged when nothing was due). | Plan |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
502 | A decision made during this tick failed validation twice; the plan is | Problem |
503 | A service this endpoint needs is not configured in this deployment (M0's boot guarantee: the API boots with zero secrets, and an endpoint that needs one says so instead of returning a stack trace). | Problem |
POST /v1/plans/{plan_id}/cancel
Cancel a plan
Ends an active plan (canceled) and its run. A pending approval request the plan filed stays for a person to deny in the web app, and an issued approval ends on its own window — the plan can request consent but cannot revoke it, and says so in its final event. 409 when the plan already ended.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
plan_id | path | Id | Plan id ( |
Responses
| Status | Description | Body |
|---|---|---|
200 | The canceled plan. | Plan |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
Schemas (9)
The schemas these operations reach before any other tag’s do. A type that links elsewhere is rendered on that tag’s page.
PlanPage
| Field | Type | Description |
|---|---|---|
items | array of Plan | |
next_cursor | string | null |
PlanCreate
| Field | Type | Description |
|---|---|---|
task | string | What the robot should do, in plain words. |
device | string | A device id ( |
budget_usd | number | Cap on the plan's model spend in USD; the plan ends |
max_minutes | integer | The plan's window; it hands back when it passes. Every step and every approval fits inside it. |
parent_run_id | string | null | Lineage — the run (a training run or another plan's run) this plan follows from. An unreadable one is |
Plan
One orchestration and its whole timeline. run_id is the kind: orchestration run it is recorded as; cost_usd is model spend metered per decision (retries included); steps are in execution order; events oldest first.
| Field | Type | Description |
|---|---|---|
id | Id | |
run_id | Id | |
parent_run_id | string | null | |
status | PlanStatus | |
status_reason | string | null | |
task | string | |
device | PlanDevice | |
owner | RepoOwner | |
created_by | UserPublic | null | |
budget_usd | number | |
cost_usd | number | Model spend so far, USD (6 decimal places). |
priced | boolean | False when the model has no rate row; usage is still recorded. |
max_minutes | integer | |
expires_at | string (date-time) | |
decisions | integer | |
max_decisions | integer | The hub's cap on decisions per plan ( |
attempts | integer | Model calls including retries. |
model | string | null | |
backend | string | null |
|
current_request_id | string | null | |
current_approval_id | string | null | |
waiting_on | PlanWaiting | null | |
steps | array of PlanStep | |
events | array of PlanEvent | |
next_tick_at | string (date-time) | null (date-time) | |
created_at | string (date-time) | |
updated_at | string (date-time) | |
finished_at | string (date-time) | null (date-time) |
PlanDevice
| Field | Type | Description |
|---|---|---|
id | string | null | Null once the device row is gone; the name stays. |
name | string |
PlanWaiting
What the plan is blocked on right now — the card the plan page shows.
| Field | Type | Description |
|---|---|---|
kind | string | |
step_id | Id | |
message | string | |
request_id | string | null | |
approval_id | string | null | |
until | string (date-time) | null (date-time) | When the waiting step expires. |
approve_in | string | Where a person approves — the web app's Devices page. No API key can. |
PlanStep
| Field | Type | Description |
|---|---|---|
id | Id | |
index | integer | Position in the plan (0-based, execution order). |
decision | integer | Which decision (1-based) proposed the step. |
kind | PlanStepKind | |
params | object | The kind-specific fields the planner set: |
reason | string | The planner's task-level reason. Never names a joint, a torque, a velocity or a raw action (enforced). |
status | PlanStepStatus | |
expires_in_s | integer | Seconds the step may stay active once it starts. |
expires_at | string (date-time) | The plan's window until the step activates, then |
cost_cap_usd | number | Cumulative model spend the plan may reach after this step; enforced before the step runs. |
started_at | string (date-time) | null (date-time) | |
finished_at | string (date-time) | null (date-time) | |
result | object | null | What happened — the request id filed, the approval seen, the session opened, the condition's observed value. |
created_at | string (date-time) |
PlanEvent
| Field | Type | Description |
|---|---|---|
id | Id | |
kind | string |
|
step_id | string | null | |
message | string | |
data | object | null | |
cost_usd | number | null | Model spend this event represents (decisions and rejections). |
created_at | string (date-time) |
PlanStepKind
The closed vocabulary. request_approval files an approval request (a human approves it); await_started waits for the approval and the device's signed started; open_session waits for the device to open its endpoint session; wait waits for seconds or an event; evaluate tests one telemetry metric; hand_back ends the plan.