How to Build Your First Browser Agent with Browser Use 0.13 in Python: Setup, Limits, Structured Output

Read time: ~12 minutes. Key facts:

  • Browser Use is an MIT-licensed Python library that runs an AI agent against a real Chromium browser. The current PyPI release is 0.13.11 (published October 7, 2026) and needs Python 3.11 or newer.
  • Install path from the official docs: uv add browser-use (or uv pip install browser-use), then uvx browser-use install to download Chromium.
  • You bring the model: ChatBrowserUse (the vendor’s bu-2-0 model, $0.60 / $3.50 per 1M input/output tokens), or ChatOpenAI, ChatAnthropic, ChatGoogle, ChatOllama and other wrappers with your own key.
  • The two settings every first agent needs: allowed_domains on the browser and an explicit max_steps on run(). In 0.13.11 the code default for max_steps is 500, even though the configuration page still says 100.
  • history.is_done() only means the agent stopped. Browser Use’s own docs say to verify important outcomes yourself.

What we tested: we installed 0.13.11 with uv on Linux (Python 3.12), ran the Chromium installer, launched a headless browser session from Python, and checked every class, parameter and default below against the installed package. We did not run a paid model, so no task-success numbers here are ours.

If you’ve wanted an agent that can open a site, find what you need and hand it back as clean JSON, the open-source Browser Use library is the most direct way to build one in Python. This guide takes you from an empty folder to a locked-down, read-only agent that returns validated structured data, and it covers the settings that keep a first agent from wandering, overspending or quietly getting things wrong.

Browser Use published an explainer on October 18, 2026, What Is a Browser Agent?, and its practical advice is a good frame for this tutorial: start with one read-only task, define which pages the agent may visit and what counts as success, set a time, step or cost limit, and compare results against the source pages before you expand. Each step below turns one of those rules into code.


1. When a browser agent is the right tool

Before writing any code, check that you actually need an agent. Browser Use’s own guidance is blunt about it:

Your situationUse
The service has an API that supports the taskThe API. It’s faster, cheaper and deterministic.
No API, but the steps are the same every time (e.g. download the same report every Monday)A script in Playwright, Selenium or Puppeteer.
The steps change from site to site, or you can’t write them down in advanceA browser agent.

Scripts and API calls are deterministic: the same inputs give the same decisions. Agent decisions are probabilistic, so the same task can take different paths on different runs. That’s what makes agents useful for unpredictable pages, and it’s also why you need limits and verification, which this guide builds in from the start.

Browser Use also reports what people actually run on its hosted service. In a snapshot it checked on October 18, 2026, extracting structured data was 14% of runs, getting past a login 18%, open-ended research 6%, job applications 6%, finding filings and reports 5%, and booking or buying 3% (15% was unclassified). Those shares describe usage, not success rates.

How an agent works, in one paragraph. A browser agent is a loop: observe the page, pick an action, execute it, check the result, and repeat until the goal is met or a limit is hit. It has three parts: a model that decides the next action, a harness that runs the loop and the tools, and a browser that holds tabs, cookies and login state. Browser Use is the harness. It reads the page through the DOM and, when vision is on, through screenshots, and it drives Chromium over the Chrome DevTools Protocol.


2. Prerequisites

  • Python 3.11+. PyPI lists requires_python >=3.11,<4.0 for 0.13.11. The README uses Python 3.12.
  • uv, the Python package manager the docs use. If you don’t have it, the quickstart installs it with pip install uv.
  • An API key for one model provider. Pick one in step 4. A local model through Ollama works too, with no key, if your hardware can run it.
  • About 5 minutes and a few hundred MB of disk for Chromium and its dependencies.

3. Install Browser Use and Chromium

Create a project and add the package. These are the commands from the official README:

mkdir my-browser-agent && cd my-browser-agent
uv init --python 3.12
uv add browser-use

If you’d rather use a plain virtual environment, the docs’ Human Quickstart does the same thing this way:

pip install uv
uv venv --python 3.12
source .venv/bin/activate        # On Windows: .venv\Scripts\activate
uv pip install browser-use

Then download the browser:

uvx browser-use install

