Build an AI Browser Agent with OpenAI Agents SDK and Playwright (2026)

Developer tutorial Β· Last reviewed 20 October 2026 Β· 14 min read

An AI browser agent built with the OpenAI Agents SDK and Playwright can read pages, follow links and extract data on its own, but only if you split the work correctly. This production-oriented Python tutorial walks through typed agent tools, Playwright browser control, structured extraction, routing traffic through ProxyEmpire proxies, sessions, approval gates, retries, prompt-injection defenses and scaling to hundreds of agents.

The short version

Let the model choose from a small, typed tool surface, and let your code do everything else. The Agents SDK provides the agent loop, tools, sessions, guardrails, human approval and tracing. Playwright provides deterministic browser execution. Your code enforces policy, the browser context owns web state, and the proxy session owns network identity.

Figure 1 β€” the architecture in one line
User goal β†’ OpenAI Agent β†’ typed function tools β†’ Playwright controller β†’ browser context β†’ ProxyEmpire proxy β†’ website

Key takeaways

  • Never expose the whole Playwright API to the model. Wrap it in a controller and expose a few narrow, typed tools.
  • Prompt instructions are not a security boundary. Enforce allowed domains and actions in code.
  • Return visible text or structured data, not raw HTML, and cap every tool output.
  • Agent session, browser context and proxy session are three different things. Model them as three fields.
  • Gate consequential actions such as submit, purchase or publish behind human approval.
  • Give every job a finite budget of steps, time, tokens, retries and bandwidth.

Why Combine an Agent Runtime with Playwright?

Adaptive decisions, controlled execution

A normal Playwright script is deterministic: you define navigation, locators, clicks and extraction in advance. An AI agent is adaptive: it can choose among allowed tools based on the user’s goal and the state returned by previous actions. Combining them gives you flexible decision-making without giving the model unrestricted browser or operating-system access.

SystemStrengthWeakness
Playwright scriptDeterministic, testable browser controlRigid when page structure or task sequence varies
LLM aloneFlexible reasoningCannot safely manipulate a real browser by itself
Agent + typed Playwright toolsAdaptive decisions with controlled executionNeeds explicit policy, state management and observability

If headless browsers are new to you, our headless browsers primer and the explainer on why modern web automation runs on Chromium cover the background.

What the OpenAI Agents SDK Provides

The agent loop and its building blocks

The Python Agents SDK includes agents, function tools, agents-as-tools and handoffs, guardrails, sessions, human-in-the-loop approval flows and tracing. Function tools derive their schemas from Python types and docstrings, which makes them a natural boundary around a Playwright controller.

Install the SDK, Playwright and Chromium
pip install openai-agents playwright
playwright install chromium

Set API credentials through the environment rather than in source code:

API key from the environment
export OPENAI_API_KEY="..."

Python 3.10 or later. Current releases of openai-agents require Python 3.10+. The SDK evolves quickly, so pin the version you test against.

Step 1: Build a Small Browser Controller

The controller owns the browser, not the agent

Do not expose the entire Playwright API to the model. Start with a controller that owns browser lifecycle, page state, network configuration and cleanup.

Example 1 β€” BrowserController (Python)
import os
from playwright.async_api import async_playwright

class BrowserController:
    def __init__(self):
        self._pw = None
        self.browser = None
        self.context = None
        self.page = None

    async def start(self):
        self._pw = await async_playwright().start()
        self.browser = await self._pw.chromium.launch(headless=True)

        proxy = None
        if os.getenv("PROXY_SERVER"):
            proxy = {
                "server": os.environ["PROXY_SERVER"],
                "username": os.environ.get("PROXY_USER", ""),
                "password": os.environ.get("PROXY_PASSWORD", ""),
            }

        self.context = await self.browser.new_context(proxy=proxy)
        self.page = await self.context.new_page()

    async def navigate(self, url: str) -> str:
        await self.page.goto(url, wait_until="domcontentloaded")
        return f"Opened {self.page.url}"

    async def page_text(self, max_chars: int = 12000) -> str:
        text = await self.page.locator("body").inner_text()
        return text[:max_chars]

    async def close(self):
        if self.context:
            await self.context.close()
        if self.browser:
            await self.browser.close()
        if self._pw:
            await self._pw.stop()

The controller, not the agent, should launch processes, hold Playwright object references, apply proxy settings and clean up contexts.

Step 2: Enforce Destinations in Code

Prompts are not a network security boundary

Prompt instructions are useful, but they are not a security boundary. Enforce allowed destinations before Playwright navigates:

