open_url, read_page and click tools directly: they opened https://example.com, read it and followed its link. We didn't run Runner.run(), which needs your OpenAI key; that part follows the Agents SDK's documented API.Prerequisites
- A Browsa API key (it starts with
agt_). Sign up for 100 free credits, no card. - Python 3.10 or newer.
- An OpenAI API key.
Install
pip install openai-agents playwright browsaThen put your keys in the environment:
export BROWSA_API_KEY="agt_live_..."
export OPENAI_API_KEY="sk-..."You don't need playwright install: the browser runs on Browsa, and Playwright only connects to it.
The script
One file. The tools take the Playwright page from the run context, so every tool call works on the same tab.
import asyncio
import os
from dataclasses import dataclass
from agents import Agent, RunContextWrapper, Runner, function_tool
from browsa import Client
from playwright.async_api import Page, async_playwright
BROWSA_API_KEY = os.environ["BROWSA_API_KEY"]
TASK = "Open https://example.com, follow its link, and tell me the title of the page you land on."
@dataclass
class Browser:
page: Page
@function_tool
async def open_url(ctx: RunContextWrapper[Browser], url: str) -> str:
"""Open a URL in the browser. Returns the page title."""
page = ctx.context.page
await page.goto(url, wait_until="domcontentloaded")
return f"Opened {page.url}. Title: {await page.title()}"
@function_tool
async def read_page(ctx: RunContextWrapper[Browser]) -> str:
"""Return the URL and the visible text of the current page."""
page = ctx.context.page
text = await page.inner_text("body")
return f"URL: {page.url}\n\n{text[:8000]}"
@function_tool
async def click(ctx: RunContextWrapper[Browser], text: str) -> str:
"""Click the first link or button whose visible text contains `text`."""
page = ctx.context.page
await page.get_by_text(text).first.click(timeout=10_000)
await page.wait_for_load_state("domcontentloaded")
return f"Clicked. Now on {page.url}. Title: {await page.title()}"
@function_tool
async def fill(ctx: RunContextWrapper[Browser], label: str, value: str, press_enter: bool) -> str:
"""Type `value` into the input whose label or placeholder matches `label`."""
page = ctx.context.page
box = page.get_by_label(label).or_(page.get_by_placeholder(label)).first
await box.fill(value, timeout=10_000)
if press_enter:
await box.press("Enter")
await page.wait_for_load_state("domcontentloaded")
return f"Filled. Now on {page.url}"
agent = Agent[Browser](
name="Browser agent",
instructions=(
"You control a real Chrome browser through tools. "
"Use read_page to see the page before you click. Answer in one or two sentences."
),
tools=[open_url, read_page, click, fill],
model="gpt-5.6-luna",
)
async def main():
browsa = Client(api_key=BROWSA_API_KEY)
cloud = browsa.create_agent(ttl="5m") # a Browsa cloud browser, 1 credit
try:
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(
cloud["cdp_http"], headers={"X-API-Key": BROWSA_API_KEY}
)
context = browser.contexts[0]
page = context.pages[0] if context.pages else await context.new_page()
result = await Runner.run(agent, TASK, context=Browser(page), max_turns=20)
print(result.final_output)
finally:
browsa.destroy_agent(cloud["id"])
if __name__ == "__main__":
asyncio.run(main())Run it:
python agent.pyThe Agents SDK also has a ComputerTool, which takes an AsyncComputer with methods like screenshot, click and type. You can implement one on the same Playwright page; the Claude computer use guide shows the screenshot-and-click mapping. Function tools like the ones above are cheaper, because the model reads text instead of screenshots.
How it works
- Connect Playwright.
connect_over_cdp(cloud["cdp_http"], headers={"X-API-Key": ...})attaches to the Browsa browser, and the script uses the tab that is already open. - Expose the page as tools. Each
@function_toolis a Playwright call. The page travels in the run context (RunContextWrapper[Browser]), so the model never sees it, only the tools' text results. - Let the agent run.
Runner.run()loops: the model calls tools until it can answer, up tomax_turns.destroy_agent()infinallyshuts the browser down.
Troubleshooting
401 Unauthorized when connecting
The X-API-Key header didn't reach Browsa, or the key is wrong. Check that BROWSA_API_KEY is set in the shell that runs the script and that it starts with agt_; the Browsa SDK refuses keys that don't. Pass the header exactly as the script does.
The connection drops after a few minutes
The browser reached the end of its TTL and shut down. The scripts use ttl="5m" (1 credit). For longer runs, ask for more, up to "1h"; any TTL over 5 minutes costs 5 credits. Start a new browser for the next run.
MaxTurnsExceeded
The agent needed more tool calls than max_turns allows. Raise it, and raise the TTL with it.
A click times out
click matches visible text. Have the agent call read_page first, or add a tool that clicks by CSS selector for pages where text is ambiguous.
Next steps
See the same browser driven by screenshots in the Claude computer use guide, or run browser-use for a ready-made agent.