---
title: Structured output
description: Pass an output schema and get validated JSON in data, built only from the pages the agent visited.
---

Every run returns a text `answer`. Add an `output_schema` and the run also returns JSON in `data` that matches it. The JSON is built from the text and links of the pages the agent visited and from its answer, never from guesses, so link URLs are the real ones from the page. Structured output costs nothing extra.

## Get JSON with output_schema

Pass the shape you want. The short form maps each field to a type:

```bash title="cURL"
curl https://lovelyrouter.com/v1/browser/runs \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "goal": "Get the top 3 stories on the front page.",
    "start_url": "https://news.ycombinator.com/",
    "output_schema": {
      "stories": [{"title": "string", "url": "string", "points": "integer"}]
    }
  }'
```

```python title="Python"
run = requests.post(f"{API}/runs", headers=H, json={
    "goal": "Get the top 3 stories on the front page.",
    "start_url": "https://news.ycombinator.com/",
    "output_schema": {
        "stories": [{"title": "string", "url": "string", "points": "integer"}],
    },
}).json()

while run["status"] == "running":
    run = requests.get(f"{API}/runs/{run['id']}", headers=H, params={"wait": 30}).json()

for story in run["data"]["stories"]:
    print(story["points"], story["title"], story["url"])
```

```js title="Node.js"
let run = await fetch(`${API}/runs`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    goal: 'Get the top 3 stories on the front page.',
    start_url: 'https://news.ycombinator.com/',
    output_schema: {
      stories: [{ title: 'string', url: 'string', points: 'integer' }],
    },
  }),
}).then((r) => r.json());

while (run.status === 'running') {
  run = await fetch(`${API}/runs/${run.id}?wait=30`, { headers }).then((r) => r.json());
}
for (const s of run.data.stories) console.log(s.points, s.title, s.url);
```

The finished run carries the JSON in `data`, next to the `answer`:

```json
{
  "id": "brun_8f3c2d1e0a9b8c7d6e5f4a3b",
  "object": "browser_run",
  "status": "succeeded",
  "steps": 1,
  "answer": "The top 3 stories are \"Bitwarden Dual License Model\" (95 points), \"Talorys – A self-hosted personal AI agent\" (57 points) and \"REA Reverse – Engineer Anything\" (498 points).",
  "data": {
    "stories": [
      { "title": "Bitwarden Dual License Model", "url": "https://community.bitwarden.com/…", "points": 95 },
      { "title": "Talorys – A self-hosted personal AI agent", "url": "https://github.com/rociiu/talorys", "points": 57 },
      { "title": "REA Reverse – Engineer Anything", "url": "https://rea.tools/", "points": 498 }
    ]
  },
  "charge_usd": 0.006
}
```

The Python and Node.js samples use the `API`, `H` and `headers` setup from [Run your first task](/docs/browser-agent#run-your-first-task).

## Shorthand schema

The shorthand is plain JSON where each value describes a field:

| You write | You get |
| --- | --- |
| `"string"`, `"number"`, `"integer"`, `"boolean"`, `"array"`, `"object"`, `"null"` | A value of that type, or `null` when the page doesn't show it |
| Any other text, e.g. `"price as shown, with currency"` | A string, or `null`. Your text tells the agent what to put there |
| `["string"]`, `[{...}]` | A list of that item. `[]` is a list of anything |
| `{"field": ...}` | An object with exactly those fields, all present |

A description makes the field a string: `"price": "price in USD"` returns `"$19.99"`, not a number. Use `"number"` when you want a number.

If the top level of your schema has a key named `type`, `properties`, `items` or `$schema`, it is read as a full JSON Schema, not as shorthand. To return a field with one of those names, write a full JSON Schema.

## Full JSON Schema

For enums, formats, required fields or descriptions on typed fields, pass a JSON Schema (draft 2020-12):

```json
{
  "goal": "Find the 2026 Toyota Camry trims on the build page with their starting MSRP and engine.",
  "start_url": "https://www.toyota.com/camry/",
  "output_schema": {
    "type": "object",
    "properties": {
      "trims": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "name": { "type": "string" },
            "msrp_usd": { "type": "number", "description": "Starting MSRP, without fees" },
            "engine": { "type": "string" },
            "hybrid": { "type": "boolean" }
          },
          "required": ["name", "msrp_usd"]
        }
      }
    },
    "required": ["trims"]
  }
}
```

The result is validated against the schema. If the first attempt doesn't validate, the agent gets the validation errors and fixes it once. The schema can be up to 20 KB. A schema that isn't valid JSON Schema returns `400` at start: `"output_schema" is not a usable JSON Schema: …`.

## Read data and data_error

| Field | When |
| --- | --- |
| `data` | The JSON, once the run has ended. `null` while it runs, when you asked for no structure, or when extraction failed. |
| `data_error` | Present only when extraction failed, with the reason, e.g. `extracted data does not match the schema: …` followed by the failing fields. |

Values the pages don't show come back as `null`, and lists come back empty, rather than made up. Secret values never appear in `data`: they are replaced with `[secret:<name>]`.

Check `data_error` before you use `data`:

```python title="Python"
if run.get("data_error"):
    raise RuntimeError(f"no data: {run['data_error']}")
trims = run["data"]["trims"]
```

```js title="Node.js"
if (run.data_error) throw new Error(`no data: ${run.data_error}`);
const { trims } = run.data;
```

## Describe the format in the goal instead

You can also ask for a structure in the goal itself, for example "Return a JSON list of the open positions with title, team and location." When the goal asks for structured data in a specific shape (JSON, named fields, a list or table with given columns), the run fills `data` in that shape too. Without a schema there is nothing to validate against, so use `output_schema` when your code depends on the exact shape.

## When the run ends early

- **Step limit.** A run that reaches `max_steps` ends as `failed` with `reached the step limit (N)`. It still writes an `answer` from the last page and fills `data` from what it saw.
- **Failed or timed out.** `data` is filled from the pages visited so far, so it may be partial.
- **Stopped.** A run you stop returns no `data`.

## Get better data

- **Name fields after what the page calls them.** Field names guide the extraction, and the top-level names are added to the agent's instructions so it ends on the pages that show them. `points` and `comments` work better than `score` and `n`.
- **Ask in the goal for everything in the schema.** "Get the top 5 stories with title, link and points" plus a matching schema beats a schema alone.
- **Keep the pages few.** Data comes from the last six pages the agent visited. For data spread over many pages, run one task per page or per group of pages.
- **Make optional fields optional.** In a full schema, list only the fields you always need in `required`. Shorthand fields can always be `null`.
- **Use accurate mode for long tables.** It can read a long table or list with page code, not only from the visible text. See [Modes](/docs/browser-agent/modes#choose-a-mode).

## Examples

Order confirmation after submitting a form:

```json
{
  "goal": "Order one Blue Mug (size large) for Ada Lovelace, 12 Main St, Springfield, IL 62701, and tell me the order number and total.",
  "start_url": "https://shop.example.com/mugs/blue",
  "secrets": ["shop_user", "shop_password"],
  "output_schema": { "order_number": "string", "total": "number", "currency": "string" }
}
```

With the default `"approve": "risky"`, this run pauses for your approval before it places the order. See [Human in the loop](/docs/browser-agent/human-in-the-loop#approve-before-submitting).

Search results with nested fields:

```json
{
  "goal": "Search for 'noise cancelling headphones' and get the first 5 results with name, price, rating and number of reviews.",
  "start_url": "https://www.example-store.com/",
  "output_schema": {
    "results": [
      { "name": "string", "price": "number", "rating": "number", "reviews": "integer", "url": "string" }
    ]
  }
}
```