Example 2 β€” a domain allowlist (Python)
from urllib.parse import urlparse

def assert_allowed_url(url: str, allowed_domains: set[str]):
    host = (urlparse(url).hostname or "").lower()
    if host not in allowed_domains:
        raise ValueError(f"Domain not allowed: {host}")

A production validator should also account for redirects, subdomains, URL normalization, loopback and private-network addresses, and SSRF-style routing risks. The important point is that destination policy lives in code, not only in the agent prompt.

Step 3: Expose Typed Browser Actions as Agent Tools

Narrow, semantic, easy to validate

The Agents SDK provides the @tool decorator in agents.decorators, an alias for function_tool. Keep tools narrow, semantic and easy to validate:

Example 3 β€” two read-only tools and an agent (Python)
from agents import Agent, Runner
from agents.decorators import tool

browser = BrowserController()
ALLOWED_DOMAINS = {"example.com", "www.example.com"}

@tool
async def open_url(url: str) -> str:
    """Open an allowed public URL in the browser."""
    assert_allowed_url(url, ALLOWED_DOMAINS)
    return await browser.navigate(url)

@tool
async def read_page(max_chars: int = 12000) -> str:
    """Return visible text from the current page."""
    return await browser.page_text(max_chars=max_chars)

agent = Agent(
    name="Web Research Agent",
    instructions=(
        "Use browser tools to research the user's request. "
        "Do not submit forms, purchase anything, delete data, "
        "publish content, or access private accounts."
    ),
    tools=[open_url, read_page],
)

The SDK infers each tool’s schema from its signature and docstring. Critical restrictions should still be checked inside the tool or controller, so a poor model decision cannot bypass them.

Step 4: Run the Agent

The runner manages the tool loop
Example 4 β€” running the agent (Python)
import asyncio

async def main():
    await browser.start()
    try:
        result = await Runner.run(
            agent,
            "Open https://example.com and summarize what the page is for.",
        )
        print(result.final_output)
    finally:
        await browser.close()

asyncio.run(main())

The runner manages the agent/tool loop. When the model selects a browser tool, your Python function executes locally and returns a controlled result to the agent.

Do not return the entire DOM to the model

Raw HTML is often the wrong tool output. It can contain scripts, repeated navigation, hidden elements, tracking metadata and thousands of irrelevant tokens. Instead:

  • Prefer visible text or structured extraction over raw HTML.
  • Cap tool outputs and paginate when necessary.
  • Expose focused tools such as get_links(), get_headings() or extract_table().
  • Use Playwright locators and accessibility semantics for controls.
  • Store large artifacts outside the model context and return references or summaries.

Step 5: Add Controlled Interaction

Semantic inputs, resolved by your controller

Avoid tools that accept arbitrary JavaScript or unrestricted selectors from the model. Prefer semantic inputs that your controller resolves through Playwright:

Example 5 β€” clicking by visible text (Python)
@tool
async def click_text(text: str) -> str:
    """Click one visible link or button with the exact supplied text."""
    locator = browser.page.get_by_text(text, exact=True).first
    await locator.click()
    await browser.page.wait_for_load_state("domcontentloaded")
    return f"Clicked {text!r}. Current URL: {browser.page.url}"

For a real product, add action categories, explicit allowlists, preconditions and post-action verification.

Hard rule. A model-generated string should never become an unrestricted shell command, filesystem path, JavaScript program or privileged browser operation.

Step 6: Return Structured Data When the Task Has a Schema

Typed outputs are easier to trust
Example 6 β€” a typed list_links tool (Python)
from typing import TypedDict

class LinkItem(TypedDict):
    text: str
    href: str

@tool
async def list_links(limit: int = 30) -> list[LinkItem]:
    """Return visible links from the current page."""
    links = browser.page.locator("a:visible")
    count = min(await links.count(), limit)
    output = []

    for i in range(count):
        item = links.nth(i)
        output.append({
            "text": (await item.inner_text()).strip(),
            "href": await item.get_attribute("href") or "",
        })

    return output

Typed outputs reduce ambiguity and make browser tools easier to validate, log, test and pass to downstream systems.

Step 7: Route Browser Traffic Through ProxyEmpire

Network identity without exposing credentials to the model

Playwright accepts a proxy per browser context. The controller above reads the proxy settings from environment variables, so ProxyEmpire can be added without the model ever seeing the credentials:

