# Reading results

Source: https://webagent-docs-staging.lookover.ai/qa-testing/reading-results

> What each webagent tool returns, what the stop reasons mean, and how long evidence links last.

Your agent gets back a test report, a task result or a skill run. It tells you what it saw. This page helps you check it.

| Tools | Result | Use for |
|---|---|---|
| `smoke_test`, `get_report` | A **test report**: one verdict, the steps, console errors and failed network calls | A pass/fail check of a page or flow |
| `run_task`, `get_task` | A **task**: a goal the browser agent worked through | Exploring, or a flow you want the agent to judge |
| `skill_<name>`, `run_skill`, `get_skill_run` | A **skill run**: the raw record of a saved check | Reading a skill's own result |
| Any tool, on failure | An **error**: `[code] message` | A call that did not run |

`get_report` also reads the report of a task or a skill run, so every result can be turned into a test report.

## The test report [#the-test-report]

`smoke_test` and `get_report` answer with a short text and the whole report as `structuredContent`. The same report is at `GET /v1/skill-runs/:id/report.json` and `GET /v1/tasks/:id/report.json`. It looks the same for a skill run and for a task.

This is the whole report of a real smoke test of a Test Arena address that does not exist, with only the ids and links replaced. The page answered `404`, so the second step failed and the network call is listed:

```json
{
  "id": "run_us_xxxxxxxxxxxxxxxxxxxxxxxx",
  "kind": "skill_run",
  "sessionId": "sess_us_xxxxxxxxxxxxxxxxxxxxxxxx",
  "name": "smoke-uat@1.2.1",
  "verdict": "fail",
  "outcome": "failed",
  "summary": "Smoke test of https://webagent-test-web.pages.dev/missing-page FAILED in 1 s: 1 of 2 steps passed. Step 2 \"assert 2 conditions\" failed: Page responded without an error status. 1 failed network call (GET /missing-page 404).",
  "url": "https://webagent-test-web.pages.dev/missing-page",
  "finalUrl": "https://webagent-test-web.pages.dev/missing-page",
  "startedAt": "2026-11-12T00:10:56.386Z",
  "endedAt": "2026-11-12T00:10:57.319Z",
  "durationMs": 933,
  "counts": { "steps": 2, "failedSteps": 1, "findings": 0, "consoleErrors": 0, "networkFailures": 1 },
  "steps": [
    { "index": 0, "title": "navigate https://webagent-test-web.pages.dev/missing-page", "kind": "navigate", "status": "pass", "ms": 423 },
    {
      "index": 1,
      "title": "assert 2 conditions",
      "kind": "assert",
      "status": "fail",
      "error": "Page responded without an error status",
      "ms": 4,
      "screenshotUrl": "https://..."
    }
  ],
  "findings": [],
  "consoleErrors": [],
  "networkFailures": [
    { "kind": "http_error", "method": "GET", "url": "https://webagent-test-web.pages.dev/missing-page", "status": 404 }
  ],
  "evidence": {
    "reportPath": "/v1/skill-runs/run_us_xxxxxxxxxxxxxxxxxxxxxxxx/report",
    "reportSignedUrl": "https://...",
    "recordingPath": "/v1/sessions/sess_us_xxxxxxxxxxxxxxxxxxxxxxxx/recording",
    "logsPath": "/v1/sessions/sess_us_xxxxxxxxxxxxxxxxxxxxxxxx/logs",
    "urlsExpireAt": "2026-11-12T00:15:58.494Z"
  },
  "notes": []
}
```

A test with steps and an `expectText` lists every step: the page checks (`navigate`, `check basic-a11y`, `check broken-links`), one `act` step for each of your steps, `check console-errors`, then the expectation checks. A check you did not ask for (for example `expectUrl` when you set only `expectText`) is listed with `status: "skipped"`. A real report lists up to 50 steps.

### The verdict [#the-verdict]

| `verdict` | Meaning | What to do |
|---|---|---|
| `pass` | The test ran and every check passed. For a task: the agent says it is done. | Nothing. For a task, see the note below |
| `fail` | The test ran and found a problem in the page or the flow. Read `summary` and the failed `steps`. | Fix the app, or the test |
| `error` | The test could not run to a result: the run was cancelled, denied a permission, or the browser failed. For a task: the agent could not test (CAPTCHA, a blocked address, an internal error). | This is not a bug in your app. Fix the cause and run again |
| `running` | Not finished. The `summary` says to call `get_report` with the `id`. | Call `get_report` again |

`outcome` is the raw result under the verdict: the skill status (`passed`, `failed`, `error`, `cancelled`, `denied`) or the task `stoppedBy` (`done`, `stuck`, `budget` and so on). `isError` on the tool call is `true` only for verdict `error`.