Inside a uv project, uv run browser-use install runs the same installer from your project’s environment. On our Linux box it downloaded Playwright’s FFmpeg and Chromium plus system dependencies, then printed Installation complete.

Check the install:

uv run python -c "import importlib.metadata as m; print(m.version('browser-use'))"
# 0.13.11

Good to know on first launch: Browser Use loads a few automation extensions by default (uBlock Origin Lite, a cookie-banner handler and a background-tab helper), and it downloads them the first time a browser starts. If you’re behind a strict proxy or want a bare browser, set enable_default_extensions=False on Browser(...).


4. Pick a model and add your key

Create a .env file in the project folder. You only need the line for the provider you’ll use:

# .env
BROWSER_USE_API_KEY=   # for ChatBrowserUse (bu-2-0), or for cloud browsers
OPENAI_API_KEY=        # for ChatOpenAI
ANTHROPIC_API_KEY=     # for ChatAnthropic
GOOGLE_API_KEY=        # for ChatGoogle (GEMINI_API_KEY is deprecated)

Your options, all from the supported models page:

WrapperKeyNotes
ChatBrowserUse()BROWSER_USE_API_KEYDefaults to bu-2-0, the vendor’s browser-tuned model. $0.60 input, $0.06 cached, $3.50 output per 1M tokens. Eligible new sign-ups get $1 in one-time credit. It also accepts provider-prefixed IDs such as anthropic/claude-sonnet-4-6 through the Browser Use gateway.
ChatOpenAI(model=...)OPENAI_API_KEYAny OpenAI model, and any OpenAI-compatible endpoint via base_url. The README’s example is ChatOpenAI(model='gpt-5.6-luna', reasoning_effort='xhigh').
ChatAnthropic(model=...)ANTHROPIC_API_KEYClaude models.
ChatGoogle(model=...)GOOGLE_API_KEYGemini and Gemma, plus Vertex AI with vertexai=True.
ChatOllama(model=...)noneLocal models. Run ollama serve, then ollama pull <model>.

How to choose:

  • Cheapest way to get a first success: ChatBrowserUse() with the free credit, since it’s what the library is tuned for.
  • You already pay for OpenAI, Anthropic or Google: use that wrapper. If you’re on OpenAI, our GPT-5.6 model ID guide explains the Sol, Terra and Luna tiers. If you’re on Gemini, the Gemini 3.6 Flash guide covers which Flash model to pick for high-volume work.
  • Local only: ChatOllama costs nothing per token, but expect more failed steps from small models. The docs warn that smaller Qwen models, for example, can return the wrong action format, and they recommend adding concrete examples of the correct format to your prompt if you go that way.

5. Run your first agent

Save this as agent.py. It’s the README’s example with the OpenAI and Browser Use options side by side:

import asyncio

from browser_use import Agent, ChatBrowserUse, ChatOpenAI
from dotenv import load_dotenv

load_dotenv()

async def main():
    llm = ChatBrowserUse()                       # needs BROWSER_USE_API_KEY
    # llm = ChatOpenAI(model='gpt-5.6-luna')     # or use your OpenAI key
    agent = Agent(
        task="Find the number of stars of the browser-use repo",
        llm=llm,
    )
    history = await agent.run()
    print(history.final_result())

if __name__ == "__main__":
    asyncio.run(main())

Run it:

uv run agent.py

The agent opens Chromium, finds the repository and prints its answer. If you have a desktop, you’ll see the browser window. On a server with no display, the browser runs headless automatically, because the headless setting auto-detects whether a display is available.

You don’t write a system prompt. Agent(...) supplies Browser Use’s own prompt for whichever model you pick. If you need extra rules, use extend_system_message="..."; override_system_message replaces the default entirely, which you rarely want.


6. Lock it down: allowed domains and a step cap

The first agent above can go anywhere and has no meaningful step limit. Fix both before you point it at anything real.

import asyncio

from browser_use import Agent, Browser, ChatBrowserUse
from dotenv import load_dotenv

load_dotenv()

