Sentinel

Data operations

Control episode recording, then query and annotate the results.

An episode is one recorded attempt at a task. Episodes are created by operators in the headset, by physical buttons, or through this API — all three produce the same data.

The flow end to end: the robot records an episode and syncs it to the platform → you query and annotate it → you curate episodes into a dataset and snapshot it → you export the manifest to your training pipeline. To react to new episodes without polling, use webhooks.

Two kinds of episode data

The API keeps a hard line between what the robot recorded and what you add later:

  • Recorded facts are stamped by the robot at collection time: outcome, operator, control mode, timing, and segments. They are immutable after ingest.
  • Annotations are everything added after the fact: QA status, curation tags, corrected outcomes. They live in a separate mutable layer and never overwrite recorded facts.

Episode rows expose the two layers as separate metadata and annotations fields, so the split survives into your queries.

Control recording

POST /v1/robots/{id}/episodes/start
POST /v1/robots/{id}/episodes/finish
POST /v1/robots/{id}/episodes/fail
POST /v1/robots/{id}/episodes/discard

Requires robots:command. Each verb tells the robot to make one recording transition. The robot stamps the recording with its active task and operator context (the optional task_id/metadata body fields are reserved and not attached yet).

curl -X POST https://api-prod.avearobotics.com/v1/robots/unit-04/episodes/start \
  -H "Authorization: Bearer ak_..."
{
  "status": "ok",
  "result": {
    "cmd_id": "cmd-a1b2c3d4e5f6",
    "command": "episodes.start",
    "robot_id": "5f3a9c2e-...",
    "ok": true
  }
}

Recording states

A robot is idle, recording, or briefly finalizing while it closes a recording. Each verb is valid in exactly one state:

Robot stateValid callEffect
idlestartRecording begins
recordingfinishRecording ends with outcome success
recordingfailRecording ends with outcome failure
recordingdiscardRecording ends with outcome discarded; the recorded files are deleted, the row is kept
finalizingWait a few seconds for the robot to return to idle

A verb sent in any other state fails with 500 internal — the robot refuses the transition and nothing changes. Other errors any verb can return:

ResponseMeaning
422 validation_failedThe robot's runtime predates native episode commands (fail works on every version)
503 robot_offlineRobot is not connected
504 command_timeoutNo acknowledgment in time — the transition may still happen; retry with the same Idempotency-Key, never blind

After finish or fail, the episode row appears in GET /v1/episodes once the robot syncs the recording.

Query episodes

GET /v1/episodes
GET /v1/episodes/{id}
GET /v1/episodes/{id}/segments

Requires data:read. The list endpoint paginates with cursors and filters on:

?robot_id=     ?task_id=      ?subtask_id=    ?operator_id=
?outcome=      ?from=  ?to=   ?control_mode=
?policy_version_id=           ?eval_id=

It also takes dotted equality filters into the two JSON layers: ?metadata.building=hq matches recorded metadata, ?annotation.qa=approved matches annotations.

Episode rows carry both recorded fields (recorded_outcome, control_mode, operator_id, metadata, …) and the mutable annotations and outcome set through this API.

Segments are the episode's ordered subtask timeline — one row per subtask the operator stepped through, with segment_index, the subtask, and start/end offsets into the recording.

Sync status

Episode rows include a replicas array — the episode's cloud-sync state per destination (granted → syncing → synced → verified, with grant_id, error, and updated_at). Filter the list by it:

?sync_state=unverified|synced|verified    # &to={destination_id} scopes it to one destination

unverified matches episodes reported but not yet verified at any (or the given) destination.

Annotate episodes

PATCH /v1/episodes/{id}

Requires data:write. The body shallow-merges into the episode's annotations; a null value deletes that key. Annotations only — recorded facts cannot be modified through any endpoint.

curl -X PATCH https://api-prod.avearobotics.com/v1/episodes/3e1b... \
  -H "Authorization: Bearer ak_..." \
  -H "Content-Type: application/json" \
  -d '{ "annotations": { "qa": "approved", "curation": ["grasp-failures-study"] } }'

To correct an outcome after review:

PATCH /v1/episodes/{id}/outcome

Body: { "outcome": "failure", "reason": "operator misfiled" }. Outcome changes append to an audit log; the robot-recorded outcome is kept forever alongside the override.

Export

GET /v1/episodes/export

Requires data:read. Returns an NDJSON manifest — one row per matching episode, with data locations — for feeding training pipelines. Accepts the same filters as the list endpoint, up to limit rows per request (default 1000, max 5000); continue from the X-Next-Cursor response header until it is absent.

Datasets

A dataset is a saved episode filter that you freeze when it's time to train. It has two states:

  • Live — membership is whatever currently matches the filter, and grows as new episodes sync.
  • Snapshotted — membership is pinned to an exact episode list, permanently, so a training run is reproducible.
POST /v1/datasets
GET  /v1/datasets
GET  /v1/datasets/{id}
POST /v1/datasets/{id}/snapshot
GET  /v1/datasets/{id}/manifest

Requires data:read for reads, data:write for writes. Create one with a name and a filter (the same fields the episode list accepts, including dotted metadata.*/annotation.* keys):

curl -X POST https://api-prod.avearobotics.com/v1/datasets \
  -H "Authorization: Bearer ak_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "sort-bin-a-training-v1",
    "filter": { "task_id": "d81f42a7-...", "outcome": "success" }
  }'

What each call does depends on the dataset's state:

CallLiveSnapshotted
GET /v1/datasets/{id}Returns the dataset; episode_count is nullReturns the dataset with its pinned episode_count
POST .../snapshotPins membership now; returns snapshot_at and episode_count409 conflict — snapshotting is one-shot
GET .../manifest409 conflict — snapshot firstNDJSON: a header line with the filter, then one line per member episode; page with X-Next-Cursor

Two safe defaults apply at snapshot time: eval episodes stay out unless the filter names an eval_id (no train-on-test by accident), and discarded episodes stay out unless the filter names an outcome.

Webhooks

Coming soon — subscribe to events such as episode.finalized instead of polling the list endpoint. Until then, poll GET /v1/episodes with a ?from= filter.