Skip to content

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
  1. Create a plan from structured input. Nothing is dispatched yet.
  2. Validate it: a dry run that resolves the referenced features and robots and reports whether the plan is deployable.
  3. 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 in reentryRule.
  • 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 by assign-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 (default false): whether the step actively pulls in robots while its area still has open capacity. By default, only consumable areas (or steps with a totalLimit) request robots, so an exclusive or sharedStatic step stays idle until a robot reaches it through the step sequence. Set createsDemand to true to have the step draw in an available robot whenever a slot is open. This is useful for dynamic exclusive spots 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): the type and payload for 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.