# Get Steps

- **Method:** `GET`
- **Path:** `/api/v1/runs/{run_id}/sessions/{session_id}/steps`
- **Operation ID:** `get_steps`
- **Tags:** sessions

One session's steps, both halves aligned: what the persona decided and said, beside what the browser
did and saw - url, inputs, dialogs, downloads, network failures, console errors, screenshot and video
window.

Readable while the session runs. Page through a long session with `from` and `to`, or tail a live one by
passing the previous answer's `last_step_id` as `after` until `status` is no longer `in_flight`.

## Effective servers

- `https://api.saram.sh`

  Saram

## Authentication

- **HTTPBearer**: HTTP bearer

  a Saram API key (`sk_saram_<env>_...`) or a person's AuthKit access token, as `Authorization: Bearer <credential>`. Both are told apart by shape and produce the same principal.

## Path parameters

- **`run_id` (required)**: `string`
- **`session_id` (required)**: `string`

## Query parameters

- **`from`**: `integer | null`, minimum: `0`

  Inclusive lower bound on step\_id. Mutually exclusive with after, which is the same bound minus one.
- **`to`**: `integer | null`, minimum: `0`
- **`after`**: `integer | null`, minimum: `0`

  Exclusive lower bound: return the steps written after this step\_id. The cursor for tailing a session that is still running - pass back the last\_step\_id of the previous page. Mutually exclusive with from.

## Responses

### 200 Successful Response

**Content type:** `application/json`

schema: `StepsPage`, no additional properties

A session's steps, aligned. Readable while the session runs, not only after it ends.

One step is written as the persona takes it, so a caller polling `?after=<last_step_id>` tails a live
session: each call returns the steps written since the last one, and `status` says whether more are coming.
`heartbeat_step` beside it is where the browser has got to, which is at or one ahead of `last_step_id` while
a step is in flight.

- **`mission_id` (required)**: `string`
- **`persona_id` (required)**: `string`
- **`run_id` (required)**: `string`
- **`session_id` (required)**: `string`
- **`status` (required)**: `string`

  `in_flight` while the session has not been recorded yet: the steps below are what the persona and the
  browser have written so far, both halves joined, and more will follow. The one thing a live page lacks is the
  session video, which is stored when the browser closes: `video_ref` and each step's window are there,
  `video_url` is absent until the page is final. A step whose surface half is missing on a live page is one
  the browser has not answered yet or a real gap, as on a final one. Otherwise the session's own
  status (`completed`, `abandoned`, `blocked`, `errored`, `budget_exhausted`, `cancelled`), and the page is
  final.