Example 7 β€” proxy settings for the controller
export PROXY_SERVER="http://v2.proxyempire.io:5000"
export PROXY_USER="YOUR_PROXY_USERNAME"
export PROXY_PASSWORD="YOUR_PROXY_PASSWORD"

Copy the username from the Connection Builder in the ProxyEmpire dashboard. It carries the targeting and session settings: for a sticky session, the dashboard adds a session ID (sid) to the username, as described in how to use proxy sticky sessions.

Use an HTTP proxy URL in Playwright. Playwright supports HTTP and SOCKS proxies, but it rejects username/password authentication on SOCKS5 proxies (“Browser does not support socks5 proxy authentication”). With ProxyEmpire’s username and password login, keep PROXY_SERVER on http://.

ProxyEmpire offers 30M+ rotating residential IPs with country, region, city and ISP targeting, plus new-IP-per-request, sticky-session and timed-rotation modes. Use the endpoint and username generated in the dashboard for the exact product and targeting profile you need.

Browser context owns cookies, local storage and tabs. Proxy session owns network exit identity. Agent session owns conversation and task memory. Treat them as three different fields in your job model.

Step 8: Use Agents SDK Sessions for Conversational Memory

Memory for the conversation, not the browser

The Agents SDK has built-in session memory. A session persists conversation history across Runner.run calls. It does not persist Playwright cookies or guarantee the same proxy IP.

Example 8 β€” SQLiteSession across two runs (inside an async function)
from agents import SQLiteSession

session = SQLiteSession("browser_research_123", "agent_sessions.db")

first = await Runner.run(
    agent,
    "Open the allowed site and identify the main pricing plans.",
    session=session,
)

second = await Runner.run(
    agent,
    "Now compare the two cheapest plans from the previous result.",
    session=session,
)

For production, choose a session backend that matches your deployment. The SDK includes local SQLite options and additional backends for distributed applications. Keep browser persistence as a separate concern.

StateOwned byHolds
Agent sessionAgents SDK session backendConversation and task memory
Browser statePlaywright browser contextCookies, local storage and tabs
Network identityProxy sessionThe exit IP the website sees

Step 9: Require Approval for Consequential Tools

Observing is not the same as acting

The Agents SDK can pause a run for human approval before a tool executes. Browser agents benefit from this, because observing a page is fundamentally different from creating an external side effect.

Example 9 β€” a tool that needs approval (Python)
@tool(needs_approval=True)
async def submit_current_form() -> str:
    """Submit the current form after a human approves the action."""
    await browser.page.locator("form").first.evaluate("form => form.requestSubmit()")
    return "Form submitted."

In the application layer, handle the resulting interruption, show the pending action to the user, and resume only after approval. Keep read-only tools autonomous where appropriate, but gate actions such as submit, publish, purchase, delete, send or account changes.

Production: Jobs, Retries and Budgets

Policy as data, not prompt folklore

A production job model

Example 10 β€” BrowserJob (Python)
from dataclasses import dataclass

@dataclass
class BrowserJob:
    job_id: str
    goal: str
    allowed_domains: set[str]
    proxy_profile: str
    browser_profile: str
    agent_session_id: str
    max_steps: int = 30
    deadline_seconds: int = 120

This turns policy into data instead of prompt folklore. Workers can enforce domain rules, step budgets, deadlines, browser isolation and network profiles even when the model makes a poor decision.

Retries: retry the layer that failed

FailureLikely responseAvoid
Transient navigation timeoutRetry navigation with bounded backoffRestarting the whole agent immediately
Missing locatorRefresh page state and let the agent reconsiderBlind repeated clicking
Proxy connection failureRetry or replace the network sessionAssuming the model or target is at fault
403 / 429 from destinationRespect target policy; slow down or stop as appropriateInfinite IP rotation
Consequential action uncertainPause for approvalGuessing or auto-submitting

Blindly restarting the entire workflow is expensive and can duplicate side effects. Classify failures so the retry happens at the browser, network, destination or agent layer that actually failed. Our reference on proxy error codes helps tell network failures from destination responses.

Step budgets prevent infinite agents

Every browser-agent job should have a finite budget. Otherwise a model can loop between pages, repeatedly inspect the same state, or keep searching after the marginal value has gone.

StepsMaximum agent turns or tool calls per job
TimeMaximum wall-clock duration
TokensMaximum model/token budget
ScopeMaximum pages or domains visited
RetriesMaximum retries per failure class
BandwidthMaximum downloaded bytes and proxy bandwidth

When a budget is exhausted, return an explicit partial result with the reason instead of an opaque crash.

Prompt Injection Is a Browser Security Problem

