Skip to main content
This is the complete contract for driving a session turn by turn. The quickstart runs one happy-path turn; this page covers the ordering rule that trips people up, how errors travel, how to index turns, and how to recover from each failure. If you follow one thing here, make it the loop order.

The loop order

Each turn hands you a decision and a session status. Act on them in this order:
  1. If action_name is non-null, execute it first, even if this turn is terminal. A non-null action is always yours to run. If executing fails, add an error block to your next turn.
  2. Then honor session_status. If it is anything other than open (completed, gave_up, closed, max_turns, spend_cap_reached, balance_exhausted), stop, after executing any action from step 1.
  3. Otherwise (open), take the next turn. If you executed an action, capture the new frame and submit at turn_index + 1. If action_name was null (an intent no_match or none_needed), execute nothing, the world is unchanged, and submit the next turn anyway. The outcome feeds back to the participant.
A terminal turn can still carry your last action. The participant often bundles their final action with done in the same response: a non-null action_name and a terminal session_status together. Checking “is the session over?” first and skipping the action silently fails any task whose last step is an action. The TV never powers off; the order never places. session_status describes the participant’s decision state, never your obligation to execute a returned action.
The participant may also split the two: the action on a continue turn, then done with a null action_name on the next turn. Both bundled and split are valid, and the ordering above handles both. Execute a non-null action, then check the status.

Errors are observations

There is no separate “executed” field on a turn. When an action you executed fails, you report it as an error observation block on the next turn, alongside the image:
A failed execution is something a person would perceive, a dead button or a blocked door, so it travels through the same channel as everything else the participant senses. One turn carries exactly one image block; the optional error block is additional, never a second image.

Turn indexing

The turn_index on a turn must equal the session’s current turn count. The first turn is 0, and every recorded turn increments it by one. Every returned turn is recorded, including no_match and none_needed turns. Drive the next index off the response’s turn_index, plus one, not off whether you executed anything. Two cases move differently:
  • A 502 decision_invalid records nothing. Retry the same turn_index with a fresh frame.
  • A 409 turn_index_mismatch hands you the index the server expected. Resubmit with turn_index set to that expected_turn_index.

Reading the trace

Read a session back to replay its decisions. The trace is a cursor page: pass after_index to get turns strictly after an index and limit to size the page (default 50, max 200). Page again with after_index set to the last index you received while has_more is true.
The model behind the page is in sessions and turns.

Idempotency and replay

The create call takes an optional Idempotency-Key header. The same key with the same body returns the existing session (200, not 201); the same key with a different body is a 409 with error_kind: idempotency_conflict. Every create on the API takes the header on the same terms, so a retried registry write is as safe as a retried session: sessions, environments, environment versions, and tasks. Each create looks its key up in its own scope, the workspace for sessions, environments, and tasks, and the parent environment for a version, so the same string on two different creates never collides. Turns have a related safety net. An exact-duplicate replay of your final turn, the same turn_index, the same observation, the same valid_actions, returns the stored decision even after the session has ended. Replay is checked before the “is the session open?” guard, so a lost response to the last turn is always safe to retry. A different body at that index is a turn_index_mismatch.

Handling errors

These are the errors the session loop itself produces. Branch on detail.error_kind, not on the status. Every kind the API returns, including the registry ones this table leaves out, is in the error reference.

Two shapes of 422

Validation errors come back in two shapes, so parse defensively:
  • A malformed request (wrong types, a missing field, an invalid observation type, a bad enum value, parameters outside the allowed subset) returns a structured envelope with an errors array of field locations:
  • A business-rule rejection raised inside the endpoint (for example “exactly one image observation block is required”, or a declared-action that is not enum-or-string) returns a plain detail:

Billing and limits

Two limits can end or block a session for cost or capacity reasons, separate from the participant’s own termination. Running out of budget. Each turn accrues credits against the organization’s credit pool, and the charge settles when the session ends. A turn that cannot be paid for is refused with a 402 and is not applied; the session becomes terminal. Three refusals, three remedies. Branch on detail.error_kind, not on the status, because the first two share one status: The session also records the reason as ended_reason, so a session you read back later still says which of the two balance_exhausted cases it was. Treat any terminal status as “stop the loop”; to continue, resolve the cause and open a new session. Session-create quota (per key). An API key has a cap on how many sessions it can open per day. A create that exceeds it returns a 429. Unlike the spend cap, this is transient: back off and retry the create later. It applies to API keys only, not to user tokens.

Open unions: tolerate unknown values

sentiment, session_status, resolution, and the observation block type are open unions. New values may be added over time. Tolerate a value you do not recognize rather than hard-failing on it. spend_cap_reached is a live example: it joined session_status after the first statuses shipped. The 11 sentiments today are Delighted, Excited, Confident, Satisfied, Relieved, Neutral, Confused, Uncertain, Anxious, Frustrated, and Disengaged, and felt_intensity is one of mild, moderate, or strong.

The reference loop

The whole contract in one place, in any language:

Author your environment

Declare actions, write labels, size frames.

Score outcomes

Judge success from your state, not the participant’s words.