- **`steps` (required)**: `array`

  **Items:**

  schema: `AlignedStep`, no additional properties

  One step, both halves: what the persona decided (the agent half) and what the browser did (the surface
  half), joined on `step_id`. A half that is missing is flagged (`has_agent`, `has_surface`), not invented; the
  trailing `abandon`/`complete` step is agent-only by design (`terminal`).

  On an `in_flight` page (`StepsPage.status`) both halves are there for every step the browser has answered.
  What waits for the session to end is the video, which is stored when the browser closes: a live step carries
  its `video_ref` and window but no `video_url` yet.
  - **`has_agent` (required)**: `boolean`
  - **`has_surface` (required)**: `boolean`
  - **`step_id` (required)**: `integer`
  - **`terminal` (required)**: `boolean`
  - **`action_args`**: `object`
  - **`action_type`**: `string | null`
  - **`console_errors`**: `array of object`
  - **`dialogs`**: `array of object`

    Native dialogs the page opened, or that were answered, at this step (`surface_trace.dialogs`): type,
    message verbatim, and how the step left it - `pending` on the step that opened one, `accept`, `dismiss` or
    `auto_accepted_beforeunload` on the step that answered it. A dialog floats above the page and is in no
    screenshot, so this is the only place a reader sees it. Empty when the pool saw none.
  - **`dom_mutation_count`**: `integer | null`
  - **`downloads`**: `array of object`

    Files the browser saved at this step (`surface_trace.downloads`): filename, media type, size, hash and
    the stored copy's ref when the pool kept the bytes - never a path. A download moves no pixel and no url, so
    without this the step reads as a click that did nothing. Empty when the pool saw none.
  - **`end_reason`**: `string | null`
  - **`field`**: `object | null`

    The focused text-entry control after this step, as the persona was told it (`agent_trace.field`): its
    kind, accessible name, the value it really holds (cut at 200 characters), the value's full length, and the
    hint it shows while empty - never part of the value. A password field, and any field holding one of the
    session's credentials, is `masked`: the length is there and the value is not. Present on a `click`, `type`
    or `key` step whose focus landed in a text control, on a pool that reports it; absent otherwise. On the
    agent half, so it is readable on an `in_flight` page.
  - **`files_attached`**: `array of object`

    Files the body handed to the page at this step (`surface_trace.files_attached`): name, size, media
    type, hash and whether it went through the file picker or straight into a file input - never a path. Empty
    on every step but an `upload`.
  - **`inputs`**: `array of object`

    What the body dispatched at this step (`surface_trace.inputs_dispatched`): pointer and keyboard input,
    with the selector and text each resolved onto. On a `type` the persona made with the session's credential
    (`action_args.secret`), every keyboard input's `detail.text` and `resolved_text` is `[secret]`, whatever
    the pool recorded: the password is on no step a reader is served.
  - **`latency_ms`**: `integer | null`
  - **`model`**: `string | null`
  - **`narration`**: `string | null`

    SUBJECTIVE: the persona's diary line said before this action. Quote it as the persona's, never as a
    finding.
  - **`network_failures`**: `array of object`
  - **`observation_mode`**: `string | null`
  - **`perceived_state`**: `string | null`
  - **`reasoning_summary`**: `string | null`
  - **`result`**: `string | null`

    On the terminal step, the artifact result it implies (`completed`, `abandoned`, `budget_exhausted`).
  - **`route`**: `string | null`
  - **`screenshot_ref`**: `string | null`
  - **`screenshot_url`**: `string | null`
  - **`target_description`**: `string | null`
  - **`timing`**: `object`, schema: `StepTiming`, no additional properties

    How long the browser took over one step, in milliseconds. Each is absent when the browser did not
    measure it.
    - **`action_to_settle_ms`**: `integer | null`
    - **`longest_task_ms`**: `integer | null`
    - **`navigation_ms`**: `integer | null`
  - **`tokens`**: `object | null`, schema: `StepTokens`, no additional properties

    The model tokens behind one step's decision: prompt, reply, and the part of the prompt read from the
    cache. Each is absent when the provider did not report it.
    - **`cached`**: `integer | null`
    - **`input`**: `integer | null`
    - **`output`**: `integer | null`
  - **`tolerance_state`**: `object`
  - **`ts_agent`**: `string | null`, format: `date-time`
  - **`ts_end_s`**: `number | null`
  - **`ts_start_s`**: `number | null`

    Where this step starts in the session video, seconds from its first frame.
  - **`ts_surface`**: `string | null`, format: `date-time`
  - **`url`**: `string | null`
  - **`video_ref`**: `string | null`
  - **`video_url`**: `string | null`
  - **`viewport`**: `string | null`
- **`total_steps` (required)**: `integer`

  Steps in the whole trace, before the `from`/`to` window - not the count of `steps`.
- **`agent_trace_ref`**: `string | null`
- **`first_step_id`**: `integer | null`
- **`from_step`**: `integer | null`

  The inclusive lower bound in effect, whether it came from `from` or `after` + 1.
- **`heartbeat_step`**: `integer | null`

  LIVE. The `step_id` the persona last reported: where the browser has got to, which is at or one ahead of
  `last_step_id` while a step is in flight - a step is written after the browser answers, so a step in flight
  shows here first. Only asked for while `status` is `in_flight`: absent on a final page, and on a live one
  whose live state cannot be read.
- **`last_heartbeat_at`**: `string | null`, format: `date-time`

  When that heartbeat reached the server: how stale `heartbeat_step` is. Absent whenever it is.