The agent reads adversarial content by design

A browser agent consumes untrusted web content by design. A page can contain instructions that try to redirect the model, reveal secrets or trigger unrelated actions. A stronger prompt is not enough on its own:

  1. Treat page instructions as untrusted data.
  2. Keep tool permissions narrower than the model’s language capabilities.
  3. Never expose proxy or API credentials through browser-readable tool outputs.
  4. Validate destinations and redirects outside the model.
  5. Use low-privilege accounts for automation.
  6. Require approval for irreversible or externally consequential actions.

Scaling, Tracing and Testing

From one agent to hundreds

Scaling from one agent to hundreds

Figure 2 β€” workers, browser contexts and the proxy layer
API / scheduler
      |
      v
durable job queue
      |
  +---+----------+
  |              |
  v              v
worker 1  ...  worker N
  |              |
browser        browser
context        context
  |              |
  +------+-------+
         v
    proxy layer
         |
         v
      websites

Scale horizontally with workers, but keep browser concurrency bounded inside each worker. A useful capacity metric is successful jobs per unit of infrastructure and cost, not maximum simultaneous tabs.

  • Use a durable queue and backpressure.
  • Measure memory per active browser context.
  • Keep per-tenant and per-destination concurrency limits.
  • Map each job to an explicit browser profile and proxy profile.
  • Recycle unhealthy or excessively long-lived browser processes.

For capacity planning at larger scale, see our guide to designing a browser cluster for 10,000 concurrent Chromium sessions.

Tracing and observability

The Agents SDK includes tracing for agent flows. Combine those traces with browser and proxy telemetry so you can follow one job end to end:

LayerRecord
Jobjob_id and the agent run/trace identifier
ToolsTool name, argument classification and duration
BrowserPage URL/domain, navigation latency, browser context/profile identifier
NetworkProxy pool/session identifier (never the password)
ModelModel usage and cost
OutcomeStatus, failure classification, final outcome and validation result

Testing browser agents

  1. Unit-test browser tools without the model.
  2. Test policy enforcement with disallowed domains and actions.
  3. Run deterministic fixtures against stable demo pages.
  4. Evaluate whether the agent chooses the right tool sequence.
  5. Inject navigation timeouts and proxy failures.
  6. Test prompt-injection pages in a controlled environment.
  7. Measure success rate, steps, latency, model cost and proxy cost on a repeatable benchmark set.

When to Use Playwright MCP Instead

Local tools or a protocol service

Direct Agents SDK function tools are a strong fit when one Python service owns the agent and the browser runtime. Playwright MCP is a stronger fit when browser automation should be a reusable protocol service for multiple AI clients, or when standardized MCP tool discovery matters. Those AI clients can also manage their proxies over MCP: the ProxyEmpire MCP server gives them proxy lists, coverage checks and bandwidth data from your account.

QuestionAgents SDK + local Playwright toolsPlaywright MCP
One Python service owns everythingStrong fitPossible, but adds a protocol boundary
Custom Python policy in each toolVery directRequires MCP/server-side policy design
Many AI clients need the same browser toolsMore integration workStrong fit
Standard MCP tool surface requiredNoYes

Production Checklist

Before you ship a browser agent

Checklist

  • Expose a narrow, typed browser tool surface.
  • Keep browser lifecycle outside the model.
  • Enforce domain and network policy in code.
  • Use isolated browser contexts for unrelated jobs.
  • Keep agent session, browser session and proxy session separate.
  • Use ProxyEmpire sticky sessions intentionally, not as random retries.
  • Require approval for consequential actions.
  • Set step, time, token, retry and bandwidth budgets.
  • Use bounded worker concurrency and durable queues.
  • Trace agent decisions together with browser and network telemetry.
  • Benchmark successful outcomes per dollar, not just raw throughput.

Final Thoughts

Deterministic tools, adaptive decisions

The goal is not to make Playwright “intelligent.” Keep Playwright deterministic and give the agent a carefully designed set of actions.

The agent reasons; tools execute; browser contexts hold web state; proxy sessions provide network identity; policies constrain risk; queues constrain load; tracing explains what happened.

Once those responsibilities are explicit, the jump from a demo that opens a webpage to a production browser-agent platform becomes much more manageable.

Frequently Asked Questions

AI browser agents, in short
Can the OpenAI Agents SDK control a browser?

Not directly. It runs the agent loop and calls the tools you give it. Wrap Playwright in a controller and expose a few typed function tools, such as opening an allowed URL or reading visible page text; the SDK then decides which tool to call, and your Python code performs the browser action.

