Lovely RouterDocs
Get a key
Browser agent

Structured output

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:

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"}]
    }
  }'

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.

Shorthand schema#

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

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

FieldWhen
dataThe JSON, once the run has ended. null while it runs, when you asked for no structure, or when extraction failed.
data_errorPresent 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:

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

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.

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.

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" }
    ]
  }
}