Operations
An operation is a unit of work running on a fleet. You create work through a plan: a reviewable, structured request that you validate and then deploy into a real operation. This is the surface an automation or LLM agent uses to put robots to work.
What it represents
The plan lifecycle separates intent from execution so work can be reviewed before it runs:
draft -> validating -> valid | invalid -> deploying -> deployed | failed
- Create a plan from structured input. Nothing is dispatched yet.
- Validate it: a dry run that resolves the referenced features and robots and reports whether the plan is deployable.
- Deploy a valid plan. It becomes a running operation through the same dispatch path the web app uses.
Once deployed, you can read live feedback and progress for the operation.
Authentication
Operation endpoints are called with an API key. Each step needs its own permission:
| Action | Permission |
|---|---|
| Create a plan | operations:draft |
| Validate a plan | operations:validate |
| Read a plan, feedback, or progress | operations:read |
| Deploy a plan | operations:deploy |
| Deploy without human approval | operations:deploy_without_human_approval |
| Stop, resume, delete, or stop-and-reset an operation | operations:control |
| Adjust a running operation (abort missions, reset a step, add or remove robots, assign a spot, set priorities) | operations:control |
| Create or delete scheduled commands | operations:control |
See Authentication. The create, validate, and deploy
endpoints require an Idempotency-Key header (see
Idempotency).
Plan types
A plan's plan_request declares a type and a payload. The supported types
are:
| Type | Use |
|---|---|
haul_job |
Move material from a load location to a dump location with one or more robots. |
route_series_job |
Run a series of routes. |
advanced_job |
A general job defined as an explicit sequence of steps. |
The payload fields depend on the type. Reference features and robots by id;
discover those ids through Features and Robots.
Advanced job steps
An advanced_job payload is an explicit sequence of steps you stitch together
yourself:
{
"name": "Inspect and park",
"robots": { "mode": "capabilities", "capabilities": ["inspect"] },
"steps": [
{
"name": "Inspect",
"capabilities": ["inspect"],
"reentryRule": "always",
"location": { "mode": "set", "layerId": "feat-route", "layerType": "route" },
"stepCompletionMode": "automatic"
},
{
"name": "Park",
"reentryRule": { "afterAnyOf": ["Inspect"] },
"location": { "mode": "dynamic" },
"stepCompletionMode": "operatorRelease",
"decompositionPolicy": {
"geometryPolicy": "exclusive",
"simultaneousCapacity": 1,
"createsDemand": true
}
}
]
}
Key per-step fields:
name(required): the step's display name. Other steps reference it inreentryRule.capabilities: capabilities a robot must have to run the step.reentryRule: when a robot may run the step again. One of"always","afterDifferentStep", or{ "afterAnyOf": ["<stepName>", …] }.location: where the work happens. Either{ "mode": "dynamic" }(the area is filled in at runtime, for example byassign-spot) or{ "mode": "set", "layerId": "<featureId>", "layerType": "shape" | "route" }.stepCompletionMode:"automatic"or"operatorRelease".geometrySelection: how the next sub-area is chosen. One of"first","roundRobin","random","nearest","farthest","leastRecentlyUsed".totalLimit: optional cap on how many times the step may run.decompositionPolicy: how this step's area is divided and managed (see below).
decompositionPolicy
Shapes how a step's location is divided among robots. Send it for dynamic steps
to configure the area they build up at runtime; omit it for set steps that point
at a feature which already has its decomposition configured through
Configure how a feature is divided.
{
"geometryPolicy": "exclusive",
"simultaneousCapacity": 1,
"createsDemand": true,
"spacing": 1.0,
"completionAreaType": "coverage",
"coverageMethod": "hexagonal"
}
Every field is optional:
geometryPolicy:"consumable","sharedStatic", or"exclusive".simultaneousCapacity: how many robots may hold the area at once.createsDemand(defaultfalse): whether the step actively pulls in robots while its area still has open capacity. By default, only consumable areas (or steps with atotalLimit) request robots, so anexclusiveorsharedStaticstep stays idle until a robot reaches it through the step sequence. SetcreatesDemandtotrueto have the step draw in an available robot whenever a slot is open. This is useful fordynamicexclusivespots such as charging bays, parking spots, or staging queues that should fill as capacity frees up.spacing: spacing in meters when dividing an area into points.completionAreaType: the area type a completed sub-area becomes.coverageMethod:"hexagonal"or"square".
Create a plan
POST /v1/orgs/{org_id}/fleets/{fleet_id}/plans
Creates a reviewable plan. Does not deploy.
Request body:
{
"original_user_text": "Haul from the pit to the stockpile with three trucks.",
"source": { "kind": "llm_agent", "client_name": "ops_assistant", "model": "…" },
"plan_request": {
"type": "haul_job",
"payload": {
"name": "Pit to Stockpile",
"robots": { "mode": "capabilities", "capabilities": ["haul"] },
"queueLayerId": "feat-queue",
"loadLocation": { "mode": "set", "layerId": "feat-load", "layerType": "shape" },
"dumpLocation": { "mode": "set", "layerId": "feat-dump", "layerType": "shape" }
},
"options": {}
}
}
plan_request(required): thetypeandpayloadfor the work.original_user_text(optional): the natural-language request that produced the plan, kept for review and audit.source(optional): describes the client that created the plan.
Response (201 Created):
{
"plan_id": "plan-uuid",
"status": "draft",
"operation_type": "haul_job",
"summary": { "title": "Pit to Stockpile", "description": "Haul from the pit…" },
"refs": { "feature_refs": ["feat-load", "feat-dump"], "robot_refs": [], "operation_ref": null, "mission_refs": [] },
"review": { "requires_human_approval": true },
"actor": { "type": "api_key", "id": null, "api_key_id": "key-uuid" },
"created_at": "2026-06-18T17:00:00Z",
"updated_at": "2026-06-18T17:00:00Z"
}
Sending the same Idempotency-Key again with an identical body returns the
original plan instead of creating a second one. A different body with the same
key returns 409.
Get a plan
GET /v1/orgs/{org_id}/fleets/{fleet_id}/plans/{plan_id}
Returns the current plan, including its status, references, the latest validation result, and the deploy result once deployed.
Validate a plan
POST /v1/orgs/{org_id}/fleets/{fleet_id}/plans/{plan_id}/validate
Performs a dry-run translation: resolves the referenced features and robots and builds the step sequence, without dispatching anything. Re-validating is safe and returns the same result.
Response when the plan is deployable:
{
"plan_id": "plan-uuid",
"status": "valid",
"deployable": true,
"issues": [],
"warnings": [
{
"severity": "warning",
"code": "robot_availability_uncertain",
"message": "Robot availability may change before deployment.",
"path": "/plan_request/payload/robots"
}
],
"computed": { "estimated_robot_count": 3, "estimated_mission_count": 4 }
}
Response when the plan cannot be deployed:
{
"plan_id": "plan-uuid",
"status": "invalid",
"deployable": false,
"issues": [
{
"severity": "error",
"code": "feature_not_found",
"message": "Referenced feature could not be resolved.",
"path": "/plan_request/payload"
}
],
"warnings": []
}
Each issue carries a stable code and a JSON-pointer path so a client can
branch without parsing the message. Possible codes include missing_plan_type,
invalid_plan_type, missing_payload, invalid_payload, no_steps,
feature_not_found, no_robots_selected, and conversion_failed. A plan is
deployable only when there are no error-severity issues.
Deploy a plan
POST /v1/orgs/{org_id}/fleets/{fleet_id}/plans/{plan_id}/deploy
Deploys a valid plan into a running operation. The plan must have been validated successfully first.
Deployment requires human approval. Pass it in the body:
{ "approval": { "approved_by_human": true, "approval_note": "Confirmed in the UI." } }
A key that holds operations:deploy_without_human_approval may deploy without
this approval; for any other key, omitting approval returns 400.
Response:
{
"plan_id": "plan-uuid",
"operation_id": "op-uuid",
"status": "deployed",
"deployed_at": "2026-06-18T17:05:00Z"
}
Deploy never runs a plan twice: a second deploy of an already-deployed plan returns the original result.
Operation feedback
GET /v1/orgs/{org_id}/fleets/{fleet_id}/operations/{operation_id}/feedback
Returns the operation's live status built on demand from its running state.
Permission: operations:read.
Response:
{
"operation_id": "op-uuid",
"status": "running",
"name": "Pit to Stockpile",
"completion": { "totalUnits": 10 },
"metrics": { },
"steps": [ { "missionId": "m1" } ],
"robot_refs": ["a1b2…"],
"mission_refs": ["m1"],
"num_robots_total": 3,
"num_robots_active": 2,
"num_robots_idle": 1
}
If the operation is not currently running, this returns 404. If the live state
is briefly unavailable, it returns 503; retry shortly.
Operation progress
GET /v1/orgs/{org_id}/fleets/{fleet_id}/operations/{operation_id}/progress
Returns overall and per-step progress, per-robot participation, an estimated
completion time when available, and any per-robot block reasons.
Permission: operations:read.
Response:
{
"operation_id": "op-uuid",
"status": "running",
"progress": { "totalUnits": 10 },
"steps": [ { "missionId": "m1" } ],
"robot_participation": [ { "robotId": "a1b2…" } ],
"estimated_completion": { "etaMs": 123000 },
"blocked": [ { "robot_ref": "c3d4…", "reason": "waiting_for_load" } ]
}
Same 404 / 503 behavior as feedback.
Control a running operation
Four endpoints change the lifecycle of a running operation. Each acts on an
operation that belongs to the fleet in the path (an unknown operation returns
404). Permission: operations:control.
| Method and path | Action |
|---|---|
POST …/operations/{operation_id}/stop |
Stop the operation. |
POST …/operations/{operation_id}/resume |
Resume a stopped operation. |
POST …/operations/{operation_id}/delete |
Delete the operation. |
POST …/operations/{operation_id}/stop-and-reset |
Stop the operation and reset its progress. |
None of these take a request body. Each returns:
{ "success": true, "operationId": "op-uuid" }
Adjust a running operation
Beyond stopping and resuming, these endpoints change a running operation in
place. The operation must belong to the fleet in the path (404 if unknown), and
each returns { "success": true, "operationId": "…" }. Permission:
operations:control.
| Method and path | Action | Body |
|---|---|---|
POST …/operations/{operation_id}/abort-missions |
Abort the operation's in-flight missions. | none |
POST …/operations/{operation_id}/reset-step |
Reset one step's runtime state without stopping the operation. | { "stepIndex": 0 } (must be 0 or greater) |
POST …/operations/{operation_id}/robots |
Add a robot to the operation. | { "robotId": "…" } (the robot must be in the fleet) |
DELETE …/operations/{operation_id}/robots/{robot_id} |
Remove a robot from the operation. | none |
POST …/operations/{operation_id}/assign-spot |
Add a shape to a step's work area. | { "shape": <Shape>, "stepIndex": 0 } |
Set operation priorities
POST /v1/orgs/{org_id}/fleets/{fleet_id}/operation-priorities
Sets the dispatch priority of one or more operations in the fleet.
Permission: operations:control. Every listed operation must belong to the
fleet; if any does not, the whole request is rejected and nothing changes.
{
"priorities": [
{ "operationId": "op-uuid-1", "priority": 1 },
{ "operationId": "op-uuid-2", "priority": 2 }
]
}
Response:
{ "success": true, "count": 2 }
This is a fleet-level path, not nested under a single operation_id.
Scheduled commands
Scheduled commands run fleet actions on a schedule (for example starting or stopping an operation), the same way the web app schedules them. They are distinct from scheduled missions: to schedule a mission, use Schedule a mission. For all the scheduling and template endpoints in one table, see Scheduling at a glance.
Create a scheduled command
POST /v1/orgs/{org_id}/fleets/{fleet_id}/scheduled-commands
Permission: operations:control. Requires the Idempotency-Key header.
The body is a scheduled-command object describing the command and its schedule.
The organization, fleet, owner, and id are set by the server. A schedule with no
future fire time is rejected with 400. A mission command is rejected here;
schedule missions through POST …/missions instead.
Response:
{ "scheduleId": "schedule-uuid", "status": "submitted" }
List scheduled commands
GET /v1/orgs/{org_id}/fleets/{fleet_id}/scheduled-commands
Returns the fleet's scheduled commands as { "items": [ … ] }.
Permission: missions:read. Optional query params: limit (1 to 200,
default 50) and a start_time / end_time window (epoch milliseconds) matched
against each command's scheduled start.
Delete a scheduled command
DELETE /v1/orgs/{org_id}/fleets/{fleet_id}/scheduled-commands/{schedule_id}
Removes a scheduled command. The schedule must belong to the fleet in the path.
Permission: operations:control.
Response:
{ "success": true }
Activity log
GET /v1/orgs/{org_id}/fleets/{fleet_id}/audit-log
Returns the activity trail of plan and operation events for the fleet, newest
first. Permission: audit:read.
Optional filters: event_type, actor_type, actor_id, operation_id,
start_time / end_time (epoch milliseconds), limit (1 to 200, default 50),
and cursor (epoch milliseconds; returns rows older than this).
Response:
{
"items": [
{
"event_id": "evt-uuid",
"timestamp": "2026-06-18T17:05:00Z",
"event_type": "plan_deployed",
"actor": { "type": "api_key", "id": "key-uuid" },
"target": { "type": "operation", "id": "op-uuid" },
"summary": "plan_deployed (api_key)",
"metadata": {}
}
],
"next_cursor": null
}
next_cursor is the cursor to pass on the next request, or null on the final
page.