> **Warning: A task pass is not a check**
>
> A task has no assertions. For a task report, `pass` only means the agent says it is done, and the report says so in `notes`. To check that a text, an address or a result is really there, use `smoke_test` with `expectText`, `expectUrl` or `expectNoText`.

### The fields [#the-fields]

| Field | Meaning |
|---|---|
| `id`, `kind` | The run id (`run_…`, kind `skill_run`) or the task id (`tsk_…`, kind `task`) |
| `sessionId` | The browser session the test ran in |
| `name` | `smoke-uat@1.2.1` for a skill run. The goal, cut to 300 characters, for a task |
| `summary` | Plain sentences you can pass on as they are: what was tested, how many steps passed, the first failed step with its reason, the findings and the console and network totals |
| `url` | The page under test. For a task it is the first page the browser opened, taken from the session log |
| `finalUrl` | Where the browser ended. From the session log, so it is missing until the log is saved |
| `startedAt`, `endedAt`, `durationMs` | When it started and ended, and how long it took. `endedAt` and `durationMs` appear when it has ended |
| `counts` | The real totals before the lists below are cut: `steps`, `failedSteps`, `findings`, `consoleErrors`, `networkFailures` |
| `steps` | Up to 50 steps in order. Each has `index` (from 0), `title`, `kind`, `status` (`pass`, `fail` or `skipped`), `error`, `ms` and, for a failed step of a skill run, a `screenshotUrl`. When there are more than 50, failed steps come first and a note says how many are shown |
| `findings` | Up to 30 problems the checks found (a broken link, an accessibility issue), worst first. Each has `severity` (`info`, `low`, `medium`, `high`, `critical`), `title`, and often `rule`, `url`, `fix` and `detail`. A task has none |
| `consoleErrors` | Up to 15 browser console errors and uncaught page errors: `source` (`console` or `pageerror`), `text` and `url` |
| `networkFailures` | Up to 20 failed requests: `kind` (`http_error` for a 4xx or 5xx answer, `request_failed` when nothing came back), `method`, `url`, `status` or `error` |
| `evidence` | Links to the proof. See below |
| `notes` | Up to 8 sentences on what is missing and why. Read them when a list looks too empty |

A task step has only the action and its target (for example `click e12`). It has no page address, screenshot or readable label. A report of a skill run has all of these for its steps.

If `counts.consoleErrors` is larger than the list, only the first entries are shown. Use `logsPath` for the full log.

### Console and network errors come from the session log [#console-and-network-errors-come-from-the-session-log]

Console errors and failed network calls are read from the session log. The log is saved when the session ends. A report read while the session is still open has empty lists, no `finalUrl`, and this note: "The session log is not available (it is stored when the session ends), so console errors, failed network calls and URLs are missing." That does not mean the page had no errors. Read the report again after the session has ended. For a task that left its session open (`keepOpen`) that means after `stop_session`. `smoke_test`, `get_report` after a skill run and a `run_task` that closes its own session have the log at once; in CI, read the report once more a few seconds later if `evidence.logsPath` is missing, as the script in [Smoke tests in CI](/guides/ci) does.

### Evidence and when links stop working [#evidence-and-when-links-stop-working]

| Field | What it is | Lifetime |
|---|---|---|
| `evidence.reportSignedUrl` | The HTML report as a link that opens without a key | The links stop working at `urlsExpireAt`. This is about 5 minutes after you asked for the report |
| `steps[].screenshotUrl` | A screenshot of a failed step | The same time |
| `evidence.reportPath` | Skill runs only. `GET` this path with your API key: it redirects to the HTML report | The redirect lasts 5 minutes |
| `evidence.recordingPath` | The replay video of the session. Only when the session was recorded: always for `smoke_test`, and for `run_task` only with `record: true` | The path needs your key. The video is saved when the session ends |
| `evidence.logsPath` | The full console and network log of the session | Needs your key. Present when the log was read |

Call `get_report` again to get new links. Download what you need to keep. See [Evidence](/concepts/evidence).

## Task results [#task-results]

`run_task` answers with a short text, and the same data as `structuredContent`:

Here is a task that could not finish, because the page was an interstitial and not the app. The text:

```text
Task stopped (blocked) in 2 steps, $0.0044, 10 s.
Reason: The AI model could not find a way forward (50% sure): The page is an ngrok interstitial. ...
Session sess_us_xxxxxxxxxxxxxxxxxxxxxxxx was closed.
```

And the data, trimmed:

