Skip to content

Commit c6c4a85

Browse files
committed
docs: renumber Browser Use guide to 09 and switch to a single-file example
1 parent 694bd1b commit c6c4a85

7 files changed

Lines changed: 128 additions & 195 deletions

File tree

Lines changed: 15 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
---
22
id: browser-use
3-
title: Use Browser Use
3+
title: Browser AI agents with Browser Use
44
description: Build an Apify Actor that automates a browser with an LLM agent using the Browser Use library.
55
---
66

7-
import CodeBlock from '@theme/CodeBlock';
8-
import Tabs from '@theme/Tabs';
9-
import TabItem from '@theme/TabItem';
7+
import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock';
108

11-
import BrowserUseMain from '!!raw-loader!./code/browser_use_project/my_actor/main.py';
12-
import BrowserUseAgent from '!!raw-loader!./code/browser_use_project/my_actor/agent.py';
13-
import BrowserUseEntrypoint from '!!raw-loader!./code/browser_use_project/my_actor/__main__.py';
14-
import BrowserUseDockerfile from '!!raw-loader!./code/browser_use_project/Dockerfile';
9+
import BrowserUseExample from '!!raw-loader!roa-loader!./code/09_browser_use.py';
1510

16-
In this guide, you'll learn how to use the [Browser Use](https://browser-use.com/) library in your Apify Actors.
11+
In this guide, you'll learn how to use the [Browser Use](https://browser-use.com/) library to drive a browser with an LLM agent in your Apify Actors.
1712

1813
## Introduction
1914

@@ -41,62 +36,38 @@ Browser Use needs an LLM to drive the agent. You choose a provider wrapper, give
4136
- **`ChatAnthropic`** - Anthropic Claude models such as `claude-sonnet-4-5` or `claude-haiku-4-5`. Reads the key from `ANTHROPIC_API_KEY`.
4237
- **`ChatGoogle`** - Google Gemini models such as `gemini-2.5-flash`. Reads the key from `GOOGLE_API_KEY`.
4338

44-
The example Actor in this guide uses `ChatOpenAI`, but switching providers is a one-line change in `my_actor/agent.py`. More capable models generally complete tasks in fewer steps and more reliably, while smaller models are cheaper per step.
39+
The example Actor in this guide uses `ChatOpenAI`, but switching providers is a one-line change in `run_agent_task`. More capable models generally complete tasks in fewer steps and more reliably, while smaller models are cheaper per step.
4540