- **`last_step_id`**: `integer | null`

  The last step in the whole trace. On an `in_flight` page this is what a tailing caller passes back as
  `after` to get the next batch; absent means the session has written no step yet.
- **`surface_trace_ref`**: `string | null`
- **`to_step`**: `integer | null`
- **`video_ref`**: `string | null`
- **`video_url`**: `string | null`
- **`waiting_for_seat`**: `boolean | null`

  LIVE. `true` while the session is queued for a browser seat, which is why a live page can have no steps at
  all: every seat in the pool is taken, and it takes no steps until one frees. A session resuming after a pause
  or a pool failure queues the same way, so it can be `true` on a page that has steps. Only asked for while
  `status` is `in_flight`: absent on a final page, and on a live one whose live state cannot be read.
- **`waiting_since`**: `string | null`, format: `date-time`

  When the session was queued for the seat it is waiting for: how long it has waited. Absent whenever
  `waiting_for_seat` is not `true`.
- **`workflow_id`**: `string | null`

  The session's workflow id, as `SessionRow.workflow_id`: what `heartbeat_step` was read off.

**Example:**

```json
{
  "run_id": "",
  "session_id": "",
  "persona_id": "",
  "mission_id": "",
  "status": "",
  "heartbeat_step": 1,
  "last_heartbeat_at": "",
  "waiting_for_seat": true,
  "waiting_since": "",
  "workflow_id": "",
  "from_step": 1,
  "to_step": 1,
  "total_steps": 1,
  "first_step_id": 1,
  "last_step_id": 1,
  "agent_trace_ref": "",
  "surface_trace_ref": "",
  "video_ref": "",
  "video_url": "",
  "steps": [
    {
      "step_id": 1,
      "has_agent": true,
      "has_surface": true,
      "terminal": true,
      "ts_agent": "",
      "action_type": "",
      "target_description": "",
      "action_args": {
        "additionalProperty": "anything"
      },
      "narration": "",
      "reasoning_summary": "",
      "perceived_state": "",
      "observation_mode": "",
      "tolerance_state": {
        "additionalProperty": "anything"
      },
      "latency_ms": 1,
      "tokens": {
        "input": 1,
        "output": 1,
        "cached": 1
      },
      "model": "",
      "end_reason": "",
      "result": "",
      "field": {
        "additionalProperty": "anything"
      },
      "ts_surface": "",
      "url": "",
      "route": "",
      "viewport": "",
      "inputs": [
        {
          "additionalProperty": "anything"
        }
      ],
      "files_attached": [
        {
          "additionalProperty": "anything"
        }
      ],
      "dialogs": [
        {
          "additionalProperty": "anything"
        }
      ],
      "downloads": [
        {
          "additionalProperty": "anything"
        }
      ],
      "network_failures": [
        {
          "additionalProperty": "anything"
        }
      ],
      "console_errors": [
        {
          "additionalProperty": "anything"
        }
      ],
      "dom_mutation_count": 1,
      "timing": {
        "action_to_settle_ms": 1,
        "longest_task_ms": 1,
        "navigation_ms": 1
      },
      "screenshot_ref": "",
      "screenshot_url": "",
      "video_ref": "",
      "video_url": "",
      "ts_start_s": 1,
      "ts_end_s": 1
    }
  ]
}
```

### 403 the API key lacks the required scope

### 422 Validation Error

**Content type:** `application/json`

schema: `HTTPValidationError`

- **`detail`**: `array`

  **Items:**

  schema: `ValidationError`
  - **`loc` (required)**: `array of (string | integer)`
  - **`msg` (required)**: `string`
  - **`type` (required)**: `string`
  - **`ctx`**: `object`
  - **`input`**: `any`

**Example:**

```json
{
  "detail": [
    {
      "loc": [
        ""
      ],
      "msg": "",
      "type": "",
      "input": null,
      "ctx": {}
    }
  ]
}
```

## Schemas

- `AlignedStep` — shown above.
- `HTTPValidationError` — shown above.
- `StepTiming` — shown above.
- `StepTokens` — shown above.
- `StepsPage` — shown above.
- `ValidationError` — shown above.