```json
{
  "sessionId": "sess_us_xxxxxxxxxxxxxxxxxxxxxxxx",
  "sessionClosed": true,
  "task": {
    "ok": false,
    "stoppedBy": "blocked",
    "reason": "The AI model could not find a way forward (50% sure): The page is an ngrok interstitial. ...",
    "steps": [
      { "op": "wait", "provider": "claude:claude-haiku-5-5", "ok": true, "ms": 3200, "costMicros": 140 }
    ],
    "costMicros": 4400,
    "ms": 10000,
    "text": "..."
  }
}
```

| Field | Meaning |
|---|---|
| `ok` | `true` only when the agent stopped because it was done (`stoppedBy: done`). |
| `answer` | What the agent says it found. Present when it finished. |
| `stoppedBy` | Why the task ended. See the table below. |
| `reason` | A plain sentence about why it stopped, when it did not finish. |
| `limit` | Which limit ended it, when one did: `maxSteps`, `maxCostUsd` or `allowHosts`. |
| `steps` | One entry per action: `op` (click, type, navigate…), the element `ref`, `ok`, time in ms, cost and `error`. |
| `costMicros`, `ms` | Cost in millionths of a dollar, and total time in milliseconds. |
| `text` | The final page diff or snapshot text, cut short. |
| `sessionClosed` | Whether the tool closed the browser. `run_task` closes a session it opened, unless you passed `keepOpen`. |

A goal task has no assertions of its own. `ok: true` means the agent believes it reached the goal. It does not prove your page is correct. Ask for a specific thing to check in the prompt, such as the exact text that must appear, and read `answer`. For a check with a fixed pass or fail, use `smoke_test`. To get a report for a task, call `get_report` with its `taskId`: the answer ends with that hint.

### Still running [#still-running]

A task that takes longer than the wait (90 seconds for `run_task`, 30 seconds by default for `get_task`) is not an error. The tool answers:

```json
{ "sessionId": "sess_us_...", "taskId": "tsk_...", "status": "running", "note": "still running: call get_task ..." }
```

The browser keeps working and the session stays open. Call `get_report` with the `taskId`, or `get_task` with the `sessionId` and `taskId`, until it has finished. Neither closes the session: call `stop_session` when you are done.

`get_task` answers with `status: finished` and the stored task. Its steps and cost have the same meaning as above.

### Stop reasons [#stop-reasons]

| `stoppedBy` | Meaning | Treat as |
|---|---|---|
| `done` | The agent finished and gave an answer. | Pass or fail, from the answer |
| `blocked` | The agent could not go on. For example a login wall it has no login for, or an address webagent refused to open. `reason` says which. `limit: allowHosts` means a site allowlist refused a navigation. | Could not test, unless the block is the app's fault |
| `stuck` | Several steps in a row left the page unchanged or failed, or the agent kept repeating itself. | Often a real problem on the page: read `reason` and the failed steps |
| `budget` | The task hit its step or cost cap. `limit` says which. | Could not test. Raise `maxSteps` (1 to 50) or split the goal |
| `captcha` | The page showed a CAPTCHA. Webagent does not solve CAPTCHAs. | Could not test |
| `paused` | A person took over the session. | Could not test |
| `error` | Something failed inside the agent or the browser. | Could not test |
| `aborted` | The task was stopped from outside, for example the session was closed. | Could not test |
| `unanswered` | A question to the user waited with no answer. Over MCP the agent cannot answer, so this should not happen. | Could not test |

"Could not test" is not a bug in your app. An agent that reports a CAPTCHA or a budget stop as "the app is broken" is wrong. The [agent setup snippet](/qa-testing/agent-setup) asks your agent to keep the two apart.

### `ok: false` and `isError` [#ok-false-and-iserror]

A task that stops without finishing is still a normal tool result in most cases. `isError` is set only when the agent could not test: `stoppedBy` is `captcha`, `blocked`, `error`, `aborted` or `paused` (or a capacity or timeout stop). A task that ended `stuck`, `budget` or `unanswered` has `ok: false` and no `isError`. Always read `ok` and `stoppedBy`, not only `isError`. `get_report` turns the same rule into a verdict: `error` for the first group, `fail` for the second.

## Skill runs [#skill-runs]