async def main():
    browser = Browser(
        allowed_domains=['news.ycombinator.com'],  # navigation outside this is blocked
        headless=True,                             # explicit for servers and CI
    )
    agent = Agent(
        task="Open https://news.ycombinator.com/show and list the titles of the first 5 posts",
        llm=ChatBrowserUse(),
        browser=browser,
        max_failures=3,          # default 5: retries for steps that error
        step_timeout=120,        # default 180 seconds per step
        calculate_cost=True,     # default False: track API cost
    )
    history = await agent.run(max_steps=25)
    print(history.final_result())
    print("steps:", history.number_of_steps(), "seconds:", history.total_duration_seconds())

if __name__ == "__main__":
    asyncio.run(main())

What each setting does, from the agent and browser parameter references:

  • allowed_domains restricts navigation. 'example.com' matches only https://example.com/*; '*.example.com' matches the domain and every subdomain; 'http*://example.com' matches both protocols. Wildcards in the top-level domain (example.*) aren’t allowed. If you also set prohibited_domains, allowed_domains wins.
  • max_steps caps the loop. Set it explicitly. The configuration page says the default is 100, but in the 0.13.11 source Agent.run(max_steps=...) defaults to 500. An unattended agent that loops for 500 steps on a paid model is exactly the variable cost the vendor warns about.
  • max_failures (default 5) is how many times a failing step is retried. With final_response_after_failure (default True), the agent makes one last model call to report what it had when it gives up.
  • step_timeout (default 180 seconds) and llm_timeout (default chosen per model: 30, 75 or 90 seconds) bound how long a single step or model call may take.
  • calculate_cost (default False) turns on cost tracking.

The URL in the task matters too: directly_open_url defaults to True, so when your task contains a URL the agent opens it straight away instead of searching for it. That’s one fewer step and one less chance to land somewhere unexpected.


7. Return validated JSON with Pydantic

A string answer is fine for a demo. For anything you’ll feed into code, pass a Pydantic model as output_model_schema. This is adapted from the repo’s custom output example:

import asyncio

from pydantic import BaseModel
from browser_use import Agent, Browser, ChatBrowserUse
from dotenv import load_dotenv

load_dotenv()

class Post(BaseModel):
    post_title: str
    post_url: str
    num_comments: int
    hours_since_post: int

class Posts(BaseModel):
    posts: list[Post]

async def main():
    agent = Agent(
        task="Go to https://news.ycombinator.com/show and give me the first 5 posts",
        llm=ChatBrowserUse(),
        browser=Browser(allowed_domains=['news.ycombinator.com']),
        output_model_schema=Posts,
    )
    history = await agent.run(max_steps=25)

    result: Posts | None = history.structured_output
    if result is None:
        print("No structured result. Check history.errors()")
        return
    for p in result.posts:
        print(p.post_title, p.num_comments, p.post_url)

if __name__ == "__main__":
    asyncio.run(main())

history.structured_output parses the final result with your schema and returns None if there’s no final result. In the installed source it calls model_validate_json on final_result(), so you get a real Posts object or a validation error, never a half-parsed dict.

Valid JSON is not the same as correct data, though. Browser Use’s explainer makes this point directly: compare the returned values with the source page. For a scraper you’ll run repeatedly, spot-check a few records by hand on the first runs and log history.urls() so you can see which pages the values came from.


8. Read the history, and don’t trust “done”

agent.run() returns an AgentHistoryList. The methods worth knowing, from the output format docs:

history.final_result()           # final extracted content
history.structured_output        # parsed Pydantic object (with output_model_schema)
history.is_done()                # did the agent emit a "done" action?
history.is_successful()          # the agent's own verdict (None if not done)
history.has_errors()             # any step errors?
history.errors()                 # per-step errors (None for clean steps)
history.urls()                   # pages visited
history.action_names()           # actions taken
history.number_of_steps()
history.total_duration_seconds()

The important distinction: is_done() does not mean success, and is_successful() is only the agent grading itself. The docs say to verify important external outcomes, such as a submitted form or a completed purchase, against the destination system. The explainer adds a practical warning: retrying a run that actually succeeded can create a duplicate order or application. Check for the saved record or confirmation before you retry anything that writes.

