# Get Run

- **Method:** `GET`
- **Path:** `/api/v1/runs/{run_id}`
- **Operation ID:** `get_run`
- **Tags:** runs

One run: its status, what it has cost and billed so far, the artifact's summary once there is one,
and every session with where it has got to.

Poll this while a run is open. `status` moves `pending`, `running`, `observing`, then stops on
`completed`, `failed` or `cancelled`, and only `completed` means there are results to read. Live fields
(`billed_steps`, each session's `heartbeat_step`) climb while the run does. A run of another tenant's is a
404, the same as a run that does not exist.

## 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`

## Responses

### 200 Successful Response

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

schema: `RunDetail`, no additional properties

One run in full: everything `RunSummary` carries, plus its budgets, the files it hands its personas, its
live state while it is open, and every session.

- **`created_at` (required)**: `string`, format: `date-time`
- **`evidence_expires_at` (required)**: `string`, format: `date-time`

  When this run's step-by-step evidence is deleted: `created_at` plus the days its plan keeps evidence - 14 on
  Free, 90 on Pro, this deployment's default on a plan that sets none - or the days its plan kept when the run
  was created, whichever is longer, so a plan change never moves this date earlier and an upgrade moves it later.
  The screenshots, the video, the agent and surface step-event streams, the checkpoints and the per-session
  observation documents go at this date, in every bucket they live in. Earlier when they went earlier: a run
  whose recordings the storage had already removed is found by the retention sweep, which takes the rest of its
  evidence and records when, and that moment is this date. The run's *record* is kept and has no
  expiry: this response, its issues, its verdicts, its opportunities, the spec it ran and the assembled
  artifact. A reader that wants a screenshot after that date must keep the bytes, not the ref.
- **`run_id` (required)**: `string`
- **`run_spec_ref` (required)**: `string`
- **`status` (required)**: `string`

  `pending`, `running`, `observing`, then one terminal value. `completed` means at least one session
  reached an ending its persona got to (`completed`, `abandoned`, `blocked`, `budget_exhausted`), so there
  are results to read. `failed` covers both a run that broke - a rejected spec, a reset hook or the
  observer - and a run whose every session `errored`, which came back with no persona outcome at all;
  `status_reason` says which. `cancelled` is a `cancel` signal. Poll until the value is terminal, and treat
  only `completed` as results-ready.
- **`target` (required)**: `string`
- **`actor_cost_usd`**: `number | null`

  Of `cost_usd`, the persona loop's model calls: the sum of the priced sessions' `cost_usd`. Absent when no
  session's model had a price.
- **`artifact_ref`**: `string | null`
- **`baseline_run_id`**: `string | null`
- **`billable_usd`**: `number | null`

  What this run costs you, to the cent: `billed_steps` at your plan's step price - or your own agreed
  price - in force when the run ran. This is Saram's charge; `cost_usd` beside it is model spend and is not.
  LIVE with `billed_steps` while the run is open, so it is what the run has cost you so far. Absent means
  nobody has priced your tenant, which is not the same as free - never read a missing price as $0. Your plan's
  included steps and monthly fee are not in it: they are properties of a month, and the invoice line with the
  allowance applied is this run's `billable_usd` on `GET /api/v1/usage`.
- **`billed_steps`**: `integer`, default: `0`

  What this run is charged for: the steps its personas took - one browser action each - never the
  reproduction replay's, which are included at no charge. LIVE: a session is recorded when
  it ends, so while the run is open this is the finished sessions' counts plus each unfinished
  session's last `heartbeat_step`, and it climbs until the run does end.
- **`budgets`**: `object`
- **`caller`**: `string | null`

  Who started this run, in words: the caller name the API key gate identified at `POST /runs` - the
  operator's own name for a break-glass entry, a name like `acme/ci` for an API key. Absent when nobody was
  identified, which is the gate being off, a run an operator started in-process, or a run created before runs
  recorded their caller; the three are deliberately indistinguishable rather than dressed up with an invented
  name. Attribution, beside the authorization `tenant_id` carries.
- **`confirmation_rate`**: `number | null`

  Confirmed issues over the issues reproduction ruled on. Available as soon as reproduction finishes, which
  is before `summary.confirmation_rate` exists; the two are the same number computed from the same lineage.
- **`cost_usd`**: `number | null`

  What this run's model calls cost: `actor_cost_usd` and `judge_cost_usd` together. NOT what Saram charges
  you - `billable_usd` is that: on your own keys this is your provider's bill, and on Saram's models it is Saram's
  own cost, shown so your Saram usage and your provider's invoice add up. Written when the run finishes. Absent
  means nothing priced the models, which is not the same as free. On a run finished before the judge was priced
  it is the persona loop's alone, and `judge_cost_usd` is absent beside it.
- **`created_by`**: `string | null`

  Which credential started it: `key_...` for an API key, `operator` for a break-glass entry or a run an
  operator started in-process. The id of the key rather than its name, so rotating a key leaves the runs it
  started still pointing at it.
- **`evidence_expired`**: `boolean`, default: `false`

  Whether that date has passed. `GET /api/v1/blobs` on this run's session evidence answers 404 once it
  has, and that 404 means "expired", not "never existed" - which is why the date is published beside it.
- **`files`**: `array`

  The files the run hands its personas, in the spec's order: its `files[]`, without the rest of the spec.
  Empty for a run that names none.

  **Items:**

  schema: `RunFile`, no additional properties

  One file a run hands its personas: the run spec's `files[]` entry as the run was started with it.

  Served on `RunDetail` so the run page can list a run's handouts without the spec document itself, which also
  carries its providers, connectors and seed-account refs. Read from the run's own row, never from the store:
  the spec is immutable once the run is created, and the run row keeps the copy that was validated.
  - **`description` (required)**: `string`

    What the persona was told about it, verbatim.
  - **`name` (required)**: `string`

    What the persona calls it, and the filename the target receives. Unique within the run.
  - **`ref` (required)**: `string`

    `saram-file://<tenant_id>/<file_id>`, as `FileRecord.ref`. The file may since have been deleted or have
    expired: this is what the run was handed, not what the tenant still has.
  - **`assign`**: `object | null`, schema: `RunFileAssign`, no additional properties

    Which sessions carry it. Absent means every session.
    - **`missions`**: `(array of string) | null`

      Mission ids. Absent means any mission.
    - **`personas`**: `(array of string) | null`

      Persona addresses, as the spec's `personas` names them. Absent means any persona.
- **`finished_at`**: `string | null`, format: `date-time`
- **`held_usd`**: `number | null`

  What this run still reserves from your balance: the most its spec's budgets can use (the Usage ceiling),
  less each finished session's share. Released when the run's last session is settled. Absent when the run
  holds nothing - it has finished, or the run gate did not hold for it.
- **`judge_cost_usd`**: `number | null`

  Of `cost_usd`, the observer's model calls: the judge's reading of each session, attribution, the merge,
  and the reproduction's claim check and tie-break - a failed attempt included, because it was paid for. On
  today's models this is often the larger half. A retest you ask for later is not in it. Absent when none of
  those calls had a price, and on a run finished before they were priced.
- **`live`**: `object | null`, schema: `LiveStatus`, no additional properties

  The run's state as its workflow reports it while the run is open. Absent on a closed run.
  - **`status` (required)**: `string`
  - **`paused`**: `boolean`, default: `false`
  - **`paused_reason`**: `string | null`

    Why: `signal` for a human pause, `budget_cost` when `budget.total_cost_usd` was reached. Either way
    only a `resume` releases the run.
  - **`sessions`**: `object`

    Session count per state while the run is open: pending (the run has not started it yet), running,
    waiting\_for\_seat, and each artifact result. A session the run has started is counted in running once it has a
    browser seat and in waiting\_for\_seat while it is queued for one - taken from each session's own
    `waiting_for_seat`, so the two add up to the sessions in flight. A session whose live state cannot be read
    is counted running.

    **Additional properties:**

    `integer`
- **`packs`**: `array of string`

  The distinct persona packs this run's sessions were drawn from, sorted: the cohorts it covers. A run whose
  personas have no pack lists none rather than an empty cohort.
- **`refunded_usd`**: `number | null`

  Of this run's Usage, the dollars given back because a session failed on Saram's side (the pool, a lost
  worker, a setup step in Saram's code: fault `saram`). Not in `usage_usd`. Absent wherever `usage_usd` is.
- **`repro_attempts`**: `integer | null`
- **`repro_gate_discards`**: `integer | null`

  How many things reproduction's gate threw away, of two kinds: claim-check quotes the cited evidence does
  not contain, each of which left its issue `ungrounded`, and replay attempts whose path diverged
  before the judged step, each of which ruled on nothing and left its issue `skipped`. Read beside
  `confirmation_rate`: that says how much of what the judge said survived the replay, this says how often the
  replay's own checker cited loosely or its path stopped being walkable. Zero is the expected value. Absent
  until reproduction has run, and on runs reproduced before this was recorded.
- **`repro_gate_ref`**: `string | null`

  The records behind `repro_gate_discards`, fetched through `GET /api/v1/blobs`: `{"run_id", "discards"}`,
  one discard each with its `session_id`, `kind`, `reason` and a `payload` naming the `issue_id`. A
  `claim_quote` payload carries the `quote`; a `path_diverged` one carries the `attempt`, the
  `attempt_session_id`, the original `step_id` it diverged at (null for the entry url), that step's `action`
  and `expect`, and the `url` the replay was on instead. Written even when empty, so an absent ref is
  reproduction that has not run, never "nothing thrown away".
- **`session_counts`**: `object`

  Session count per status for this run: pending, running, the artifact results, or cancelled.

  **Additional properties:**

  `integer`
- **`sessions`**: `array`

  **Items:**

  schema: `SessionRow`, no additional properties

  One session of a run: one persona on one mission, in its own browser. What it ended as and whose fault
  an error was, the tokens, cost and Usage it took, where it has got to while it runs, and the refs to its
  step-by-step traces and video.
  - **`created_at` (required)**: `string`, format: `date-time`
  - **`end_step` (required)**: `integer`
  - **`mission_id` (required)**: `string`
  - **`persona_id` (required)**: `string`
  - **`session_id` (required)**: `string`
  - **`status` (required)**: `string`

    `pending`, `running`, an artifact result, or `cancelled`. Nothing reports `running` today: a session is
    recorded once, when it ends, so a session in flight still reads `pending` - see `heartbeat_step`, and
    `waiting_for_seat` for one that is queued for a browser seat rather than on a step.
  - **`steps` (required)**: `integer`
  - **`tokens_in` (required)**: `integer`

    The whole prompt plus the reply: `tokens_in` already includes both cache halves below.
  - **`tokens_out` (required)**: `integer`
  - **`wall_clock_s` (required)**: `number`
  - **`abandon_step`**: `integer | null`
  - **`agent_trace_ref`**: `string | null`
  - **`browser_session_id`**: `string | null`
  - **`cost_usd`**: `number | null`

    What this session's model calls cost. Absent when nothing prices its model; never 0 to mean "unknown".
  - **`end_reason`**: `string | null`

    How the session ended: the loop's word for an ending the persona reached (`success_criteria_met`,
    `patience_exhausted`, `budget_steps`), and on an `errored` one a sentence that says what kind of failure it
    was - the target's, the model provider's (with the provider's own message), or Saram's - with `fault` beside
    it.
  - **`fault`**: `string | null`, possible values: `"none", "saram", "provider", "target"`

    Whose failure ended the session. `none` for every ending the persona or the loop reached, and for a
    session a person cancelled; on an `errored` one, `saram` (the harness: the pool, a setup step, a lost
    worker), `provider` (the model API: an HTTP error or a transport failure after the retries, or a reply the
    loop could not act on) or `target` (the product: its entry page did not load, or its reset hook refused the
    session). What decides whether an errored session is charged. `null` while the session is open, and on a
    session recorded before this was classified - never read it as `none`.
  - **`finished_at`**: `string | null`, format: `date-time`
  - **`heartbeat_step`**: `integer | null`

    LIVE. The `step_id` the persona last reported, read live: a session is recorded once, when it ends, so
    this is the only field that says where a session in flight has got to. Best effort - absent on a finished
    session, and on a running one whose live state cannot be read. Compare it with the steps page's
    `last_step_id` to see how far the readable trace lags the browser: the trace records a step after the browser
    answers, so a step in flight shows here first.
  - **`heartbeat_usage`**: `number | null`

    LIVE. This session's Usage so far, to 0.001, from the step times on its last heartbeat: persona steps
    weighted by how long each one took, measured the way the session's Usage will be when it ends. A report, not a
    charge. Best effort, as `heartbeat_step` - and also absent for a session on a worker that does not report step
    times.
  - **`last_heartbeat_at`**: `string | null`, format: `date-time`

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

    Why nobody read this session, when nobody did, in Saram's words: the judge failure that left the session read
    as `unclear` - the model provider's status and own message when the provider refused, the `providers` entry's
    refusal when it did not resolve, Saram's sentence otherwise. Absent on every session a judge pass read,
    including one it found nothing in - which is the only way a reader tells a clean session from one no model ever
    saw.
  - **`outcome_read`**: `string | null`

    What the judge read the session's outcome as, independent of what the loop reported.
  - **`pack`**: `string | null`

    The persona's pack as the registry had it when the run was snapshotted - the session's cohort. Absent
    for a persona that declares none, and for a session recorded before sessions carried one.
  - **`price_source`**: `string | null`

    Which price table produced `cost_usd`: this deployment's own, `env` (a bare model id's row) or `env/<kind>`
    (a row for the model on one provider kind), else `genai-prices/<version>/<provider>`.
  - **`recorded_under_mask`**: `boolean`, default: `false`

    Whether the browser pool recorded this session under its mask: every field the persona typed into, every
    one-time-code field and every `data-saram-mask` element painted over in every screenshot and video frame it
    stored. Only such a session of yours can be shared by link while `ShareList.mask_required` is `true`: a share of
    any other is refused with a 409, so read the two together before offering one. `false` for every session
    recorded before the pool masked, and by a pool that does not.
  - **`refunded_usd`**: `number | null`

    What was given back for this session because it failed on Saram's side (fault `saram`): its whole debit. 0
    otherwise. Absent wherever `usage_usd` is.
  - **`result`**: `string | null`

    The artifact result once the loop reported; absent before that and on a session that never started.
  - **`resumed_at_steps`**: `(array of integer) | null`

    The first step of each resumed browser execution, one per resume: the re-orienting `look` the loop
    takes when its activity is retried. `null` and `[]` read as above. A browser checkpoint carries a url and
    a storage state and nothing else, so each of these is where a new execution picked the experiment up, not
    where the same page carried on.
  - **`surface_trace_ref`**: `string | null`
  - **`tags`**: `array of string`

    The persona's tags at snapshot time, beside `pack`.
  - **`tokens_cache_read`**: `integer`, default: `0`

    Of `tokens_in`, read from the prompt cache - billed at a tenth of the input rate on Anthropic.
  - **`tokens_cache_write`**: `integer`, default: `0`

    Of `tokens_in`, written to the prompt cache.
  - **`uncertain_steps`**: `(array of integer) | null`

    The steps of this session whose effect on the product nothing can attest: the call was dispatched, the
    response was lost, and the resume reconciled against the pool's step id rather than re-issuing it. `null`
    means nobody looked - a session recorded before the harness produced this, or one whose activity was lost
    before it or its checkpoint could report - and `[]` means the loop checked and every step is attested.
    Never read `null` as "none": it is the same three-way reading as `artifact.outcomes[].uncertain_steps`.
  - **`usage`**: `number | null`

    This session's Usage, to 0.001: the credit ledger's figure once it is settled, measured from its recorded
    step times before that, and on a session in flight its last heartbeat (`heartbeat_usage`). Absent on a session
    that has not taken a step, and on one recorded without step times.
  - **`usage_usd`**: `number | null`

    What this session's Usage costs you, net of any refund (`refunded_usd`). Absent when nothing prices it: a
    plan billed in steps.
  - **`verdict_decision`**: `string | null`

    The judge's verdict decision once it has read the session: positive, negative, or undecided.
  - **`verdict_discarded`**: `object | null`, schema: `VerdictDiscarded`, no additional properties

    NOT A VERDICT. The answer the judge gave about this session that the quote gate threw away, and why: set only
    when `verdict_decision` is absent on a session a judge read. Absent when the judge returned no verdict, which is
    the difference it is here for - both leave no decision. The decision is the judge's with no line of the persona's
    diary behind it, so it is never the persona's verdict and is never counted as one. Absent on a session observed
    before the control plane recorded it, whose gate ledger is the only record.
    - **`gate_reason` (required)**: `string`
    - **`judge_decision` (required)**: `string`, possible values: `"positive", "negative", "undecided"`

      Schema: `JudgeDecision`.

      The decision the judge returned, before the gate discarded it. The judge's and not the persona's: no diary line at the steps it cites supports it. Named apart from `verdict.decision` so that nothing reading a row by field name takes it for the persona's.
  - **`video_ref`**: `string | null`
  - **`video_url`**: `string | null`
  - **`waiting_for_seat`**: `boolean | null`

    LIVE. `true` while this session is queued for a browser seat: the run has started it, but every seat in the
    browser pool is taken, so it has no browser and takes no steps until one frees - a run with more sessions than
    the pool has seats shows this on the rest, and they start in turn without spending an attempt. `false` while
    the session is not waiting: setting up, on a seat, or paused. Read live, best effort as `heartbeat_step` -
    absent on a finished session, on one the run has not started yet (its `max_concurrent_sessions` holds it
    back), and on one whose live state cannot be read. A session resuming after a
    pause or a pool failure queues the same way, so it can be `true` beside a `heartbeat_step`.
  - **`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`
- **`sessions_per_hour`**: `number | null`

  Sessions that ran a step, per hour of the execution phase (run start to last session finish).
- **`started_at`**: `string | null`, format: `date-time`
- **`status_reason`**: `string | null`

  Why a run ended `failed` or `cancelled`, in a sentence for the caller: the spec's refusal or the target's
  reset hook's, quoted; that Saram could not start the run, or could not finish judging its sessions; that every
  session errored; or `cancelled by signal`. Absent on a `completed` run.
- **`summary`**: `object | null`, schema: `Summary`, no additional properties

  The artifact's `summary` block, once the artifact is assembled.
  - **`abandoned` (required)**: `integer`
  - **`blocked` (required)**: `integer`
  - **`completed` (required)**: `integer`
  - **`issues_total` (required)**: `integer`
  - **`personas_run` (required)**: `integer`
  - **`sessions` (required)**: `integer`
  - **`actor_cost_usd`**: `number | null`, minimum: `0`
  - **`budget_exhausted`**: `integer | null`, default: `0`
  - **`confirmation_rate`**: `number | null`, minimum: `0`, maximum: `1`
  - **`cost_usd`**: `number | null`
  - **`errored`**: `integer | null`
  - **`gate_discards`**: `integer | null`, default: `0`, minimum: `0`
  - **`issues_marker_matched`**: `integer | null`
  - **`issues_new_vs_baseline`**: `integer | null`
  - **`issues_persisting_vs_baseline`**: `integer | null`
  - **`issues_reproducible`**: `integer | null`
  - **`issues_resolved_vs_baseline`**: `integer | null`
  - **`judge_cost_usd`**: `number | null`, minimum: `0`
  - **`missions`**: `integer | null`
  - **`opportunities_total`**: `integer | null`
  - **`path_diversity`**: `number | null`, minimum: `0`, maximum: `1`
  - **`reproduction`**: `object | null`, schema: `Reproduction`, no additional properties

    The counts behind `confirmation_rate`, so a reader can see its denominator instead of trusting it. Funnel stages and readings of one set of verdicts, not a partition: `candidates` are the per-session findings the judge raised, merging folds them into `merged`, reproduction ruled on `evaluated` of those and confirmed `confirmed`. The two overlaps are deliberate. A `flaky` finding is in `evaluated` (the experiment ran) and in `inconclusive` (it settled nothing). `excluded` is a subset of the unconfirmed - the ones dropped for having no persona voice left once the replay did not confirm them - and they are exactly the findings this artifact no longer carries, so `merged - excluded` is `issues_total`.
    - **`candidates` (required)**: `integer`
    - **`confirmed` (required)**: `integer`
    - **`evaluated` (required)**: `integer`
    - **`excluded` (required)**: `integer`
    - **`inconclusive` (required)**: `integer`
    - **`merged` (required)**: `integer`
  - **`tester_errors`**: `integer | null`
  - **`verdicts`**: `object | null`, schema: `Verdicts`, no additional properties

    Cohort-level read of persona verdicts. Subjective, derived from outcomes\[].verdict. positive + negative + undecided = sessions: a session with no grounded verdict is undecided, not absent. That includes a session whose judge answered and had the answer discarded by the quote gate: its row names it in `outcomes[].verdict_discarded`, and it is still counted here as undecided, because this tally counts only verdicts grounded in the persona's words.
    - **`negative`**: `integer | null`
    - **`positive`**: `integer | null`
    - **`top_reasons`**: `(array of string) | null`
    - **`undecided`**: `integer | null`
  - **`wall_clock_s`**: `integer | null`
- **`target_name`**: `string | null`
- **`tenant_id`**: `string | null`

  Who owns this run, and so who may read it: every route filters by the tenant the request's key belongs
  to, and a run outside it is a 404 rather than a 403. Always your own tenant, which is what makes it safe to
  publish - a response can only ever carry the id you are already authenticated as. Absent on runs created
  before runs recorded their tenant; those are readable by this deployment's own fleet tenant and by nobody
  else.
- **`usage`**: `number | null`

  This run's Usage: persona steps weighted by how long each took, summed over its sessions, to 0.001. LIVE
  while `usage_state` is `live`: settled sessions read what the ledger debited, and a session in flight its last
  heartbeat. Final once `usage_state` is `settled` or `refunded`, when it is the credit ledger's own figure.
  Absent on a finished run none of whose sessions was measured, which is not the same as none used.
- **`usage_state`**: `string | null`, possible values: `"live", "held", "settled", "refunded"`

  How final `usage` and `usage_usd` are. `held`: the run is open and holds `held_usd`, and no step is
  measured yet. `live`: the run is open, or has just finished and is being settled - the figures are so far and
  still rise. `settled`: every session is settled and the figures are the credit ledger's. `refunded`: settled,
  and every dollar of its Usage was given back because it failed on Saram's side.
- **`usage_usd`**: `number | null`

  What this run's Usage costs you in dollars, at your plan's Usage price when each session ran: charged from
  your prepaid balance, refunds already taken off - so a run that failed on Saram's side reads 0 and the money
  given back is `refunded_usd`. Moves with `usage` while the run is live. This is Saram's charge on a plan that
  prices Usage (Free, Pro); your model provider's bill is never in it. Absent when nothing prices this run's
  Usage - a plan billed in steps, where `billable_usd` is the charge - which is not the same as free.
- **`workflow_id`**: `string | null`

**Example:**

```json
{
  "run_id": "run_2026-09-26_4f1c2a",
  "status": "completed",
  "target": "https://example.com",
  "tenant_id": "ten_2026-06-02_3f9a2c14",
  "created_by": "key_2026-09-01_8d1e04b7",
  "caller": "acme/ci",
  "confirmation_rate": 1,
  "repro_attempts": 2,
  "repro_gate_discards": 0,
  "repro_gate_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/repro_gate.json",
  "billed_steps": 27,
  "cost_usd": 0.59,
  "actor_cost_usd": 0.38,
  "judge_cost_usd": 0.21,
  "usage": 30.114,
  "usage_usd": 0.48,
  "refunded_usd": 0,
  "usage_state": "settled",
  "sessions_per_hour": 49.7,
  "artifact_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/artifact.json",
  "summary": {
    "personas_run": 2,
    "missions": 1,
    "sessions": 2,
    "completed": 1,
    "abandoned": 1,
    "blocked": 0,
    "errored": 0,
    "budget_exhausted": 0,
    "issues_total": 1,
    "issues_reproducible": 1,
    "issues_marker_matched": 0,
    "tester_errors": 0,
    "opportunities_total": 0,
    "verdicts": {
      "positive": 1,
      "negative": 1,
      "undecided": 0,
      "top_reasons": [
        "The sign-up form failed without saying why.",
        "Signing up was fast."
      ]
    },
    "confirmation_rate": 1,
    "reproduction": {
      "candidates": 2,
      "merged": 1,
      "evaluated": 1,
      "confirmed": 1,
      "inconclusive": 0,
      "excluded": 0
    },
    "gate_discards": 0,
    "path_diversity": 1,
    "cost_usd": 0.59,
    "actor_cost_usd": 0.38,
    "judge_cost_usd": 0.21,
    "wall_clock_s": 214
  },
  "session_counts": {
    "completed": 1,
    "abandoned": 1
  },
  "packs": [
    "general"
  ],
  "created_at": "2026-09-26T14:02:11Z",
  "started_at": "2026-09-26T14:02:14Z",
  "finished_at": "2026-09-26T14:05:47Z",
  "evidence_expires_at": "2026-12-25T14:02:11Z",
  "evidence_expired": false,
  "workflow_id": "run-run_2026-09-26_4f1c2a",
  "budgets": {
    "steps_per_session": 30
  },
  "run_spec_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_spec.json",
  "files": [],
  "sessions": [
    {
      "session_id": "run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
      "persona_id": "dana_first_timer@1",
      "pack": "general",
      "tags": [
        "general",
        "novice",
        "desktop",
        "first_time"
      ],
      "mission_id": "sign_up",
      "status": "abandoned",
      "result": "abandoned",
      "end_reason": "patience_exhausted",
      "fault": "none",
      "steps": 18,
      "end_step": 18,
      "abandon_step": 18,
      "tokens_in": 212340,
      "tokens_out": 3120,
      "tokens_cache_read": 168000,
      "tokens_cache_write": 0,
      "model": "anthropic:claude-sonnet-5",
      "cost_usd": 0.26,
      "price_source": "genai-prices/0.0.41",
      "wall_clock_s": 142.6,
      "uncertain_steps": [],
      "resumed_at_steps": [],
      "verdict_decision": "negative",
      "outcome_read": "abandoned",
      "usage": 21.412,
      "usage_usd": 0.34,
      "refunded_usd": 0,
      "workflow_id": "run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_01_dana_first_timer",
      "agent_trace_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_01_dana_first_timer/agent.ndjson",
      "surface_trace_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_01_dana_first_timer/surface.ndjson",
      "video_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_01_dana_first_timer/video.webm",
      "video_url": "/api/v1/blobs?ref=s3%3A%2F%2Fsaram-evidence%2Frun_2026-09-26_4f1c2a%2Frun_2026-09-26_4f1c2a.sess_01_dana_first_timer%2Fvideo.webm",
      "recorded_under_mask": false,
      "created_at": "2026-09-26T14:02:14Z",
      "finished_at": "2026-09-26T14:04:39Z"
    },
    {
      "session_id": "run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer",
      "persona_id": "marcus_fast_skimmer@1",
      "pack": "general",
      "tags": [
        "general",
        "power_user",
        "desktop",
        "skimmer",
        "fast"
      ],
      "mission_id": "sign_up",
      "status": "completed",
      "result": "completed",
      "end_reason": "success_criteria_met",
      "fault": "none",
      "steps": 9,
      "end_step": 9,
      "tokens_in": 88410,
      "tokens_out": 1460,
      "tokens_cache_read": 70200,
      "tokens_cache_write": 0,
      "model": "anthropic:claude-sonnet-5",
      "cost_usd": 0.12,
      "price_source": "genai-prices/0.0.41",
      "wall_clock_s": 71.3,
      "uncertain_steps": [],
      "resumed_at_steps": [],
      "verdict_decision": "positive",
      "outcome_read": "completed",
      "usage": 8.702,
      "usage_usd": 0.14,
      "refunded_usd": 0,
      "workflow_id": "run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer",
      "agent_trace_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer/agent.ndjson",
      "surface_trace_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer/surface.ndjson",
      "video_ref": "s3://saram-evidence/run_2026-09-26_4f1c2a/run_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer/video.webm",
      "video_url": "/api/v1/blobs?ref=s3%3A%2F%2Fsaram-evidence%2Frun_2026-09-26_4f1c2a%2Frun_2026-09-26_4f1c2a.sess_02_marcus_fast_skimmer%2Fvideo.webm",
      "recorded_under_mask": false,
      "created_at": "2026-09-26T14:02:14Z",
      "finished_at": "2026-09-26T14:03:27Z"
    }
  ]
}
```

### 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

- `HTTPValidationError` — shown above.

### JudgeDecision

**Type:** `string`, possible values: `"positive", "negative", "undecided"`

The decision the judge returned, before the gate discarded it. The judge's and not the persona's: no diary line at the steps it cites supports it. Named apart from `verdict.decision` so that nothing reading a row by field name takes it for the persona's.

- `LiveStatus` — shown above.
- `Reproduction` — shown above.
- `RunDetail` — shown above.
- `RunFile` — shown above.
- `RunFileAssign` — shown above.
- `SessionRow` — shown above.
- `Summary` — shown above.
- `ValidationError` — shown above.
- `VerdictDiscarded` — shown above.
- `Verdicts` — shown above.
