Skip to main content
After this guide, your agent drives a real browser for the work that needs one, and does it the cheap, boring way: the narrowest method that does the job, a browser locked to the sites it needs, a person’s approval before anything is submitted, and someone’s saved login only where it is genuinely required. The Browser reference has every option and error code; this page is about which ones to reach for. Verified against lua-cli 3.58.0. Before you begin
  • A project created with lua init and signed in with lua auth configure (Install and sign in).
  • browser: true on the agent, or at least one LuaBrowser in browsers, then lua push agent.

Pick the narrowest way in

A browser session costs money and time. Start at the top of this list and only move down when the one above cannot do it.
Browser.read opens its own session and closes it again, so it is the cheapest call in the API. Use the step-by-step commands when one page is not enough, and give them a sessionKey so they share one session:
Keep Browser.execute for the parts you cannot pin down, give it a schema so the answer has a shape, and cap it:

Name the sites, always

allowedDomains is the one setting worth having on every browser. It is checked before every command that opens a URL, it covers redirects and everything a page loads, and it is enforced on the server, so a prompt-injected page cannot talk the agent into leaving.
src/browsers/supplier.ts
Include the hosts a page loads its content from, or the page renders empty. Setting the list also keeps every page on the Lua cloud browser, because the list cannot be enforced on a remote one.

Ask before it submits, pays or sends

Put every action that changes something at the other end in confirmActions. submit covers clicking Buy, Pay, Place order, Sign in or Send, and pressing Enter.
When a run reaches one, it stops with stopReason: 'confirmation_required' and a pendingAction. Tell the person what is waiting, in their words, and approve it from their next message:
A held action cannot be approved in the same turn that held it, and it is dropped after 60 seconds. A job or a webhook has nobody to ask, so it can hold an action but never approve one: do not put a confirmed step inside a job.

Logins: ask for the least access

A saved login is the person’s own account, not the agent’s. Ask for one only when the page genuinely needs it, and ask for read unless the work really writes:
  • read can open, navigate, scroll and read. Most work is read.
  • act can click, fill, type, select and press, and the person approves each one.
  • act-auto does those without asking. Ask for it only for work the person has already said yes to as a whole, such as a nightly task they set up.
Rules worth following:
  • Never put a password in a tool, a prompt or an environment variable. There is no API that takes one. The person types it on a Lua page, and the browser fills it in.
  • Never ask for a password or a verification code in the chat. When a site asks for a code, Lua sends the person a private link; say that, and stop.
  • One login per site per person, named for the site: 'supplier', 'linkedin'. Asking again with the same name replaces that person’s login; a different name on the same site just leaves two logins to go stale. A name already saved against different origins is refused with browser_login_origin_changed.
  • Return names and states from a tool, never a receipt. The receipt carries no link, but keep the habit.

Do not loop on a failed sign-in

A saved login is tried sparingly on purpose: one attempt per 30 minutes by default, and a sign-in that did not work starts a 30-minute cooldown. Sites lock accounts out, and a retry loop spends the person’s attempts for them.
reason is already written for the person, so pass it along rather than inventing one. On too_many_attempts, say when they can try again and stop. On wrong_password, ask them to add the login again. On needs_verification, captcha or needs_approval, tell them a private link is on its way and that the run waits for them. Never start a second sign-in for the same login. A signed-in session is saved and reused next time, so a second run usually comes back with restored: true and no sign-in at all.

Let a job use a login only with a grant

A job has nobody to ask, so a login is off limits to it until the person says otherwise on a live turn. The grant names the job by its id, which Jobs.getAll gives you:
Then the job signs in as usual. Pair it with a tool that takes the grant away again:
A job that signs in without a grant is refused with browser_login_grant_required. The person can also remove the login or the grant themselves, which deletes the saved session, so handle a sign-in that stops working rather than assuming it will always be there.

Files: smallest cap, named types, mind the scan

Files are off until a browser sets maxDownloadMb. Set the smallest number that fits the work, and name the types:
src/browsers/invoices.ts
A download arrives with scanStatus: 'scanning' and cannot be opened or uploaded until the malware scan clears it:
  • There is no download by URL. Take a snapshot, find the link or button, and click it with download({ ref }).
  • Files have their own confirm categories. Add 'download', 'upload' or 'pdf' to confirmActions when the person should see each file before it moves.
  • Uploads take only the same person’s own files, and only after their scan passed. If the file is from an older message, ask the person to send it again and upload it in that turn rather than digging for an old one.
  • A file over the cap, or a run that has used up its per-run total, is refused with file_cap. Download one file per run where you can.
  • Files are not available on the Browser-Use browser (remote_session), and in a signed-in session download and upload need an act-auto login.

Page scripts only when there is no command

Browser.eval is the last resort, not a shortcut. Reach for get, snapshot, find or extract first; use a script only for something no command exposes, such as a value the page computes.
It needs allowScripts: true and an allowedDomains list naming a real domain, so a browser that can run scripts is always a browser locked to its sites. Keep that browser separate from the one that opens arbitrary pages. Scripts are developer-only: the chat model never runs them, Browser.execute never runs them, and they never run in a signed-in session. Add 'eval' to confirmActions when even your own scripts should wait for a person.

Keep sessions short

Sessions are the cost. A session stays open until you close it, its idle timeout passes (120 seconds by default), or its length limit does (10 minutes by default).
  • Use Browser.read for one page. It closes its own session.
  • Close what you open. await Browser.close({ sessionKey }) at the end of a tool, and in your error path too.
  • Do not raise the limits to cover a slow page. Raise timeoutMs on the call instead.
  • Watch concurrency. An agent may hold 3 cloud sessions at once for one person by default, and at most 3 Browser.execute runs; one too many fails with BROWSER_SESSION_LIMIT or BROWSER_EXECUTE_CONCURRENCY. Give each piece of work its own sessionKey, and do not open one per item in a loop.
  • Cap every execute. maxSteps and maxCostUsd are the only things between a confused model and a long bill.

Try it from the terminal first

Both commands run on the platform against the agent you pushed, so they show you exactly what your code will get:
lua logs --type browser is where the browser tells you the truth: which engine ran, what it was refused, which wall it hit, which files it moved and which sign-ins it tried. Read it after every failure before changing code.

When it goes wrong

Next

  • Browser — every method, saved logins, files, page scripts, limits and error codes
  • LuaBrowser — named browsers and every setting one takes
  • lua browser — the terminal commands in full
  • Logs and debugging — reading agent logs