For debugging, two options produce artifacts you can review: generate_gif=True on the agent records the run as a GIF, and record_video_dir on the browser saves MP4s (this needs pip install "browser-use[video]"; without it, no video is saved and no error is raised).


9. Logins and secrets, done carefully

Start read-only. When you do need a logged-in session, the library gives you three options, roughly from safest to riskiest:

Reuse saved cookies instead of passwords. Pass storage_state='./auth.json' to Browser(...). The docs’ sensitive-data page recommends this over sending credentials.

Reuse your own Chrome profile. Browser.from_system_chrome() finds your Chrome install and profile; Browser.list_chrome_profiles() lists them, and you can pick one with profile_directory='Profile 1'. Close Chrome fully first, because a profile can’t be used by two instances at once. This gives the agent everything that profile is signed into, so use a dedicated profile, not your main one.

Pass credentials as placeholders. With sensitive_data, the model only ever sees placeholder names, and the real values are injected into form fields after the model call:

import os
from browser_use import Agent, Browser, ChatOpenAI

os.environ['ANONYMIZED_TELEMETRY'] = "false"

sensitive_data = {
    'https://*.example-staging.com': {'x_user': 'me@example.com', 'x_pass': '...'},
}

agent = Agent(
    task='Log into example-staging.com with username x_user and password x_pass',
    sensitive_data=sensitive_data,   # per-domain: secrets only used on matching sites
    use_vision=False,                # so screenshots can't leak the values to the model
    llm=ChatOpenAI(model='gpt-5.6-luna'),
    browser=Browser(allowed_domains=['*.example-staging.com']),
)

The docs’ best practices for this pattern are the three settings shown: restrict navigation with allowed_domains, turn off vision with use_vision=False, and prefer storage_state over passwords when you can. Load the real values from environment variables or a secrets manager rather than hard-coding them as in this sketch.

Prompt injection is the other half of this. A page can contain text that tries to redirect your agent. Browser Use’s advice is to treat page content as untrusted, limit which accounts and actions the agent can reach, and require approval for sensitive actions in your own application, while being clear that this reduces the risk without removing it. We covered the same trade-off from OpenAI’s side in what ChatGPT’s Lockdown Mode actually blocks.


10. Add your own tool

When the agent needs something a browser can’t do, such as calling your API or writing a file, register a Python function with Tools. This is the README’s example:

import asyncio
from datetime import datetime, timezone

from browser_use import ActionResult, Agent, ChatBrowserUse, Tools
from dotenv import load_dotenv

load_dotenv()
tools = Tools()

@tools.action(description='Get the current date and time in UTC.')
def get_current_time() -> ActionResult:
    return ActionResult(extracted_content=datetime.now(timezone.utc).isoformat())

async def main():
    agent = Agent(
        task="What is the current UTC time?",
        llm=ChatBrowserUse(model='bu-2-0'),
        tools=tools,
    )
    history = await agent.run(max_steps=10)
    print(history.final_result())

if __name__ == "__main__":
    asyncio.run(main())

The description is what the model reads to decide when to call the tool, so write it like a one-line docstring for a colleague.


11. Moving off your laptop: local browser or cloud browser

The same code can drive a hosted browser instead of local Chromium. Set BROWSER_USE_API_KEY and change one line:

browser = Browser(use_cloud=True)

The README lists cloud browsers at $0.02 per browser-hour plus network traffic, with stealth, residential proxies, CAPTCHA solving and a live view URL, and notes this avoids installing Chrome at all in Docker, CI or serverless. Model tokens are billed separately. Even then, the docs say no browser configuration guarantees every CAPTCHA can be avoided or solved.

If you already run a coding agent such as Claude Code or Codex, you may not need the Python library at all: Browser Use’s separate CLI (Browser Harness) gives an existing agent browser control, and the README’s Path 2 has the install prompt. If you’re comparing this with Anthropic’s own hosted option, see our note on Claude’s Computer Use and Browser Use APIs going GA.


12. Troubleshooting

ModuleNotFoundError: No module named 'browser_use'. You installed into one environment and are running another. Inside a uv project, use uv run agent.py. With a venv, activate it first (source .venv/bin/activate).

