Browser-use, an LLM agent that drives the browser
Browser-use is a Python library that hands a language model control of a browser: the model clicks, fills in forms and reads pages for which no official API exists. The current version is 0.13.8, released on 16 August 2026, the licence is MIT, and the browser-use/browser-use repository has around 109.9 thousand stars. The price for that convenience is tokens and unreliability.
What browser-use actually does
The heart of the library is a loop. The agent collects the page state, sends it to the model together with the task text and a trimmed history of previous steps, receives a list of actions in return, performs them in the browser and starts again. The loop ends when the model calls the done action, when the failure limit runs out, or when the per-step timeout expires.
The interface is short. Agent(task=..., llm=...) plus await agent.run() is the entire minimal program. The rest of the parameters, and the constructor has well over fifty of them, exist to trim that loop: how many actions per step, how much history, whether to attach a screenshot, how long to wait for the network, which domains are allowed.
The set of actions available to the model is closed and defined in the tools registry. The obvious ones are there: navigate, click, input, scroll, send_keys, go_back, switch, close, select_dropdown and dropdown_options. There are read operations too: extract passes the page markdown through a model, search_page looks for a pattern in the text the way grep does, find_elements queries the DOM with a CSS selector. There are file actions, write_file, read_file, replace_file, save_as_pdf and screenshot, plus evaluate, which runs arbitrary JavaScript on the page. At the end sits done, which closes the run and returns the result.
In version 0.13 the browser is driven directly over the Chrome DevTools Protocol. Playwright no longer appears among the package dependencies; cdp-use and browser-harness do. Field names in BrowserProfile stayed close to Playwright's, because the profile inherits from classes describing launch and context arguments, but the execution layer is the project's own.
What browser-use does not do matters just as much. It is not a framework for collecting content at scale, that is Firecrawl. It is not a test runner, that role belongs to Playwright. It does not isolate executed code, which is what E2B is for. Nor is it a general purpose agent framework like LangChain, although it can be embedded in one as a single tool.
Version, licence and project health
The licence leaves no room for doubt, which is a rare comfort in this category of tooling. The LICENSE file in the published package carries the full MIT text with the note "Copyright (c) 2024 Gregor Zunic". The PyPI classifier says License :: OSI Approved :: MIT License, and the GitHub API returns the SPDX identifier MIT for the repository. Three independent sources agree, there is no separate directory with commercial terms and no clause restricting competing hosting.
The project started on 31 October 2024 and grew fast. Today it has roughly 109.9 thousand stars, 12,084 forks and 363 open issues, it is not archived, and the latest change on the main branch dates from 21 August 2026. The release cadence is high and the interface shifts between minor releases: Controller is an alias of Tools, Browser an alias of BrowserSession, and constructor parameters keep arriving and changing their defaults. Pin the exact version and treat every upgrade as a change that requires re-testing your scenarios.
A separate operational problem is how dependencies are declared. The package pins them with an equals sign rather than a range: openai==2.16.0, anthropic==0.76.0, pydantic==2.12.5, mcp==1.26.0, httpx==0.28.1 and several dozen more. If your application uses its own OpenAI or Claude client at a different version, installing into one virtual environment will end in a conflict. A separate environment or a separate process is not overkill here, it is a practical requirement. The minimum Python version is 3.11.
A commercial cloud runs around the library at browser-use.com. Billing is credit based: there is a monthly credit allowance tied to the plan, annual plans receive their credits up front, and credits are spent on any product at the published usage rates. A separate track covers an annual credit pool, service level agreements and data retention terms. I am not quoting figures, because the pricing page is rendered client side and cannot be read reliably from the raw server response.
Two things on the border between open source and cloud deserve to be named plainly. First, if you create an agent without passing a model, the library falls back to ChatBrowserUse, the project's own provider, which requires a BROWSER_USE_API_KEY. The default behaviour therefore leads to a paid service rather than to the model you already have. Second, telemetry is on by default: the ANONYMIZED_TELEMETRY variable defaults to true, and BROWSER_USE_CLOUD_SYNC mirrors that value by default. A PostHog client sits among the dependencies. If the agent walks around internal company systems, set both variables to false before the first run.
How page state reaches the model
This question decides your token bill, so the answer is worth knowing precisely. Browser-use does not send raw HTML to the model and does not rely on an image alone. It builds its own simplified text representation by merging three sources from the Chrome DevTools Protocol: DOM.getDocument provides the tree structure, DOMSnapshot.captureSnapshot adds geometry and computed styles, and Accessibility.getFullAXTree contributes accessibility roles and names.
From the merged tree the serializer keeps only the nodes that matter to the agent: interactive elements, scrollable elements and frames. Every interactive node receives a sequence number and enters the text in a shape close to HTML, prefixed by that index in square brackets. An asterisk before the index marks an element that appeared since the previous step, which lets the model notice that a click opened a modal dialog.
[12]<button type=submit aria-label=Search>Search</button>
[13]<input type=text name=q placeholder=Enter a phrase />
*[14]<div role=dialog aria-label=Consent>Privacy settings</div>The set of attributes copied into that representation is fixed and has a cost implication. By default it is title, type, checked, id, name, role, value, placeholder, data-date-format, alt, aria-label, aria-expanded, data-state, aria-checked, aria-valuemin, aria-valuemax, aria-valuenow and aria-placeholder. The class attribute is commented out in the source, so it is deliberately skipped, because on a Tailwind page it would eat most of the context window on its own. The list can be overridden with the include_attributes parameter.
Serialization has a hard cap. The max_clickable_elements_length parameter defaults to 40,000 characters and truncates the representation above that threshold. On a page with a long result list this means the model sees its beginning rather than the whole thing, and that is the most common cause of the sentence "I could not find that element" when the element is right there on the page.
The screenshot is an additional layer, controlled by use_vision and enabled by default. The image travels to the model as data inside the message, and its resolution is governed by vision_detail_level with the values auto, low and high plus llm_screenshot_size, which takes a width and height tuple. For models in the Claude Sonnet family the library sets 1400 by 850 pixels itself, because above that threshold the image gets rescaled on the provider side and you pay for pixels the model never sees at full resolution.
First agent and controlling the run
Installation and startup look like this.
# a separate environment, because the dependencies are hard pinned
python -m venv .venv && source .venv/bin/activate
# pin the exact version
pip install "browser-use==0.13.8"
# switch telemetry off before the first run
export ANONYMIZED_TELEMETRY=false
export BROWSER_USE_CLOUD_SYNC=false
# your own provider key, not the browser-use cloud
export OPENAI_API_KEY=sk-...A program with a realistic configuration, rather than a marketing snippet, looks as follows.
import asyncio
from browser_use import Agent, BrowserProfile, ChatOpenAI
profile = BrowserProfile(
headless=True,
allowed_domains=['*.example.com'],
block_ip_addresses=True,
wait_between_actions=0.2,
wait_for_network_idle_page_load_time=1.0,
highlight_elements=False,
user_data_dir=None,
)
async def main():
agent = Agent(
task='Find the plan in the pricing table with a 50 user limit and save its name.',
llm=ChatOpenAI(model='gpt-5.5'),
browser_profile=profile,
sensitive_data={'https://*.example.com': {'password': 'secret'}},
initial_actions=[{'navigate': {'url': 'https://example.com/pricing'}}],
max_actions_per_step=3,
max_failures=3,
step_timeout=120,
calculate_cost=True,
)
history = await agent.run(max_steps=20)
print(history.final_result())
print(history.number_of_steps(), history.urls())
asyncio.run(main())Three parameters from that example deserve a comment. allowed_domains is the only real barrier stopping the agent from following a footer link and starting to act outside your service. sensitive_data substitutes values only inside the browser, so the model receives a symbolic name instead of the password. The max_steps argument passed to run is the last cost fuse and has no sensible default you could leave alone without thinking.
The history object returned by run exposes final_result, is_done, errors, urls, model_actions, number_of_steps and structured_output. If you pass output_model_schema with a Pydantic model, the result comes back as an object rather than as text you have to parse with a regular expression.
Token cost and how to cut it
The cost mechanism is simple and unpleasant. Every step is a separate model call carrying the system instruction, the task text, the history and the full representation of the current page, plus an image when use_vision is on. A ten step task means ten such calls. A page with heavy navigation can produce well over ten thousand tokens of state description alone, and it does so again on every step.
The library offers several levers and all of them are real constructor parameters. Setting flash_mode to true strips the reasoning fields from the response schema and disables planning, which shortens the model's answer. Setting use_thinking to false gives a gentler variant. Setting use_vision to false removes the image, usually the single largest saving. max_history_items trims how many previous steps stay in context, and message_compaction turns on folding of older history. max_clickable_elements_length lowered to a few thousand characters limits the page description at the cost of visibility into elements further down. include_attributes narrowed to three or four entries cuts out the rest.
from browser_use import Agent, ChatOpenAI
agent = Agent(
task='Collect the names and prices of the first ten products in the list.',
llm=ChatOpenAI(model='gpt-5.5'),
page_extraction_llm=ChatOpenAI(model='gpt-5.5-mini'),
use_vision=False,
use_thinking=False,
use_judge=False,
flash_mode=True,
max_history_items=8,
max_actions_per_step=5,
max_clickable_elements_length=8000,
include_attributes=['id', 'name', 'role', 'aria-label'],
calculate_cost=True,
)Two actions described in the source as costing nothing on the model side work separately from all this. search_page searches the page text for a pattern, optionally as a regular expression and scoped by a CSS selector, while find_elements returns elements matching a selector along with chosen attributes. Both execute inside the browser and return a short result instead of the whole page. If the task boils down to checking whether a given string appears on the page, hint in the task text that the agent should use search_page, rather than letting it scroll blindly.
The extract action is expensive by nature, because it pushes the page markdown through a model. That is why page_extraction_llm exists: you can substitute a cheaper model for data extraction alone than the one driving the agent. By default both are the same model, which is the most expensive configuration available. It is also useful to know that use_judge is on by default and adds a call that evaluates the result, while calculate_cost is off by default, so without switching it on you never see in the summary what the run actually cost.
Custom actions instead of clicking
The most effective way to cut cost and raise reliability is to take away from the model those parts of the task that need no understanding. The tools registry lets you add your own action, which the model calls by name and description, and whose body is ordinary Python code.
from pydantic import BaseModel
from browser_use import Agent, ChatOpenAI, Tools
tools = Tools()
class OrderQuery(BaseModel):
order_id: str
@tools.action(
'Fetch the order status from the internal API by order number.',
param_model=OrderQuery,
domains=['*.example.com'],
)
async def order_status(params: OrderQuery) -> str:
import httpx
async with httpx.AsyncClient() as client:
r = await client.get(f'https://api.example.com/orders/{params.order_id}')
return r.json()['status']
agent = Agent(
task='Check the status of order 88421 and describe it in one sentence.',
llm=ChatOpenAI(model='gpt-5.5'),
tools=tools,
)The decorator accepts a description, an optional Pydantic parameter model, a domain list restricting where the action is available, and terminates_sequence, which stops further actions within the same step. Six clicks turned into one HTTP call means six fewer model calls and six fewer chances to go wrong. The same principle applies to the evaluate action, which runs JavaScript on the page: if you know the selector, a deterministic script is cheaper and more dependable than describing to the model what it should click.
Browser-use against the alternatives
| Tool | How it works | When to pick it |
|---|---|---|
| browser-use | model drives the browser over CDP, state as a text tree | one-off tasks, sites with no API, shifting layouts |
| Playwright | deterministic script with selectors | repeatable flows, tests, CI pipelines |
| Firecrawl | fetches a page and converts it to markdown | collecting content at scale, no clicking |
| computer use in OpenAI and Claude | model drives a cursor by coordinates on an image | apps without a DOM, desktop, edge cases |
| the service's official API | an HTTP call with a contract | always, when such an API exists |
The last row is not a joke. A browser agent is the answer for situations where an API is missing or unavailable. Where one exists, the agent loses on every dimension: cost, latency, repeatability and testability. Browser-use also works well as a transitional tool, where you first let the agent perform the task a few times, watch which actions it picks, and then rewrite the stable flow as a script.
Between those two extremes sits Stagehand, which lets you mix the two in a single script: where a selector is known, you write it out, and you save natural language instructions for the parts that change. That addresses exactly the transitional problem described above, because you need not rewrite everything at once. Two things are worth knowing though: action caching, which cuts the number of model calls, works only with a browser hosted by the vendor, and version four dropped Playwright in favour of its own CDP client and a Chrome extension, so code written for version three will not carry over unchanged.
Common mistakes
Treating the agent as a function. The same model, the same task and the same page can produce three different runs. If the result is going into a production system, you need an output schema, verification of the result and a retry policy, rather than the assumption that this time it will work.
Running without allowed_domains. A page can contain text addressed to the model instructing it to go elsewhere or do something else. With an agent holding a logged-in session this is a real attack vector, and the allowed domain list together with prohibited_domains and block_ip_addresses is the first and cheapest barrier.
Pasting passwords into the task text. Everything placed in the task field travels to the model provider and lands in logs. Credentials belong in sensitive_data, where values are substituted only inside the browser.
Installing next to your application code. Hard pins on openai, anthropic, pydantic and httpx will sooner or later collide with what the rest of the project uses.
Leaving the cost defaults alone. Out of the box you get vision on, reasoning on, the judge on and cost accounting off. That is a configuration tuned for demo quality, not for the invoice at the end of the month.
No step limit. Without max_steps, an agent that falls into a loop can keep generating model calls for as long as your provider quota allows.
FAQ
Will browser-use replace Playwright?
Not in the cases where Playwright is the right tool. For tests and for flows whose layout does not change, a deterministic script is faster, cheaper and gives repeatable results. Browser-use wins where the page is unknown, changeable, or so sprawling that writing selectors would take longer than performing the task once.
Are element indices stable between steps?
No. Indices are assigned while serializing the current state and rebuilt after every action. That is why clicking an index that no longer exists returns a message saying the page may have changed and nudges the model to refresh the state. Do not build your own logic on the idea that index 12 always points at the same button.
How much does one agent step cost?
It depends on the page and the configuration, so quoting a figure would be guesswork. You can measure it though: set calculate_cost=True, run the task against your target page and compare the result with the same run at use_vision=False and a lowered max_clickable_elements_length. That measurement takes a quarter of an hour and gives you a number for your own case.
Can I run browser-use without a browser-use cloud key?
Yes. Pass your own model, for example ChatOpenAI, ChatAnthropic, ChatGoogle, ChatOllama or ChatOpenRouter. The BROWSER_USE_API_KEY is needed only when you leave the default ChatBrowserUse provider in place, which happens automatically if you omit the llm parameter.
Is the agent safe on pages with user generated content?
Not on its own. Page content reaches the model as part of the context, so a forum comment can carry an instruction aimed at the agent. Restrict domains, restrict the action set to what is necessary, do not give the agent a session with permissions the task does not require, and do not allow irreversible actions without a confirmation on your own side.
Can it run in a container with no display?
Yes, BrowserProfile(headless=True) starts the browser without a window, and cdp_url lets you attach to a browser running elsewhere. The full list of profile parameters is covered by the project documentation, and the source of the action registry lives in the GitHub repository.