Skip to content

MCP tools

Saram’s MCP server is at https://api.saram.sh/mcp. It exposes 24 tools. This page is generated from mcp-tools.json of @saram/platform-api 0.16.1; the same file is at /mcp-tools.json.

To connect Claude Code, Cursor or Codex to it, by signing in or with an API key, see Connect your coding agent.

What the server tells a connecting agent, verbatim:

Saram behavioural QA: query the artifact of a run in which synthetic personas drove a real browser through this product.

A run produces two kinds of output and they are not interchangeable:

  • Issues (get_issues, get_issue) are defects, gated on reproduction. An issue with may_file_ticket_alone: true reproduced without the persona layer and is safe to act on by itself. One with false did not: report it with its evidence, do not file it as a confirmed bug.
  • Verdicts (get_verdicts) and opportunities (get_opportunities) are persona feedback: subjective, evidence-linked, scored by agreement across personas. They are advisory. They never file a ticket on their own and must be attributed to the persona, not stated as fact.

An issue’s description is objective; its persona_rationale is the persona’s voice. Keep them apart.

Start at list_runs, then get_run for one run, then get_issues to triage and get_issue for the repro steps and evidence of one. get_session_trace shows what a persona said and what the page did, step by step; get_screenshot fetches a screenshot_ref from any of the above.

A third record sits beside those two and is neither (get_tester_errors): a finding the harness attributed to the persona’s own action, not to the product. It is kept and quoted because it says why a session stalled, and it is never a defect - it is not counted in issues_total and never files a ticket.

Starting a run is billable and takes minutes: a cohort of personas drives real browsers through the product, with real model calls. So the order is validate_spec (free, instant, starts nothing, and its cost_ceiling is the most the run can cost), then start_run, then wait_for_run until it answers done, then the read tools above.