How do I use a proxy with a Playwright AI agent?

Pass a proxy to browser.new_context() with a server, username and password, and load those values from environment variables so the model never sees them. With ProxyEmpire, use http://v2.proxyempire.io:5000 and the username from the dashboard’s Connection Builder. Playwright does not support username/password authentication on SOCKS5 proxies, so use the HTTP URL.

Does an Agents SDK session keep the same browser cookies or IP?

No. A session persists conversation history across runs. Cookies, local storage and tabs live in the Playwright browser context, and the exit IP belongs to the proxy session. Manage the three separately.

How do I stop a browser agent from submitting forms or buying things?

Don’t give it a tool that can, or mark such tools with needs_approval=True so the run pauses for a human before the action executes. Enforce allowed domains and actions in code rather than relying on prompt instructions.

Should I use Playwright MCP or local Playwright tools?

Local function tools fit best when one Python service owns the agent and the browser and you want custom policy in each tool. Playwright MCP fits best when several AI clients need the same browser tools or you need a standard MCP tool surface.

Give your browser agents a reliable network identity

30M+ rotating residential IPs with country, region, city and ISP targeting, sticky sessions or a new IP per request, and per-context proxy settings that keep credentials out of the model.

ProxyEmpire proxy types and prices

ProxyEmpire

Proxies for data collection at scale.

Residential, mobile, ISP and datacenter proxies for scraping, ad verification, SEO monitoring and multi-account work. Choose from 30M+ ethically sourced residential IPs and 4M+ 4G/5G mobile IPs in 170+ countries, target by country, city, ZIP, ISP or carrier, and connect over HTTP(S) or SOCKS5 with 99.9% uptime. Try residential and mobile proxies for $1.97.

🏘️ Rotating Residential Proxies
  • 30M+ Premium Residential IPs
  • Β 170+ Countries
    Every residential IP in our network corresponds to an actual desktop device with a precise geographical location. Our residential proxies are fast and reliable, with 99.9% uptime, and work for a wide range of use cases. You can use country, region, city, ZIP, ISP, ASN and OS fingerprint targeting for our rotating residential proxies, at no extra charge.

See our Rotating Residential Proxies

πŸ“ Static Residential Proxies
  • 24 Countries
    Buy a dedicated static residential IP from one of the 24 countries that we offer proxies in. Keep the same IP for a month or longer, while benefiting from their fast speed and stability.

See our Static Residential Proxies

πŸ“³ Rotating Mobile Proxies
  • 4M+ Premium Mobile IPs
  • Β 170+ Countries
    Access millions of clean mobile IPs with precise targeting by country, region, city, ZIP, mobile carrier, ASN and OS fingerprint, at no extra charge. Get far fewer IP blocks and CAPTCHAs with our 4G and 5G proxies.

See our Mobile Proxies

πŸ“± Dedicated Mobile Proxies
  • 4 Countries
  • 50+ Locations
    Get your own dedicated mobile proxy in one of our supported locations, with unlimited bandwidth and unlimited IP changes on demand. A great choice when you need a small number of mobile IPs and a lot of proxy bandwidth.

See our 4G & 5G Proxies

🌐 Rotating Datacenter Proxies
  • 197,000+ IPs Premium IPs
  • Β 62 Countries
    On a budget and need to do some simple scraping tasks? Our datacenter proxies are the perfect fit! Get started with as little as $2

See our Datacenter Proxies

♾️ Unlimited Residential Proxies
  • 1M+ Daily Residential IPs
  • 85+ Countries
    Our unlimited residential proxies bill by connection speed, not gigabytes: 100 to 1000 Mbit with unlimited bandwidth, on daily, weekly or monthly plans. Ask us for a 1-hour trial at 50 Mbit before you pick a plan.

See our Unlimited Residential Proxies

proxy locations

30M+ rotating IPs

99.9% uptime - high speed

99.9% uptime.

dedicated support team

24/7 Dedicated Support.

fair price

Fair Pricing.

🏠 Residential Proxies Rotating / Static / Unlimited
πŸ“± Mobile Proxies Rotating and Dedicated
πŸ–₯️ Datacenter Proxies Rotating
🌍 IP Pool 30M+ residential + 4M+ mobile IPs
πŸ“Ά Uptime 99.9% Β· Live status
πŸ’³ Payment Card Β· PayPal Β· Crypto Β· Bank transfer
πŸ’¬ Support 24/7 live chat Β· [email protected]