4641
Keep the API key out of the Actor input and source code. The example reads it from an environment variable, which on the Apify platform you set as a [secret environment variable](https://docs.apify.com/platform/actors/development/programming-interface/environment-variables) (for example `OPENAI_API_KEY`), and locally you export in your shell.
4742

4843
## Example Actor
4944

5045
The following Actor runs a Browser Use agent for a single task and stores its structured result in the default dataset. By default it opens [Hacker News](https://news.ycombinator.com) and returns the title and URL of the top five posts, but the task, model, and step limit are all configurable through the Actor input.
5146

52-
The code is split into three small modules, following the structure of the Apify Python Actor templates:
53-
54-
- `my_actor/main.py` - The Actor's main coroutine. It handles the [Actor](https://docs.apify.com/platform/actors) lifecycle, reads the input, sets up [Apify Proxy](https://docs.apify.com/platform/proxy), runs the agent, and stores the result.
55-
- `my_actor/agent.py` - The Browser Use-specific logic. It defines the output schema and a single `run_agent_task` function that builds the LLM, browser, and agent, then returns the agent's structured output.
56-
- `my_actor/__main__.py` - The entry point that runs the `main` coroutine with `asyncio`.
57-
58-
<Tabs>
59-
<TabItem value="main.py" label="my_actor/main.py">
60-
<CodeBlock className="language-python">
61-
{BrowserUseMain}
62-
</CodeBlock>
63-
</TabItem>
64-
<TabItem value="agent.py" label="my_actor/agent.py">
65-
<CodeBlock className="language-python">
66-
{BrowserUseAgent}
67-
</CodeBlock>
68-
</TabItem>
69-
<TabItem value="__main__.py" label="my_actor/__main__.py">
70-
<CodeBlock className="language-python">
71-
{BrowserUseEntrypoint}
72-
</CodeBlock>
73-
</TabItem>
74-
</Tabs>
47+
The whole Actor fits in a single file. A `run_agent_task` helper holds the Browser Use-specific logic - it defines the output schema and builds the LLM, browser, and agent - while the `main` coroutine handles the [Actor](https://docs.apify.com/platform/actors) lifecycle, reads the input, sets up [Apify Proxy](https://docs.apify.com/platform/proxy), runs the agent, and stores the result:
48+
49+
<RunnableCodeBlock className="language-python" language="python">
50+
{BrowserUseExample}
51+
</RunnableCodeBlock>
7552

7653
A few things worth pointing out:
7754

78-
- Keeping the agent setup in `run_agent_task` separates the Browser Use-specific code from the Actor's orchestration logic. `my_actor/main.py` only decides what to read from the input and what to store.
79-
- Passing `output_model_schema=Posts` makes the agent return a validated `Posts` instance via `history.structured_output`, so `my_actor/main.py` can push each item straight to the dataset. Adapt the task and the `Post`/`Posts` models together to fit your own use case.
55+
- Keeping the agent setup in `run_agent_task` separates the Browser Use-specific code from the Actor's orchestration logic. `main` only decides what to read from the input and what to store.
56+
- Passing `output_model_schema=Posts` makes the agent return a validated `Posts` instance via `history.structured_output`, so `main` can push each item straight to the dataset. Adapt the task and the `Post`/`Posts` models together to fit your own use case.
8057
- `enable_signal_handler=False` leaves signal handling to the Actor, which manages the run's lifecycle. Without it, Browser Use would install its own handlers and interfere with a clean shutdown.
8158
- `headless=Actor.configuration.headless` runs the browser without a visible window, which is what you want on the platform.
8259

8360
## Using Apify Proxy
8461

85-
Running on the Apify platform gives your agent access to [Apify Proxy](https://docs.apify.com/platform/proxy), which rotates IP addresses to avoid rate limiting and blocking. In the example above, `my_actor/main.py` creates a proxy configuration with `Actor.create_proxy_configuration` and passes a fresh proxy URL to `run_agent_task`.
62+
Running on the Apify platform gives your agent access to [Apify Proxy](https://docs.apify.com/platform/proxy), which rotates IP addresses to avoid rate limiting and blocking. In the example above, `main` creates a proxy configuration with `Actor.create_proxy_configuration` and passes a fresh proxy URL to `run_agent_task`.
8663

87-
Browser Use expects the proxy as a `ProxySettings` object with separate `server`, `username`, and `password` fields, whereas `ProxyConfiguration.new_url` returns a single URL string (for example `http://user:pass@proxy.apify.com:8000`). The `_proxy_settings` helper in `my_actor/agent.py` splits that URL into the fields Browser Use expects. To select specific proxy groups or a country, pass the relevant arguments to `Actor.create_proxy_configuration`. For more details, see the [Proxy management](../concepts/proxy-management) guide.
64+
Browser Use expects the proxy as a `ProxySettings` object with separate `server`, `username`, and `password` fields, whereas `ProxyConfiguration.new_url` returns a single URL string (for example `http://user:pass@proxy.apify.com:8000`). The `_proxy_settings` helper splits that URL into the fields Browser Use expects. To select specific proxy groups or a country, pass the relevant arguments to `Actor.create_proxy_configuration`. For more details, see the [Proxy management](../concepts/proxy-management) guide.
8865

8966
## Running on the Apify platform
9067

9168
Browser Use drives a real Chromium over CDP, so the Actor needs a browser binary available at runtime. The simplest way to provide one is to build on top of the [Apify Playwright base image](https://hub.docker.com/r/apify/actor-python-playwright), which already ships a browser together with all of its system-level dependencies. Browser Use discovers that browser automatically, so no extra install step is needed in the image.
9269

93-
<Tabs>
94-
<TabItem value="Dockerfile" label="Dockerfile">
95-
<CodeBlock className="language-docker">
96-
{BrowserUseDockerfile}
97-
</CodeBlock>
98-
</TabItem>
99-
</Tabs>
70+
Disable Browser Use's telemetry and cloud sync inside the Actor by setting the `ANONYMIZED_TELEMETRY=false` and `BROWSER_USE_CLOUD_SYNC=false` environment variables in your Dockerfile.
10071

10172
When running the Actor locally, install the browser once with the `browser-use install` command, which downloads a Chromium build together with its dependencies:
10273

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import asyncio
2+
import os
3+
from urllib.parse import urlsplit
4+
5+
from browser_use import Agent, Browser, ChatOpenAI
6+
from browser_use.browser import ProxySettings
7+
from pydantic import BaseModel
8+
9+
from apify import Actor
10+
11+
# Default task, aligned with the `Posts` schema below.
12+
DEFAULT_TASK = (
13+
'Open https://news.ycombinator.com and return the title and URL '
14+
'of the top 5 posts on the front page.'
15+
)
16+
17+
18+
class Post(BaseModel):
19+
"""A single item the agent is asked to extract."""
20+
21+
title: str
22+
url: str
23+
24+
25+
class Posts(BaseModel):
26+
"""The structured result returned by the agent."""
27+
28+
posts: list[Post]
29+
30+
31+
def to_browser_use_proxy(proxy_url: str) -> ProxySettings:
32+
"""Convert an Apify Proxy URL into Browser Use `ProxySettings`."""
33+
parts = urlsplit(proxy_url)
34+
return ProxySettings(
35+
server=f'{parts.scheme}://{parts.hostname}:{parts.port}',
36+
username=parts.username,
37+
password=parts.password,
38+
)
39+
40+
41+
async def run_agent_task(
42+
task: str,
43+
*,
44+
model: str,
45+
llm_api_key: str,
46+
max_steps: int,
47+
headless: bool = True,
48+
proxy_url: str | None = None,
49+
) -> Posts | None:
50+
"""Run a Browser Use agent for one task and return its structured output."""
51+
# Configure the LLM. Swap `ChatOpenAI` for another provider if needed.
52+
llm = ChatOpenAI(model=model, api_key=llm_api_key)
53+
54+
# Configure the browser, optionally routed through a proxy.
55+
browser = Browser(
56+
headless=headless,
57+
proxy=to_browser_use_proxy(proxy_url) if proxy_url else None,
58+
)
59+
60+
# `output_model_schema` returns a validated `Posts`; signals stay with the Actor.
61+
agent = Agent(
62+
task=task,
63+
llm=llm,
64+
browser=browser,
65+
output_model_schema=Posts,
66+
enable_signal_handler=False,
67+
)
68+
69+
history = await agent.run(max_steps=max_steps)
70+
return history.structured_output
71+
72+
73+
async def main() -> None:
74+
async with Actor:
75+
# Read the Actor input.
76+
actor_input = await Actor.get_input() or {}
77+
task = actor_input.get('task', DEFAULT_TASK)
78+
model = actor_input.get('model', 'gpt-4.1-mini')
79+
max_steps = actor_input.get('maxSteps', 25)
80+
81+
# Read the LLM API key from the environment (set it as a secret on Apify).
82+
llm_api_key = os.environ.get('OPENAI_API_KEY')
83+
if not llm_api_key:
84+
raise RuntimeError('The OPENAI_API_KEY environment variable is not set.')
85+
86+
# Route the browser through Apify Proxy.
87+
proxy_configuration = await Actor.create_proxy_configuration()
88+
proxy_url = await proxy_configuration.new_url() if proxy_configuration else None
89+
90+
Actor.log.info(f'Running the agent (model={model}) for task: {task}')
91+
92+
result = await run_agent_task(
93+
task,
94+
model=model,
95+
llm_api_key=llm_api_key,
96+
max_steps=max_steps,
97+
headless=Actor.configuration.headless,
98+
proxy_url=proxy_url,
99+
)
100+
101+
if result is None:
102+
Actor.log.warning('The agent did not return any structured output.')
103+
return
104+
105+
# Store each extracted item as a dataset row.
106+
Actor.log.info(f'The agent returned {len(result.posts)} post(s); storing them.')
107+
for post in result.posts:
108+
Actor.log.info(f'Storing post: {post.title!r} ({post.url})')
109+
await Actor.push_data(post.model_dump())
110+
111+
112+
if __name__ == '__main__':
113+
asyncio.run(main())

docs/03_guides/code/browser_use_project/Dockerfile

Lines changed: 0 additions & 21 deletions
This file was deleted.

docs/03_guides/code/browser_use_project/my_actor/__init__.py

Whitespace-only changes.

docs/03_guides/code/browser_use_project/my_actor/__main__.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

docs/03_guides/code/browser_use_project/my_actor/agent.py

Lines changed: 0 additions & 69 deletions
This file was deleted.

docs/03_guides/code/browser_use_project/my_actor/main.py

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)