> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Browser

> Open web pages in a real browser from runtime code, on the user's desktop or the Lua cloud browser

`Browser` drives a real Chrome browser from your code. Use it for pages that only show their content after JavaScript runs, such as single-page apps, price widgets, and dashboards, where a plain `fetch` returns an empty shell. There are three ways in:

* `Browser.read` opens one page, waits for it, and returns its title and content in one call.
* `Browser.execute` gives the browser a task in plain words and lets it work through the pages step by step.
* The step-by-step methods (`navigate`, `click`, `fill`, `get`, and the rest) drive a page one command at a time.

`Browser` is off until you turn it on with [`browser`](/reference/sdk/luaagent) on the agent, or attach a [named browser](#named-browsers). It works in tools, jobs, webhooks, triggers, device triggers, processors, web routes, and workflow code steps. It is not available in tool conditions, model resolvers, or MCP server resolvers.

```ts theme={null}
import { Browser } from 'lua-cli';
```

## Quick example

```ts src/agent.ts theme={null}
import { LuaAgent } from 'lua-cli';

export const agent = new LuaAgent({
  name: 'price-watcher',
  persona: 'You track product prices for the team.',
  browser: true,
});
```

```ts theme={null}
const page = await Browser.read('https://heylua.ai/pricing', { waitFor: 'networkidle' });
console.log(page.title, page.content.slice(0, 200));
```

To try the same read from your terminal before you write any code, run [`lua browser read`](/reference/cli/browser).

## Where pages open

Every command runs on one of two browsers:

* **The user's desktop browser**, when the Lua desktop app is running with its browser connected. Pages open with the user's own logins.
* **The Lua cloud browser**, a fresh Chrome that Lua runs for you. Each session starts empty, with no cookies or logins, and is deleted when it closes.

The `engine` setting decides which one runs:

| `engine` | Behavior |
| - | - |
| `'auto'` (default) | The desktop browser when it is online, otherwise the Lua cloud browser. When the Lua cloud browser can't get past a page (a captcha, a bot block, or an empty page), the command is retried once on Browser-Use, unless `fallback` is `'none'` or `allowedDomains` is set. |
| `'agent-browser'` | The same two browsers as `'auto'`, never Browser-Use. |
| `'browser-use'` | Browser-Use's hosted browser only. `Browser.execute` runs there as a Browser-Use task. `Browser.read` fails with `unsupported`, and the step-by-step commands (except `close`) return an `error` that starts with `BROWSER_STEPWISE_UNSUPPORTED`. |

The retry on Browser-Use applies to the commands that open or look at a page: `session_open`, `navigate`, and `snapshot`, including the ones `Browser.execute` runs. The session then stays on Browser-Use, and a `fallback` row is written to the logs. `Browser.read` does not move to Browser-Use; it returns the page it got.

Jobs, webhooks, triggers, web routes, and workflow steps run with no user at a desktop, so they always use the Lua cloud browser. When the desktop browser doesn't answer as a session opens, `Browser.read` and the commands that open a page (`session_open`, `navigate`) continue on the Lua cloud browser and a `fallback` row is written to the logs. If the desktop stops answering later, in the middle of a read or for any other command, the call fails with `session_lost`.

## Configuration

Set these on the agent's `browser` option. `browser: true` uses the defaults. A [named browser](#named-browsers) takes the same settings.

```ts theme={null}
browser: {
  engine: 'auto',
  fallback: 'browser-use',
  allowedDomains: ['heylua.ai', 'ikea.com'],
  confirmActions: ['submit'],
  logContent: 'summary',
  screenshots: 'onError',
  maxSessionMinutes: 20,
  idleTimeoutSeconds: 300,
}
```

<ParamField path="engine" type="'auto' | 'agent-browser' | 'browser-use'">
  Which browser runs the commands; see [Where pages open](#where-pages-open). Default `'auto'`.
</ParamField>

<ParamField path="fallback" type="'browser-use' | 'none'">
  What happens when the Lua cloud browser can't get past a page. `'browser-use'` (default) retries the command once on Browser-Use. `'none'` keeps every page on the Lua cloud browser, and the command returns the page it got. Browser-Use is never used when `engine` is `'agent-browser'` or `allowedDomains` is set.
</ParamField>

<ParamField path="proxyCountry" type="string">
  A two-letter country code, such as `'gb'` or `'de'`, for the country Browser-Use browses from. It applies only to work that runs on Browser-Use.
</ParamField>

<ParamField path="allowedDomains" type="string[]">
  Hosts the browser may open. List each host the pages need, or use a wildcard such as `*.ikea.com` for a domain and its subdomains. The list is checked before every command that opens a URL, `Browser.read` and `Browser.execute` included; a blocked URL fails with `BROWSER_DOMAIN_NOT_ALLOWED`. On the Lua cloud browser it also applies to redirects and everything a page loads (scripts, images, requests), so include the hosts a page loads its content from. On the desktop browser, `Browser.read` also checks the page it ended on after redirects and fails with `BROWSER_DOMAIN_NOT_ALLOWED` when that host is not allowed. Empty or unset allows all public sites. Setting it turns off the Browser-Use retry, because the list can't be enforced on a remote browser.
</ParamField>

<ParamField path="confirmActions" type="string[]">
  Actions that must be approved before they run. `Browser.execute` checks these categories: `navigate`, `click`, `fill`, `type`, `select`, `press`, `scroll`, and `submit`. `submit` covers clicking a button such as Buy, Pay, Place order, Sign in, or Send, and pressing Enter. When a run reaches one of them it stops with `stopReason: 'confirmation_required'` and a `pendingAction`; see [execute](#execute-input). For the step-by-step commands the list goes to the browser, which holds a matching command until you call `Browser.confirm({ id })` or `Browser.deny({ id })`. On engine `'browser-use'`, a non-empty list makes `Browser.execute` fail with `BROWSER_USE_CONFIRM_UNSUPPORTED`, because Browser-Use runs can't pause for approval.
</ParamField>

<ParamField path="logContent" type="'summary' | 'full' | 'none'">
  How much page content the browser log rows keep: `'summary'` (default) keeps titles and sizes, `'full'` also keeps page text (shortened), `'none'` keeps neither.
</ParamField>

<ParamField path="screenshots" type="'always' | 'onError' | 'never'">
  When a log row carries a screenshot reference. Default `'onError'`.
</ParamField>

<ParamField path="maxSessionMinutes" type="number">
  How long a Lua cloud browser session may stay open, in whole minutes. Default 10, maximum 60. On `browser`, a larger value is cut to 60 when the session opens. On a named browser, a value over 60 fails `lua compile`.
</ParamField>

<ParamField path="idleTimeoutSeconds" type="number">
  How long a Lua cloud browser session may go without a command before it closes, in whole seconds. Default 120, maximum 900. On `browser`, a larger value is cut to 900 when the session opens. On a named browser, a value over 900 fails `lua compile`.
</ParamField>

`credentials` is accepted but not enforced yet. A named browser also accepts `maxConcurrentSessions` (1 to 100) and `persist`; both are stored and not enforced yet.

## Named browsers

A named browser is a `LuaBrowser` with its own settings. Attach one or more to the agent with `browsers`, then pick one per call with `browser: '<name>'`. Use them when different jobs need different rules, such as one browser locked to a supplier site and another that can open anything.

```ts src/browsers/supplier.ts theme={null}
import { LuaBrowser } from 'lua-cli';

export const supplierPortal = new LuaBrowser({
  name: 'supplier-portal',
  description: 'Checks stock on the supplier site',
  allowedDomains: ['ikea.com'],
  confirmActions: ['submit'],
  fallback: 'none',
  maxSessionMinutes: 30,
});
```

```ts src/agent.ts theme={null}
import { LuaAgent } from 'lua-cli';
import { supplierPortal } from './browsers/supplier';

export const agent = new LuaAgent({
  name: 'stock-checker',
  persona: 'You check stock levels for the store team.',
  browsers: [supplierPortal],
});
```

```ts theme={null}
const page = await Browser.read('https://www.ikea.com/gb/en/p/billy-bookcase-white-00263850/', {
  browser: 'supplier-portal',
  waitFor: 'networkidle',
});
```

* **Attaching a browser turns the browser on.** `browser: true` is not needed when `browsers` has at least one entry.
* **Every method takes `browser`.** `Browser.read` takes it in its options; `Browser.execute` and the step-by-step methods take it in their input object.
* **Calls without `browser`** use the `browser` option's settings when the agent sets `browser`, and the first browser in `browsers` when it doesn't.
* **An unknown name** fails with `BROWSER_PROFILE_NOT_FOUND`: `Browser.read` throws it, and the other methods return it in `code`.
* **Names** start with a letter or digit and use only letters, digits, `_`, and `-`, up to 64 characters. The constructor throws on any other name. `defineBrowser({...})` is the same as `new LuaBrowser({...})`.
* **`lua compile` checks every setting.** An unknown `engine` or `fallback`, a `proxyCountry` that isn't two letters, a limit that isn't a whole number in range, two browsers with the same name, or an entry in `browsers` that isn't a `LuaBrowser` fails the compile.

Push a browser with `lua push browser --name supplier-portal`, then `lua push agent` so the agent picks up its `browsers` list. `lua push all` pushes browsers with everything else. [`lua browsers`](/reference/cli/browser#lua-browsers) lists and deletes them.

## Methods

### read(url, options?)

Reads one page in a new browser session and closes that session when it is done, whether the read succeeded or not. Steps: open the URL, wait for `waitFor` if given, then read the page title, the final URL, and the content of `selector`.

```ts theme={null}
Browser.read(url: string, options?: BrowserApiReadOptions): Promise<BrowserReadResult>
```

<ParamField path="url" type="string" required>
  An absolute `http://` or `https://` URL.
</ParamField>

<ParamField path="options.waitFor" type="'load' | 'networkidle' | string">
  What to wait for after the page opens. `'load'` waits for the load event, `'networkidle'` waits until the page stops making requests, and any other string is a CSS selector to wait for, such as `'.price'`. Unset reads as soon as the page has opened.
</ParamField>

<ParamField path="options.selector" type="string">
  CSS selector of the element to read. Default `'body'`, the whole page. On the Lua cloud browser with `format: 'text'`, every match is read, in page order, up to 200 matches, joined by a blank line, with empty matches skipped. With `format: 'html'` the cloud browser reads the first match. The desktop browser reads the first match in both formats.
</ParamField>

<ParamField path="options.format" type="'text' | 'html'">
  `'text'` (default) returns the visible text, `'html'` returns the element's inner HTML.
</ParamField>

<ParamField path="options.timeoutMs" type="number">
  Time budget for the whole read, from opening the browser session to the last read. Default 60000. A value under 1000 is raised to 1000 and a value over 100000 is cut to 100000. When the budget runs out, the read fails with `timeout`.
</ParamField>

<ParamField path="options.sessionKey" type="string">
  A label for the session in your logs. Every read still gets its own new session.
</ParamField>

<ParamField path="options.browser" type="string">
  The [named browser](#named-browsers) to read with.
</ParamField>

**Returns**

<ResponseField name="result" type="BrowserReadResult">
  <Expandable title="properties">
    <ResponseField name="url" type="string">The URL you asked for.</ResponseField>
    <ResponseField name="finalUrl" type="string">The URL the page ended on, after redirects.</ResponseField>
    <ResponseField name="title" type="string">The page title, or an empty string.</ResponseField>
    <ResponseField name="content" type="string">The text or HTML of `selector`, at most 100,000 characters.</ResponseField>
    <ResponseField name="format" type="'text' | 'html'">The format `content` is in.</ResponseField>
    <ResponseField name="truncated" type="boolean">`true` when `content` was cut at 100,000 characters, or when not every match of `selector` was read.</ResponseField>
    <ResponseField name="status" type="number">The HTTP status of the page, such as `200` or `404`. A 404 page that has content is still read, so check `status` before you trust `content`. Missing when the browser could not see the response.</ResponseField>
    <ResponseField name="matches" type="number">How many elements matched `selector`. Set only when you passed a selector other than `'body'`.</ResponseField>
    <ResponseField name="blocked" type="true">Set when the page is a wall instead of the page you asked for.</ResponseField>
    <ResponseField name="blocker" type="'login_required'">The URL redirected to a sign-in page, so `content` is the sign-in form.</ResponseField>
    <ResponseField name="blockerKind" type="'login'">The kind of wall.</ResponseField>
    <ResponseField name="blockerGuidance" type="string">What to do about the wall, such as reading the page on the desktop browser where the user is signed in.</ResponseField>
  </Expandable>
</ResponseField>

**Errors** — `read` throws an error with `name: 'BrowserError'`, a `code`, and for `http_error` the page's `status`; see [Errors](#errors). Closing the session after the read takes up to 10 more seconds.

#### In a tool

```ts src/tools/ReadPageTool.ts theme={null}
import { LuaTool, Browser } from 'lua-cli';
import { z } from 'zod';

export default class ReadPageTool implements LuaTool {
  name = 'read_page';
  description = 'Read the text of a web page that needs JavaScript to render';
  inputSchema = z.object({ url: z.string().url() });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const page = await Browser.read(input.url, { waitFor: 'networkidle' });
    if (page.blocked) return { blocked: true, guidance: page.blockerGuidance };
    return { title: page.title, status: page.status, text: page.content, truncated: page.truncated };
  }
}
```

#### In a scheduled job

```ts src/jobs/PriceCheckJob.ts theme={null}
import { LuaJob, Browser, Data } from 'lua-cli';

export default new LuaJob({
  name: 'price-check',
  description: 'Record the price of a tracked product every morning',
  schedule: { type: 'cron', expression: '0 8 * * *', timezone: 'Europe/London' },
  timeout: 180,
  async execute() {
    const page = await Browser.read('https://www.ikea.com/gb/en/p/billy-bookcase-white-00263850/', {
      waitFor: '.pip-price__integer',
      selector: '.pip-price__integer',
    });
    const price = page.content.split('\n\n')[0].trim();
    await Data.create('prices', { item: '00263850', price, at: new Date().toISOString() });
    return { price };
  },
});
```

#### In a webhook

```ts src/webhooks/PageChangedWebhook.ts theme={null}
import { LuaWebhook, Browser } from 'lua-cli';
import { z } from 'zod';

const bodySchema = z.object({ url: z.string().url() });

export default new LuaWebhook({
  name: 'page-changed',
  description: 'Reads the page a monitoring service reports as changed',
  bodySchema,
  execute: async (event) => {
    const parsed = bodySchema.safeParse(event.body);
    if (!parsed.success) return { ok: false, error: 'invalid body' };
    try {
      const page = await Browser.read(parsed.data.url, { waitFor: 'load', timeoutMs: 30000 });
      return { ok: true, title: page.title, length: page.content.length };
    } catch (error) {
      const { code, status } = error as { code?: string; status?: number };
      return { ok: false, code, status };
    }
  },
});
```

### execute(input)

Gives the browser a task in plain words. A model looks at the page, picks the next action (navigate, click, fill, read), and repeats until the task is done or a limit is reached. Use it when you can describe what you want but not the exact clicks.

```ts theme={null}
Browser.execute(input: BrowserExecuteInput): Promise<BrowserExecuteResult>
```

```ts theme={null}
const run = await Browser.execute({
  task: 'Find the monthly price of the Pro plan',
  startUrl: 'https://heylua.ai/pricing',
  schema: {
    type: 'object',
    properties: { plan: { type: 'string' }, monthlyPrice: { type: 'string' } },
    required: ['plan', 'monthlyPrice'],
  },
  maxSteps: 10,
  maxCostUsd: 0.25,
});
if (run.status !== 'completed') throw new Error(`${run.code ?? run.stopReason}: ${run.error}`);
return run.output;
```

#### execute input

<ParamField path="task" type="string" required>
  What the browser should do, in plain words. An empty task rejects with `name: 'BrowserExecuteError'` and code `BROWSER_EXECUTE_INVALID_INPUT`.
</ParamField>

<ParamField path="startUrl" type="string">
  The page the run starts on. Unless you set `allowedDomains` or `allowAnyDomain`, and the agent has no `allowedDomains` of its own, the run stays on this page's domain.
</ParamField>

<ParamField path="schema" type="object">
  A JSON Schema the `output` must match. The model gets a chance to fix output that doesn't match; if it still doesn't, the run fails with `BROWSER_OUTPUT_SCHEMA_MISMATCH`. A schema that is not an object rejects with `BROWSER_EXECUTE_INVALID_INPUT`.
</ParamField>

<ParamField path="maxSteps" type="number">
  The most actions the run may take. Default 25, between 1 and 60.
</ParamField>

<ParamField path="maxCostUsd" type="number">
  The most the run may spend on the model, in US dollars. Default 1, at most 10. Zero or less fails with `invalid_argument`.
</ParamField>

<ParamField path="timeoutMs" type="number">
  The most time the run may take. Default 300000 (5 minutes), at most 900000 (15 minutes).
</ParamField>

<ParamField path="model" type="string">
  A [model code](/concepts/models) to drive the run. Unset uses the platform's default. A model the agent can't use fails with `MODEL_NOT_AVAILABLE`.
</ParamField>

<ParamField path="sessionKey" type="string">
  Run in the session with this key, for example one you opened and signed in to with the step-by-step methods. That session stays open after the run. Without it, the run opens its own session and closes it at the end.
</ParamField>

<ParamField path="allowedDomains" type="string[]">
  Hosts this run may visit. When the agent has its own `allowedDomains`, only hosts on both lists are allowed.
</ParamField>

<ParamField path="allowAnyDomain" type="boolean">
  `true` lets the run leave the `startUrl` domain. The agent's `allowedDomains` still apply.
</ParamField>

<ParamField path="browser" type="string">
  The [named browser](#named-browsers) to run with.
</ParamField>

Other values out of range for `maxSteps`, `maxCostUsd`, and `timeoutMs` are moved to the nearest allowed value.

#### execute result

`execute` returns a result instead of throwing when the run fails. Check `status`.

<ResponseField name="result" type="BrowserExecuteResult">
  <Expandable title="properties">
    <ResponseField name="status" type="'completed' | 'failed' | 'stopped'">`completed` when the task finished, `stopped` when a limit, a cancel, or a confirmation ended it, `failed` otherwise.</ResponseField>
    <ResponseField name="output" type="unknown">The answer. It matches `schema` when you passed one; `null` when there is none.</ResponseField>
    <ResponseField name="steps" type="number">How many steps ran.</ResponseField>
    <ResponseField name="stopReason" type="string">Why the run ended: `done`, `model_failed`, `max_steps`, `cost_cap`, `timeout`, `confirmation_required`, `invalid_action`, `repeated_action`, `schema_mismatch`, `browser_error`, `cancelled`, or `error`.</ResponseField>
    <ResponseField name="costUsd" type="number">What the run spent, in US dollars.</ResponseField>
    <ResponseField name="finalUrl" type="string">The page the run ended on.</ResponseField>
    <ResponseField name="engine" type="'local' | 'cloud-agent-browser' | 'cloud' | 'none'">Where it ran: the desktop browser, the Lua cloud browser, Browser-Use, or nowhere (it failed before a browser opened).</ResponseField>
    <ResponseField name="error" type="string">What went wrong, when it did.</ResponseField>
    <ResponseField name="code" type="string">An error code; see [Errors](#errors).</ResponseField>
    <ResponseField name="pendingAction" type="object">With `stopReason: 'confirmation_required'`: the `category`, the `action`, the `url`, and, when the browser is holding the action, a `confirmationId` for `Browser.confirm({ sessionKey, id })`. Without a `confirmationId`, nothing is waiting; approve the action with the user and run the task again.</ResponseField>
    <ResponseField name="sessionKey" type="string">Set when the run left its session open, such as while an action waits for approval.</ResponseField>
    <ResponseField name="history" type="object[]">One entry per step: `step`, `intent`, `action`, `url`, `ok`, and `error` when the step failed.</ResponseField>
    <ResponseField name="liveUrl" type="string">A link to watch the browser, on Browser-Use runs that report one.</ResponseField>
  </Expandable>
</ResponseField>

An agent runs at most 3 `execute` runs at a time for each user. Another run fails with `BROWSER_EXECUTE_CONCURRENCY`. On engine `'browser-use'`, `execute` runs as a Browser-Use task: `steps` is `0`, there is no `history`, and `confirmActions` must be empty.

### Step-by-step commands

For pages you need to interact with, `Browser` also has one method per browser command. Each takes one object, with an optional `sessionKey` and `browser`, and returns `{ engine, data?, error?, code? }`. These methods do not throw when a command fails; check `error` and `code`.

```ts theme={null}
const s = { sessionKey: 'store' };
await Browser.navigate({ ...s, url: 'https://www.ikea.com/gb/en/p/billy-bookcase-white-00263850/' });
await Browser.fill({ ...s, selector: '#postcode', text: 'SW1A 1AA' });
await Browser.click({ ...s, selector: 'button[type=submit]' });
const stock = await Browser.get({ ...s, what: 'text', selector: '.stock-status' });
if (stock.error) throw new Error(`${stock.code}: ${stock.error}`);
await Browser.close(s);
```

| Group | Methods |
| - | - |
| Session and navigation | `health`, `session_open`, `navigate`, `back`, `forward`, `reload`, `pushstate`, `close` |
| Reading the page | `snapshot`, `get`, `is` |
| Interaction | `click`, `dblclick`, `fill`, `type`, `press`, `hover`, `focus`, `select`, `check`, `uncheck`, `scroll`, `scrollintoview`, `drag`, `upload`, `find`, `act` |
| Waiting | `wait` |
| Tabs and frames | `tab`, `window_new`, `frame` |
| Capture | `screenshot`, `pdf` |
| Page state | `cookies`, `storage`, `set`, `download`, `clipboard`, `state` |
| Other | `extract`, `auth`, `confirm`, `deny`, `network`, `console`, `errors`, `mouse`, `keyboard` |

The input type of every method is exported as `BrowserCommandInputs['<method>']`. Commands with the same `sessionKey` share one session for the length of one conversation or run. A command that needs a page before one is open fails with `no_page`; open one with `navigate` or `session_open` first. On the Lua cloud browser, these commands are not available and fail with `unsupported`: `upload`, `extract`, `pdf`, `download`, `auth`, `state`, `act` with a natural-language `instruction`, `wait` with `fn`, `screenshot` with `path`, and `network` with `action: 'har'`. On engine `'browser-use'`, every command except `close` returns an `error` that starts with `BROWSER_STEPWISE_UNSUPPORTED`.

## Limits

The session limits are defaults. Raise them with `maxSessionMinutes` and `idleTimeoutSeconds` on `browser` or on a named browser, up to the maximum. The `Browser.read` and `Browser.execute` limits are set per call. The concurrency limits are set by the platform.

| Limit | Default | Maximum | Set with |
| - | - | - | - |
| Lua cloud browser session length | 10 minutes | 60 minutes | `maxSessionMinutes` |
| Lua cloud browser idle time | 120 seconds | 900 seconds | `idleTimeoutSeconds` |
| `Browser.read` time | 60 seconds | 100 seconds | `timeoutMs` |
| `Browser.read` content | 100,000 characters | 100,000 characters | — |
| `Browser.read` matches read for `selector` | 200 | 200 | — |
| `Browser.execute` steps | 25 | 60 | `maxSteps` |
| `Browser.execute` spend | 1 USD | 10 USD | `maxCostUsd` |
| `Browser.execute` time | 5 minutes | 15 minutes | `timeoutMs` |
| `Browser.execute` runs at once | 3 per agent for each user | 3 | — |
| [`lua browser`](/reference/cli/browser) reads and runs at once | 2 per agent | 2 | — |
| `lua browser run` time | 5 minutes | 5 minutes | — |

When a Lua cloud browser session reaches its length or idle limit it closes, and the next command fails with `session_lost`; open the page again. The Lua cloud browser never opens private or internal network addresses.

## Logs

Every browser command writes rows to the agent's logs under the `browser` source:

```bash theme={null}
lua logs --type browser
```

| Row | Written when |
| - | - |
| `session.opened` | A session opens: which browser, why, and its limits. |
| `command` | Once per call. `Browser.read` writes a single `command` row named `read` inside its own session, not one row per step. |
| `agent.step` | Once per step of `Browser.execute`: the step number, the action, its intent, and the page. |
| `navigation` | The Lua cloud browser loads a page. |
| `blocked` | A page or request was refused, such as a host outside `allowedDomains` or a private address. |
| `wall` | A page shows a captcha, a bot check, a cookie consent wall, a sign-in page, or no content. |
| `fallback` | Work moved from the desktop to the Lua cloud browser, or a page moved from the Lua cloud browser to Browser-Use. |
| `confirm` | An action needs approval, and later whether it was approved, denied, or expired. |
| `session.closed` | A session closes: why, how long it ran, and how many pages it visited. |

Arguments that look like secrets are redacted. Page text appears only with `logContent: 'full'`. Rows written by [`lua browser`](/reference/cli/browser) carry the `dev` channel.

## Errors

`Browser.read` throws an error with `name: 'BrowserError'` and one of these codes. `Browser.execute` and the step-by-step methods return the code in `code` instead. Every method rejects with `BROWSER_DISABLED` and `BROWSER_UNAVAILABLE`.

| Code | Meaning |
| - | - |
| `BROWSER_DISABLED` | The agent has no `browser` option and no named browsers. Add `browser: true` to the agent. |
| `BROWSER_UNAVAILABLE` | Called where the browser can't run: a tool condition, a model or MCP resolver, or `lua test`. |
| `BROWSER_DOMAIN_NOT_ALLOWED` | The URL, or for `Browser.read` on the desktop the page it was redirected to, is not in `allowedDomains`. |
| `BROWSER_PROFILE_NOT_FOUND` | No browser with that `browser` name is attached to the agent. Add it to `browsers` and push again. |
| `BROWSER_WORKER_NOT_CONFIGURED` | The cloud browser is not available in this environment. |
| `invalid_argument` | A bad argument, such as a URL that is not `http(s)`, an unknown `format`, a selector starting with `-`, or a `maxCostUsd` of zero or less. |
| `unsupported` | The command can't run on this browser, such as `Browser.read` with `engine: 'browser-use'`. |
| `http_error` | The page answered an HTTP error status with nothing to read. The error's `status` has the status when it is known. |
| `timeout` | The page or `waitFor` did not finish within the time budget. |
| `no_page` | A command needs a page, and none is open in the session yet. |
| `session_lost` | The browser session ended (idle or length limit on the cloud, or the desktop stopped answering). Open the page again. |
| `session_cap` | The cloud browser is at capacity. Try again shortly. |
| `unreachable` | The cloud browser could not be reached. Try again shortly. |
| `engine_unavailable` | The browser could not be started. Try again shortly. |
| `blocked_domain`, `blocked_private_host`, `blocked_scheme` | The cloud browser refused the page. |
| `engine_error` | The browser reported an error, such as a selector that was never found. The message has the details. |

On engine `'browser-use'`, the step-by-step commands return no `code`; their `error` starts with `BROWSER_STEPWISE_UNSUPPORTED`.

`Browser.execute` adds these codes:

| Code | Meaning |
| - | - |
| `BROWSER_EXECUTE_INVALID_INPUT` | The `task` is empty or `schema` is not an object. `execute` rejects with `name: 'BrowserExecuteError'`. |
| `BROWSER_EXECUTE_CONCURRENCY` | The agent already has 3 runs going for this user. Wait for one to finish. |
| `BROWSER_OUTPUT_SCHEMA_MISMATCH` | The output did not match `schema`. `stopReason` is `schema_mismatch`, and `output` holds what the model returned. |
| `BROWSER_BOT_WALL` | The run hit a captcha or bot check it could not get past. |
| `MODEL_NOT_AVAILABLE` | The `model` is not available to this agent. |
| `BROWSER_USE_CONFIRM_UNSUPPORTED` | Engine `'browser-use'` with a non-empty `confirmActions`. The task was not started. |
| `BROWSER_USE_TIMEOUT`, `BROWSER_USE_CANCELLED`, and other `BROWSER_USE_` codes | The Browser-Use task timed out, was cancelled, or failed. The message has the details. |

## Testing

`lua test` does not open a browser: every `Browser` method, `execute` included, rejects with `BrowserError` with code `BROWSER_UNAVAILABLE`. Try a page or a task from the terminal with [`lua browser read` and `lua browser run`](/reference/cli/browser), or push the agent and run the tool, job, or webhook on the platform, then read `lua logs --type browser`.

## Types

These types are exported from `lua-cli`:

* `BrowserApi`, `BrowserStepwiseApi`, `BrowserStepwiseCommandName`, `BrowserSessionInput`, `BrowserCommandInputs`, `BrowserCommandResult`
* `BrowserApiReadOptions`, `BrowserReadOptions`, `BrowserReadResult`, `BrowserReadFormat`, `BrowserReadLoadState`
* `BrowserExecuteInput`, `BrowserExecuteResult`, `BrowserExecuteStatus`, `BrowserExecuteStopReason`, `BrowserExecuteStep`, `BrowserExecutePendingAction`
* `BrowserErrorCode`, `BrowserErrorFields`
* `BrowserSwitchConfig`, `LuaBrowserConfig`, `LuaBrowserSettings`, `BrowserProfileOption`

## See also

* [`LuaAgent`](/reference/sdk/luaagent) — the `browser` and `browsers` options
* [`lua browser`](/reference/cli/browser) — read a page or run a task from the terminal, and list or delete named browsers
* [Logs and debugging](/ship/logs-and-debugging) — reading agent logs


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.