Those browsers run in Saram’s cloud, not on the user’s machine, so the target has to be a URL the internet can reach: for an app on localhost, open a tunnel to it or use a preview URL (https://docs.saram.sh/connect-agent).

A run is a person’s. Start one because someone asked you to, in the session where they asked, and stay with it until it finishes - never on a schedule, never unattended, and never in a retry loop. When a run is billed to a ChatGPT subscription login (get_provider_status), that is not a preference: the login serves runs a person starts, and one run at a time is what start_run will do for you without being told twice.

Tool Behaviour Summary
list_runs Read-only List this deployment’s runs, newest first, one row each: status, target, cohort packs, session counts, issue and opportunity counts, and cost.
get_run Read-only One run in full: its status and why, the target, the budgets, the files it hands its personas, the artifact summary once assembled, and one row per persona session with its result, cost and trace refs.
get_issues Read-only A run’s issues as triage rows, most severe first: title, type, severity, route, confidence, who it affected and whether it reproduced.
get_issue Read-only One issue in full: the contract object verbatim, its reproduction result, deterministic persona-free repro steps, preconditions, the reading that says whether it may file a ticket alone, and the evidence refs get_screenshot accepts.
get_verdicts Read-only ADVISORY. Each persona’s own verdict at the end of its session, with the diary quotes it is grounded in, plus the cohort rollup.
get_opportunities Read-only ADVISORY. Behaviour-grounded persona feedback that is not a defect: unmet expectations, missing capabilities, value and trust gaps, delight.
get_tester_errors Read-only HARNESS-ATTRIBUTED, NEVER A DEFECT. Findings this run attributed to the persona’s own action rather than to the product: a mistyped credential, the wrong element, placeholder text read as a value.
diff_vs_baseline Read-only What changed since the run’s baseline (its spec’s baseline_run_id), as the run’s artifact measured it: issues new on this run, issues of the baseline this run resolved, issues that persist, and the persona sessions that regressed - completed on the baseline and not now.
get_session_trace Read-only One session’s step-by-step ledger: the persona’s diary line beside the surface the browser saw - url, inputs dispatched, network failures, console errors, screenshot_ref.
get_screenshot Read-only Fetch one screenshot as an image, by a ref from an issue’s evidence or a trace step’s screenshot_ref.
get_evidence_url Read-only The URL one piece of evidence is fetched from, with its content type and its exact size in bytes - and none of the bytes.
validate_spec Read-only Check a run spec without starting anything, and see what the run would be.
start_run Changes state, reaches outside Saram Start a run from a run spec. BILLABLE: this drives real browsers with real model calls and takes minutes - validate_spec first, and only start a run a person has just asked you for.
get_run_status Read-only Where a run has got to: its status, whether it is done, how many sessions are in each state, what it has cost, and for each session that has not finished the heartbeat_step it last reported - the only field that says where a session in flight is.
wait_for_run Read-only Wait for a run to finish, then answer exactly what get_run_status answers.
cancel_run Changes state, destructive, idempotent Cancel a run that is still going: its sessions stop where they are and the run ends as cancelled.
list_personas Read-only The synthetic personas this deployment can run, by the address a run spec names them with (id@version), with each one’s pack and tags.
get_persona Read-only One persona in full, as the registry holds it and as a run spec may inline it: who they are, what they are trying to do, how they behave under friction, and what they find hard.
list_packs Read-only The persona packs - the cohorts a run draws from - with how many personas each holds and their addresses.
get_provider_status Read-only Which model provider your runs are billed to: the state of your caller’s own ChatGPT login (none, pending, connected, expired or error), what the gateway last saw of its consumer rate limit, and - once connected - the exact providers block to paste into a run spec, credential name included.
start_chatgpt_login Changes state, reaches outside Saram Start connecting the person’s own ChatGPT subscription, or reconnecting one that get_provider_status says is not ‘connected’.
create_share_link Changes state, reaches outside Saram Publish one finished session’s recording as a public link: anyone holding the URL can watch it, with the persona’s steps and narration, without signing in; the recording hides only what the browser pool masked when it recorded it (masked and warning in the answer say what that is), so the test account’s email and whatever else the product showed are visible as the browser showed them; and it plays until it expires or you revoke it with revoke_share_link and the id this answers.
list_share_links Read-only Every public link minted on one run’s sessions, newest first - live, expired and revoked - each with its id (what revoke_share_link takes), the session it plays, its last four characters, its state, who minted and who revoked it, when it expires and how often it was opened.
revoke_share_link Changes state, destructive, idempotent Stop a public share link playing: the next request for it is refused, a video already playing included once it asks for its next part.

Read-only.

List this deployment’s runs, newest first, one row each: status, target, cohort packs, session counts, issue and opportunity counts, and cost. Start here; get_run has the rest of one run. About 400 characters a run.

Argument Type Required Default Description
limit integer no 20 How many runs to return, newest first, at most 50.
status string or null no null Only runs in this status, e.g. running, completed, failed, cancelled.
target string or null no null Entry URL or target name, matched exactly.
Field Type Description
runs array of RunRow

Read-only.

One run in full: its status and why, the target, the budgets, the files it hands its personas, the artifact summary once assembled, and one row per persona session with its result, cost and trace refs. While the run is still open this also carries the live workflow query and each unfinished session’s heartbeat_step, which is the only thing that says where a session in flight has got to. About 1,000 characters a session.

Argument Type Required Default Description
run_id string yes Run id, as returned by list_runs.
Field Type Description
run_id string
status 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.
status_reason string or 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.
target string
target_name string or null
baseline_run_id string or null
tenant_id string or 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.
created_by string or 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.
caller string or 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 or 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.
repro_attempts integer or null
repro_gate_discards integer or 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 or 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”.
billed_steps integer 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.
billable_usd number or 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.
cost_usd number or 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.
actor_cost_usd number or 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.
judge_cost_usd number or 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.
usage number or 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_usd number or 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.
refunded_usd number or 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.
held_usd number or 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.
usage_state one of "live", "held", "settled", "refunded" or null 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.
sessions_per_hour number or null Sessions that ran a step, per hour of the execution phase (run start to last session finish).
artifact_ref string or null
summary Summary or null The artifact’s summary block, once the artifact is assembled.
session_counts object of integer Session count per status for this run: pending, running, the artifact results, or cancelled.
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.
created_at string
started_at string or null
finished_at string or null
evidence_expires_at string 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.
evidence_expired boolean 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.
workflow_id string or null
budgets object
run_spec_ref string
files array of RunFile 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.
live LiveStatus or null
sessions array of SessionRow

Read-only.

A run’s issues as triage rows, most severe first: title, type, severity, route, confidence, who it affected and whether it reproduced. Issues are defects, gated on reproduction: a row with may_file_ticket_alone=true reproduced without the persona layer. Empty with the run’s status - not an error - until the artifact is assembled. Call get_issue for one issue in full: its description, repro steps and evidence refs. About 550 characters an issue.

Argument Type Required Default Description
run_id string yes Run id.
min_severity one of "low", "medium", "high", "critical" no "low" Lowest severity to return.
reproducible_only boolean no false Only issues that survived reproduction, i.e. that may file a ticket alone.
Field Type Description
run_id string
status string The run’s status. An empty issues is only a result once this is terminal.
issues_total integer The run’s issues before the filter, so a narrowed answer says how much it left out.
issues array of IssueRow

Read-only.

One issue in full: the contract object verbatim, its reproduction result, deterministic persona-free repro steps, preconditions, the reading that says whether it may file a ticket alone, and the evidence refs get_screenshot accepts. Fields named in subjective_fields (persona_rationale) are the persona’s voice, not observed behaviour.

Argument Type Required Default Description
run_id string yes Run id.
issue_id string yes Issue id, as returned by get_issues.
Field Type Description
run_id string
issue Issue A product defect the run found: what the product did wrong, where, in how many of the sessions that got there, with the evidence and the steps that reproduce it. claim.verification.status says what checked it, and only a verified issue - reproducible: true - may be filed as a ticket on its own. description is objective; persona_rationale is the persona’s voice.
reading IssueReading How one issue of IssueList.issues may be acted on: whether it may be filed as a ticket on its own, and which of its fields are the persona’s voice rather than observed behaviour.
evidence_refs EvidenceRefs The store URIs on one issue, gathered where a consumer that wants to fetch evidence looks for them. screenshots are what the MCP get_screenshot tool accepts. The other two are not - it serves images only - and are fetched over GET /api/v1/blobs?ref= with the same key, which is why they are named here rather than left for a caller to dig out of issue.evidence.

Read-only.

ADVISORY. Each persona’s own verdict at the end of its session, with the diary quotes it is grounded in, plus the cohort rollup. Subjective feedback: attribute it to the persona and never file it as a defect. Each row names the question its verdict answers (verdict_prompt) and whose it was (verdict_prompt_source: the run’s mission or the persona pack); a row with neither had no question in play. A row with no verdict says why when there is a reason: verdict_discarded is an answer the judge gave that could not be grounded in the persona’s diary, so it was thrown away - never the persona’s verdict and never counted, which is why the rollup has the session undecided - and observer_error is a session no judge read. About 600 characters a session.

Argument Type Required Default Description
run_id string yes Run id.
Field Type Description
run_id string
status string The run’s status. An empty verdicts is only a result once this is terminal.
summary Verdicts or null The artifact’s cohort read (summary.verdicts).
verdicts array of VerdictRow
advisory string How to read this whole answer: persona feedback, not defects. Never a ticket on its own.
subjective_fields array of string Fields on each row that are the persona’s opinion - for a verdict, the verdict itself.

Read-only.

ADVISORY. Behaviour-grounded persona feedback that is not a defect: unmet expectations, missing capabilities, value and trust gaps, delight. Scored by consensus (the fraction of sessions that reached the area and raised it), never by reproduction, and never a ticket on their own. Returned in display order; display.shown are the records that clear the cohort floors (40% consensus across at least 2 sessions, or solicited by the mission) and display.folded are single-persona notes - the same split GitHub and the dashboard show. About 1,300 characters an opportunity; min_consensus narrows a long list.

Argument Type Required Default Description
run_id string yes Run id.
min_consensus number no 0 Lowest consensus to return, 0 to 1. 0.5 means half the sessions raised it.
Field Type Description
run_id string
status string The run’s status. An empty opportunities is only a result once this is terminal.
opportunities array of Opportunity
advisory string How to read this whole answer: behaviour-grounded persona feedback, consensus-scored, never a ticket on its own.
subjective_fields array of string Fields on each opportunity that are opinion. description is deliberately not among them.
display DisplaySplit The run’s shown/folded split, present even when the run raised nothing: the floors are the answer to “why is this list short”, and they are the same whether or not anything cleared them.

Read-only.

HARNESS-ATTRIBUTED, NEVER A DEFECT. Findings this run attributed to the persona’s own action rather than to the product: a mistyped credential, the wrong element, placeholder text read as a value. They carry the same shape as an issue - same id, same evidence, same screenshots - and they are not issues: not in issues_total, not reproduced, never a ticket. Read them for why a session stalled and for what to fix in the test setup. Each one quotes the recorded line that shows what the persona did. Each record is the whole issue-shaped object; reading[] gives its label, what the persona did in the words the dashboard and the PR comment use. Empty with the run’s status until the artifact is assembled; classified=false means the run’s artifact predates the classification, not that it found none. About 2,500 characters a record.

Argument Type Required Default Description
run_id string yes Run id.
Field Type Description
run_id string
status string The run’s status. An empty tester_errors is only a result once this is terminal.
classified boolean Whether the run’s artifact sorts findings by whose failure they are at all. Empty with this false is an artifact from before the classification (contracts 0.5), or no artifact yet: nobody asked. Empty with this true is a run that asked and found none.
tester_errors array of Issue The artifact’s tester_errors[], each the contract’s own issue object with its tester_error block.
reading array of TesterErrorReading One entry per record, in the same order and naming its tester_error_id.
advisory string How to read this whole answer: harness-attributed, never a defect. What it may and may not be used for.
subjective_fields array of string Fields on each record that are somebody’s words rather than an observation: the persona’s narration, and tester_error.reason, the harness’s sentence about its own persona.

Read-only.

What changed since the run’s baseline (its spec’s baseline_run_id), as the run’s artifact measured it: issues new on this run, issues of the baseline this run resolved, issues that persist, and the persona sessions that regressed - completed on the baseline and not now. Only a reproduction-gated issue is ever new or resolved; a persisting one may not be, so may_file_ticket_alone on each row still decides what may be filed. A regressed session is a signal, never a defect: it is reportable only through its new_issue_ids. Resolved rows are the baseline’s issues, read whole with get_issue on baseline_run_id; every row carries the evidence refs get_screenshot and get_evidence_url accept. Refused, with the reason, for a run that named no baseline or was never compared with it, and until the run’s artifact is assembled. About 450 characters an issue.

Argument Type Required Default Description
run_id string yes Run id; its baseline is the one its spec named.
Field Type Description
run_id string
baseline_run_id string
issues_new integer
issues_resolved integer
issues_persisting integer
new array of DiffIssueRow This run’s issues the baseline did not have, on routes it reached. Only a reproduction-gated issue is ever new, so every row here may file a ticket alone.
resolved array of DiffIssueRow The baseline’s issues this run no longer has, with the baseline’s ids and evidence: read one whole with get_issue on baseline_run_id. Only an issue that reproduced there, on a route this run reached, counts.
persisting array of DiffIssueRow This run’s issues the baseline had too, reproduced or not: persisting is a fact about both runs’ findings, not a new claim about the product, so may_file_ticket_alone on each row still decides what may be filed.
regressed_sessions array of RegressedSession
advisory string How to read regressed_sessions: a signal, never a defect on its own.
note string or null Present when the artifact counts resolved issues it does not all name: one written before contracts 0.6.2, or a baseline assembled again since.

Read-only.

One session’s step-by-step ledger: the persona’s diary line beside the surface the browser saw - url, inputs dispatched, network failures, console errors, screenshot_ref. Step ids match the step_refs on issues and verdicts. Readable while the session is still running, in which case status says in_flight, more steps are coming, and the session video is not stored yet. By default each step is its diary: the action, what the persona said and perceived, the url and the screenshot_ref it was looking at - about 300 characters a step, which is what ‘what was it thinking’ needs. detail=‘full’ adds the surface block - inputs dispatched, network failures, console errors, timing, tokens, video refs - at about 1,500 a step. With no window this returns the last 60 steps of a longer session; page back with from_step/to_step (inclusive). No answer is more than 25,000 characters: a window that would be is cut to the steps that fit - the latest of an unwindowed ask, the first of an explicit one - and from_step/to_step are the steps that came back, so the next page starts at to_step + 1. total_steps is the whole trace.

Argument Type Required Default Description
run_id string yes Run id.
session_id string yes Session id, as returned by get_run.
from_step integer or null no null First step id to return, inclusive.
to_step integer or null no null Last step id to return, inclusive.
detail one of "diary", "full" no "diary" diary: per step, the action, the persona’s words, the url and the screenshot_ref. full: every field, the surface block included, at several times the size.
Field Type Description
run_id string
session_id string
persona_id string
mission_id string
status 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.
heartbeat_step integer or 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 or null When that heartbeat reached the server: how stale heartbeat_step is. Absent whenever it is.
waiting_for_seat boolean or 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 or null 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 or null The session’s workflow id, as SessionRow.workflow_id: what heartbeat_step was read off.
from_step integer or null The inclusive lower bound in effect, whether it came from from or after + 1.
to_step integer or null
total_steps integer Steps in the whole trace, before the from/to window - not the count of steps.
first_step_id integer or null
last_step_id integer or 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.
agent_trace_ref string or null
surface_trace_ref string or null
video_ref string or null
video_url string or null
steps array of AlignedStep

Read-only.

Fetch one screenshot as an image, by a ref from an issue’s evidence or a trace step’s screenshot_ref. Refs outside this run’s evidence are refused. Images only, at most 2 MiB; anything else - a video, a DOM snapshot, a raw trace - is a call to get_evidence_url, which answers a URL to fetch instead of bytes to read.

Argument Type Required Default Description
ref string yes Screenshot URI, e.g. s3://bucket/run/session/shots/0012.png.

Content without a declared schema (for example an image).

Read-only.

The URL one piece of evidence is fetched from, with its content type and its exact size in bytes - and none of the bytes. This is the way to a session video, a screenshot too large to look at inline, and anything else get_screenshot refuses: a plain GET on the URL with the same Authorization header you are configured with, which honours Range, and HEAD for the size again without a body. Read size_bytes before you fetch: evidence is megabytes, and a video is not something to read into a conversation. The ref is checked against the evidence store and the run it belongs to before any URL is answered, so a URL from this tool is one your key can fetch. Images and video only - the raw agent and surface traces and the run spec are not served here, and the modelled view of a session is get_session_trace.

Argument Type Required Default Description
ref string yes Evidence URI, e.g. s3://bucket/run/session/video.webm.
Field Type Description
ref string The store URI asked about, unchanged - what a trace or an issue’s evidence recorded.
url string The absolute GET /api/v1/blobs?ref= URL, fetched with the same API key and honouring Range.
content_type string What the route will answer as Content-Type, from the ref’s file suffix.
size_bytes integer The whole blob’s size as the store reports it, so a caller knows the cost before it pays it.

Read-only.

Check a run spec without starting anything, and see what the run would be. Runs every refusal start_run runs - the contract, the cross-references inside providers, whether each providers entry is on a provider Saram resolves, the single-user rule on a ChatGPT login, whether every awssm:// credential_ref is one your tenant uploaded, whose uploaded files the spec names, whether your caller has a model provider at all, and - for a providers block on your ChatGPT login - whether the gateway holds that login to sign with - then answers the spec as the run would store it and the sessions it would plan, one per persona per mission, plus warnings: a ChatGPT login whose access token has lapsed or whose gateway could not be asked, and a model Saram has no price for, which is new or misspelt. It also answers what the run would cost: cost_ceiling.usd is the most the run can bill on your plan, in dollars, on every plan (0 when your plan bills nothing), with a one-line basis and model_calls, who bills the model calls on your own provider, which are never in the figure. It is a ceiling, not a forecast - every step counted at its most - so when a person asks what a run would cost, answer ‘up to $’ from this one call and quote the basis. Where your plan is billed in Usage, usage_ceiling says the most Usage the run can use, the balance available to cover it, and the steps_per_session that would fit; start_run refuses a run it does not cover when enforced is true. Free, instant, and creates nothing: call it before every start_run.

Argument Type Required Default Description
spec object yes A run spec, the RunSpec contract. Required: target.entry_url, personas (id@version addresses from list_personas), missions (each an id, a goal in words a persona would understand and at least one success_criteria {kind, value}) and budget.steps_per_session. The example is the smallest spec that validates; everything else is optional.
Field Type Description
run_id string or null The run id the spec carries. Absent means POST /runs would assign one.
spec object The spec as the run would store it.
sessions array of PlannedSession
warnings array of string What the dry run can say about the spec but does not refuse it for, one sentence each, the ones about your ChatGPT login first. Today: providers entries on a ChatGPT login whose access token has lapsed (the gateway refreshes it on the run’s first model call, and the run fails if that refresh is refused) or whose gateway this API could not ask; a providers entry whose model this platform’s price table has no price for, which is either a model too new for the table or a misspelt id that fails at the run’s first model call; and a budget.max_concurrent_sessions above what your plan runs at once, which the run honours by running fewer sessions at a time.
cost_ceiling CostCeiling or null What the run would cost: the most it can bill on your plan (usd), with a one-line basis and who bills its model calls. A ceiling, not a forecast. On every plan; absent only for a caller this deployment has no tenant for.
usage_ceiling UsageCeiling or null The most Usage this run can use and the balance it is held against. Absent when this deployment’s run gate is off, or your plan is not billed in Usage.

Changes state, reaches outside Saram.

Start a run from a run spec. BILLABLE: this drives real browsers with real model calls and takes minutes - validate_spec first, and only start a run a person has just asked you for. Answers a run_id and a workflow_id and nothing else, so wait_for_run is the next call. The spec is posted exactly as you give it: no providers block is added for you (get_provider_status prints one to paste), no credential_ref is rewritten and no budget is changed. The two fields it fills in when you leave them out are trigger.kind, which becomes ‘api’, and trigger.actor, which becomes your caller name. Refuses a second run while one you started is still in flight, unless you pass allow_concurrent. A run your plan cannot pay for is refused with an error whose structured content is {detail, usage_ceiling} or {detail, monthly_cap}: usage_ceiling, present when the balance does not cover the run, is the one validate_spec answers; monthly_cap, present when the month has billed your monthly cap, is {period, billed_usd, cap_usd}.

Argument Type Required Default Description
spec object yes A run spec, the RunSpec contract, as validate_spec accepted it.
allow_concurrent boolean no false Start this run even though one you started is still in flight. Say true only because a person asked for both runs.
Field Type Description
run_id string The run’s id: the handle every other run route and tool takes.
workflow_id string The id of the workflow executing the run, for support requests.

Read-only.

Where a run has got to: its status, whether it is done, how many sessions are in each state, what it has cost, and for each session that has not finished the heartbeat_step it last reported - the only field that says where a session in flight is. A finished session says whose failure ended it (fault), and one that errored says why (end_reason). To wait for a run rather than watch it, use wait_for_run; get_run is the same run with its spec, budgets and trace refs.

Argument Type Required Default Description
run_id string yes Run id, as start_run returned it.
Field Type Description
run_id string
status string pending, running, observing, then one terminal value - the same vocabulary RunSummary.status carries, with the same meaning. Read done rather than comparing against a list of them.
done boolean Whether status is terminal (completed, cancelled, failed), i.e. nothing more will happen to this run. What a polling agent loops on; status then says how it ended and completed is the only one with results to read.
status_reason string or null Why the run ended failed or cancelled, as RunSummary.status_reason says it. Absent otherwise.
session_counts object of integer
cost_usd number or null
artifact_ref string or null Set once the artifact is assembled, which is what makes the issue and feedback views non-empty.
sessions array of SessionProgress
created_at string
started_at string or null
finished_at string or null

Read-only.

Wait for a run to finish, then answer exactly what get_run_status answers. Returns as soon as the run reaches a terminal status, or after timeout_s if it has not - so done=false means the timeout won, not that anything went wrong, and the way to keep waiting is to call this again. One call is one HTTP request, which is why timeout_s stops at 60; a run of any size takes several calls. Between them, say what the run is doing rather than waiting silently.

Argument Type Required Default Description
run_id string yes Run id, as start_run returned it.
timeout_s integer no 60 How long to wait, in seconds, at most 60.
Field Type Description
run_id string
status string pending, running, observing, then one terminal value - the same vocabulary RunSummary.status carries, with the same meaning. Read done rather than comparing against a list of them.
done boolean Whether status is terminal (completed, cancelled, failed), i.e. nothing more will happen to this run. What a polling agent loops on; status then says how it ended and completed is the only one with results to read.
status_reason string or null Why the run ended failed or cancelled, as RunSummary.status_reason says it. Absent otherwise.
session_counts object of integer
cost_usd number or null
artifact_ref string or null Set once the artifact is assembled, which is what makes the issue and feedback views non-empty.
sessions array of SessionProgress
created_at string
started_at string or null
finished_at string or null

Changes state, destructive, idempotent.

Cancel a run that is still going: its sessions stop where they are and the run ends as cancelled. There is no resume and no refund - what has been spent is spent, and the only way on from here is a new run. A run that is not running is refused rather than cancelled twice.

Argument Type Required Default Description
run_id string yes Run id.
Field Type Description
run_id string The run the signal was delivered to.
signal string The signal delivered: pause, resume or cancel.

Read-only.

The synthetic personas this deployment can run, by the address a run spec names them with (id@version), with each one’s pack and tags. Narrow to one pack with the pack argument, or call list_packs to see the cohorts. About 200 characters a persona.

Argument Type Required Default Description
pack string or null no null Only personas in this pack, matched exactly.
Field Type Description
personas array of PersonaSummary

Read-only.

One persona in full, as the registry holds it and as a run spec may inline it: who they are, what they are trying to do, how they behave under friction, and what they find hard. Read this to say whose experience a verdict or an issue is - never to speak as them.

Argument Type Required Default Description
address string yes id@version, or a bare id for the latest version.
Field Type Description
id string
version integer
name string or null
pack string or null
tags array of string or null
backstory string or null
vocabulary Vocabulary or null
facts object of string or null
conversation Conversation or null
verdict_prompt string or null
goals Goals What the persona wants out of the visit, in their own terms: the primary goal, anything secondary they also want if they get that far, and how much they want it (intensity). All three go into the persona’s briefing beside the mission’s goal; what counts as success for the run is the mission’s success_criteria.
priors Priors What the persona knows before they arrive: how well they know this product, the language of its industry and technology in general, the expectations they bring from other products, and what they were told beforehand. Each goes into the persona’s briefing and shapes what they understand, expect and notice.
action_policy ActionPolicy How the persona works a page: how much of it they read, what they work it with, whether they wander or head straight for the goal, and how they treat defaults, help and errors. Each value becomes a line of the persona’s briefing.
tolerance Tolerance How much the persona puts up with before giving up. Saram counts, step by step, the steps without visible progress, the errors and slow responses, and the moments with no clear next action, and ends the session abandoned the moment one crosses its threshold here; the model playing the persona cannot argue with the count. The persona is also told these limits in their own terms.
constraints Constraints The browser the persona uses: the kind of device, the screen size, the connection, the language, accessibility needs and the signed-in state they start in. Most of these set up the browser session rather than the persona’s briefing. A replay of an issue runs with the same ones, and the issue lists them in preconditions.
voice Voice or null
provenance Provenance or null

Read-only.

The persona packs - the cohorts a run draws from - with how many personas each holds and their addresses. A run spec has no pack field: it names personas one by one, so this is how you put a whole cohort in one. Personas that declare no pack are in none of these; list_personas has them.

None.

Field Type Description
packs array of PackSummary Sorted by name. A persona that declares no pack is in no cohort and is in none of these - list_personas is what has all of them, the same way RunSummary.packs lists a run’s cohorts and not its packless personas.

Read-only.

Which model provider your runs are billed to: the state of your caller’s own ChatGPT login (none, pending, connected, expired or error), what the gateway last saw of its consumer rate limit, and - once connected - the exact providers block to paste into a run spec, credential name included. Metadata about a login and never any part of one: no token, no routing key. When state is not ‘connected’, tell the person, and offer to sign them in to ChatGPT: with their go-ahead, start_chatgpt_login answers a link and a code for them to open in their browser. While state is ‘pending’, pending carries that link and code again - show them, rather than starting another.

None.

Field Type Description
caller string
state one of "none", "pending", "connected", "expired", "error"
pending CodexPending or null
account_id string or null
plan_type string or null
connected_at string or null
expires_at string or null
last_refresh string or null
error string or null Why the last login attempt failed, when state is error and no gateway call was involved. The OAuth error_description, which cannot carry a token.
gateway CodexGateway or null
providers CodexProviders or null

Changes state, reaches outside Saram.

Start connecting the person’s own ChatGPT subscription, or reconnecting one that get_provider_status says is not ‘connected’. Call it only when the person has asked for it, in this session: a login is theirs alone and serves only the runs they start. Answers a verification_url and a user_code - show both to the person, exactly as given, and ask them to open the link in their own browser, sign in to ChatGPT, and enter the code. The sign-in is theirs to do: never open the link, sign in or enter the code for them. The code lasts expires_in seconds (15 minutes); once they say they are done, call get_provider_status, which says ‘connected’ and prints the providers block to paste into a run spec. Refused while an earlier code is still open (the refusal carries that link and code) and for a credential that is not a person’s own.

None.

Field Type Description
caller string Whose ChatGPT login this connects: the caller the credential identifies.
verification_url string The page to open in a browser, where the person signs in to ChatGPT. Show it to them.
user_code string The code the person enters on that page. Show it to them.
expires_in integer Seconds until the code expires and the sign-in has to start again.

Changes state, reaches outside Saram.

Publish one finished session’s recording as a public link: anyone holding the URL can watch it, with the persona’s steps and narration, without signing in; the recording hides only what the browser pool masked when it recorded it (masked and warning in the answer say what that is), so the test account’s email and whatever else the product showed are visible as the browser showed them; and it plays until it expires or you revoke it with revoke_share_link and the id this answers. Call it only when a person has asked to share this session, and show them the answer’s warning before the link goes anywhere. The url is in this answer and never again - only a hash of it is kept - so a lost link is replaced by minting another and revoking this one. It expires after expires_in_days (30 unless you ask for 1 to 90) and never after the run’s evidence does; capped_by_evidence says when that won. Refused while your tenant’s public sharing is off, which only Saram turns on; for a session that has not ended, whose recording is not stored yet; for a run whose evidence has expired; and for a session the browser pool did not record under its mask, which paints over what the persona typed.

Argument Type Required Default Description
run_id string yes Run id.
session_id string yes Session id, one of the sessions get_run lists.
expires_in_days integer no 30 How many days the link plays, 1 to 90; 30 when omitted. Never past the run’s evidence lifetime, whatever you ask for. No link lives forever and none is extended: mint a new one instead.
Field Type Description
id string shr_…: the link’s id, for listing and revoking it. Not the link itself.
url string The public link. Shown once, here, and never again by any route. Anyone who has it can watch this session until it expires or is revoked, with no sign-in.
run_id string
session_id string
created_at string
created_by string key_… for an API key, usr_… for a person, operator for the operator.
expires_at string
capped_by_evidence boolean True when the run’s evidence lifetime, and not the days asked for, set expires_at: the link ends with the recording.
masked boolean True when the browser pool recorded the session under its mask: the value of every field the persona typed into, every one-time-code field and every data-saram-mask element are painted over in every frame. False only for a session of Saram’s own, which is shared unmasked.
warning string What anyone holding the link will see, what in the recording is masked and what is not, and how to revoke it. Show it to whoever posts the link.

Read-only.

Every public link minted on one run’s sessions, newest first - live, expired and revoked - each with its id (what revoke_share_link takes), the session it plays, its last four characters, its state, who minted and who revoked it, when it expires and how often it was opened. Never a link itself: a URL is shown once, by create_share_link. public_shares says whether your tenant may share at all; while it is off no link plays, whatever its state. mask_required says whether a session of yours is shared only when the browser pool recorded it under its mask (recorded_under_mask on its session row): while it is true, create_share_link refuses any other.

Argument Type Required Default Description
run_id string yes Run id.
Field Type Description
run_id string
public_shares boolean Whether your tenant may publish sessions at all. Off, a new link is refused with a 403 and no existing link plays; their rows are kept and they play again, until their own expiry, once it is back on.
mask_required boolean Whether a session of yours is shared only when the browser pool recorded it under its mask (SessionRow.recorded_under_mask): a share of any other is refused with a 409. True for every customer; false only for Saram’s own sessions, which are shared unmasked.
shares array of ShareSummary

Changes state, destructive, idempotent.

Stop a public share link playing: the next request for it is refused, a video already playing included once it asks for its next part. There is no undoing it; to share the session again, create_share_link mints a new link. Any credential of your tenant that may write runs revokes any of its links, not only the one that minted it. The link’s record stays, with who revoked it and when, and asking again answers the same record.

Argument Type Required Default Description
share_id string yes shr_…: the link’s id, as create_share_link and list_share_links answer it.
Field Type Description
id string
run_id string
session_id string
token_hint string The link’s last four characters, to tell your links apart.
state one of "live", "expired", "revoked" live plays, expired and revoked do not. Read it beside ShareList.public_shares: while your tenant’s sharing is off no link plays, whatever its state.
created_at string
created_by string
caller string or null Who minted it, in words, when the credential had a name.
expires_at string
revoked_at string or null
revoked_by string or null Who revoked it: any principal of your tenant that may write runs can.
view_count integer How many times the replay was opened. Nothing about who opened it is kept.
last_viewed_at string or null