`skill_smoke_uat` and the other `skill_<name>` tools start the run, wait for the verdict (30 seconds by default, `options.waitSeconds` up to 120) and answer with a summary and the raw run. For a test, `smoke_test` is easier: it answers with the [test report](#the-test-report) instead. This is what a skill tool answers:

```text
smoke-uat@1.2.1 run run_us_xxxxxxxxxxxxxxxxxxxxxxxx: failed in 7916 ms.
7 links checked (0 broken), 0 accessibility issues, 0 console errors
Reason: step 10 (assert 1 condition) failed: Expected text "Nonexistent heading" is not on the page
Counts: steps 13, cdpCalls 7, findings 0, warnings 0, assertions 6, failedSteps 1, findings.low 0, httpRequests 7, findings.high 0, findings.info 0, findings.medium 0, failedAssertions 1, findings.critical 0.
Failed steps (1):
- 9 assert 1 condition: Expected text "Nonexistent heading" is not on the page
Report (short-lived link): https://...
Full detail in structuredContent (no inline media); REST: GET /v1/skill-runs/run_us_...
For a pass/fail report call get_report with run_us_...
```

This is the output of a real run on the Test Arena pricing page. The `Reason:` line counts steps from 1 and the `Failed steps` list counts from 0: the same step is "step 10" and "9". A run that passes has no `Reason:` and no failed steps, for example `passed in 1229 ms.` and the `7 links checked ...` line.

| Field in `structuredContent` | Meaning |
|---|---|
| `status` | `running`, `passed`, `failed`, `error`, `cancelled` or `denied` |
| `summary`, `counts` | One line and the totals: steps, failed steps, assertions, findings by severity |
| `result.steps` | Each step: `label`, `ok`, `ms` and `error` |
| `result.findings` | Problems the checks found, each with a severity and often a one-line fix |
| `result.reason` | Why the run did not pass |
| `reportUrl` | A signed link to the HTML report, with screenshots of failed steps |
| `hasReport` | Whether a report exists |

What the statuses mean:

| `status` | Meaning |
|---|---|
| `passed` | Every check passed. |
| `failed` | The test ran and found a problem. Read the failed steps and findings. |
| `error` | The run could not finish, for example the browser or session failed. |
| `denied` | The run needed a permission it was not given. |
| `cancelled` | The run was stopped. |
| `running` | Not finished yet. Call `get_skill_run` with the `runId`. It waits up to the `waitSeconds` you give (up to 120). |

Results leave out inline screenshots and video, and cut long text, so they fit in your agent's context. The full run is in `GET /v1/skill-runs/<runId>` and the report.

A skill tool or `get_skill_run` sets `isError` only for `error`, `denied` and `cancelled`: the runs that could not test. A `failed` run is a normal result: the app has a problem. For the verdict with console errors and failed network calls, call `get_report` with the `runId`.

## Errors [#errors]

A call that does not run comes back as a tool result with `isError: true`. The text is `[code] message`, and `structuredContent.error` holds the same as an object.

```text
[navigation_blocked] ...
```

| Code | What to do |
|---|---|
| `navigation_blocked` | The address is not allowed: a private or `localhost` address. Use a public URL or a tunnel. |
| `capacity_exceeded` | Too many sessions open, or no browser capacity. Stop old sessions with `stop_session`, or wait. |
| `invalid_input` | The tool arguments were wrong. The message names the field. |
| `session_closed` | The session already ended. Start a new one. |
| `not_found` | No such session, task or run in this workspace. |
| `unauthorized`, `wrong_region` | The key is bad, or it belongs to another region. |
| `rate_limited` | Too many requests. Wait a moment and retry. |

The full list is in the [error reference](/reference/errors).

## Evidence and how long it lasts [#evidence-and-how-long-it-lasts]

| Evidence | How you get it | How long |
|---|---|---|
| Live view | Dashboard, **Sessions**, open the session | Only while the session is open. `run_task` closes its own session at the end unless you set `keepOpen`. |
| Test report | `smoke_test` or `get_report` | Read it again any time. The links inside it work for about 5 minutes. See [Evidence and when links stop working](#evidence-and-when-links-stop-working) |
| Skill report (HTML) | `reportSignedUrl` in the test report, or `reportUrl` in a skill result | The link works for about 5 minutes. Open or save it right away. |
| Failed-step screenshots | `screenshotUrl` of a failed step in the test report, and inside the HTML report | The same time |
| Replay video | Always for `smoke_test`. For `run_task` and `create_session`, only with `record: true` | Saved when the session closes |
| Event log | `GET /v1/sessions/<id>/logs` after the session ends | REST only; see [Evidence](/concepts/evidence) for how long it is kept |
| Share link | `POST /v1/sessions/<id>/share`: a link anyone can open | Between 1 minute and 7 days, as you choose |

A task result has no screenshots or console errors in it. The task report adds console errors and failed network calls from the session log. If you need proof with screenshots for a developer, run `smoke_test`: its report has a screenshot of every failed step and a recording. For a task, pass `record: true` to `run_task` to get a recording. [Evidence](/concepts/evidence) has the full picture.