Install fails on Python 3.10 or older. 0.13.11 requires Python 3.11+. Create the environment with uv venv --python 3.12 or uv init --python 3.12.

The browser never starts, or you get a Chromium-not-found error. Run uvx browser-use install (or uv run browser-use install) to download Chromium and its system dependencies. To use a browser you already have, pass executable_path (for example '/usr/bin/google-chrome' on Linux) or channel='chrome'. The CLI’s browser-use doctor command diagnoses install and browser state, and browser-use doctor --fix-snap explains how to fix Snap-packaged Chromium blocking the DevTools protocol on Linux.

It runs on your laptop but fails in Docker or on a server. With no display, headless is auto-selected; set headless=True explicitly so behavior is predictable. Chromium’s sandbox is on by default except in Docker (chromium_sandbox). If you’d rather not run Chrome in a container at all, try Browser(use_cloud=True).

from_system_chrome() raises RuntimeError or can’t open the profile. The error means Chrome wasn’t found. If Chrome is found but the profile won’t load, fully quit Chrome, because a profile can’t be shared between instances.

Pages time out on slow sites. Browser actions have their own timeouts you can raise with environment variables, for example export TIMEOUT_NavigateToUrlEvent=60.0 (default 30) or export TIMEOUT_BrowserStateRequestEvent=60.0 (default 30). For slow models, raise llm_timeout and step_timeout on the agent.

Rate limits (429) or provider outages stop the run. Set fallback_llm= to a second model. The primary model exhausts its own retries first (typically 5 with exponential backoff), then the agent switches for the rest of the run on 429, 401, 402 or 5xx errors.

A small local model keeps producing broken actions. That’s a model-capability issue the docs call out, not your code. Try a larger model, add examples of the correct action format with extend_system_message, or try flash_mode=True, which the docs describe as a fast mode that skips the evaluation, next-goal and thinking fields and keeps only memory, so each step asks the model for less output.

The agent ends with a confident but wrong answer. Tighten the task: give the exact start URL, say exactly which fields you want, and state what a successful result looks like. Use output_model_schema so the shape is enforced, then compare a sample against the page.


13. FAQ

Is Browser Use free? The library is free and MIT-licensed. You pay for model inference (any provider, including ChatBrowserUse) and for Browser Use Cloud if you use cloud browsers. A local browser plus a local Ollama model costs nothing per run beyond your hardware.

Which model should I start with? The vendor recommends its bu-2-0 model via ChatBrowserUse(), priced at $0.60 / $3.50 per 1M input/output tokens. The right choice depends on your tasks, latency and budget. If you already have a provider key, start there, run the same task several times and compare verified results.

Do I need to write a system prompt? No. The agent ships with one and keeps it when you change models. Add rules with extend_system_message.

How do I keep costs predictable? Set max_steps on every run (don’t rely on the default), keep max_failures low, turn on calculate_cost=True, and use a cheaper page_extraction_llm for reading page text if your main model is expensive. Browser Use’s explainer suggests judging cost per verified success: total spend across all attempts, including failures and retries, divided by the number of tasks you confirmed actually worked.

Can it get past logins and two-factor prompts? Sometimes. Sessions expire and multi-factor authentication can require a human. Reusing a saved session with storage_state or a dedicated Chrome profile cuts down on repeated logins, but your workflow still has to handle an expired session.

Will it get blocked? It can. Bot-management systems and rate limits target automated traffic, and the vendor notes that a better model won’t fix every access restriction. Respect each site’s terms and keep read-only scrapers polite.

How is this different from Playwright? Playwright runs steps you write. Browser Use decides the steps from a goal. If you can write the steps down and they don’t change, a Playwright script is cheaper and deterministic. Use the agent where the next step depends on what’s on the page.


Wrapping up

A useful first browser agent is small and boxed in: one read-only task, an exact start URL, allowed_domains, an explicit max_steps, a Pydantic schema for the output, and a manual check of the first few results. Browser Use 0.13 makes each of those a single argument. Once a task passes repeatedly, you can add logins with storage_state, custom tools, or a cloud browser for servers, one change at a time. For more practical builder guides, browse the latest tutorials on the 7minai homepage.

Sources