Browser automation has traditionally been used for UI testing, scraping, and repetitive browser tasks. AI agents introduce a different use case: allowing an application to decide which browser actions are required and execute those actions through controlled tools.
A browser agent might receive a request such as:
"Open the customer portal, find the latest invoice,
and download it."
Instead of implementing a fixed sequence of browser commands, an agent can reason about the task, choose available browser tools, inspect the current page, and continue through multiple steps.
The architecture is powerful, but it also introduces new risks. A browser agent can navigate to unexpected pages, click the wrong control, submit forms, or interact with sensitive information.
For that reason, a production browser agent should not simply connect an LLM to unrestricted browser automation. It should combine Playwright, .NET, tool boundaries, validation, permissions, and observability.
This article shows how to build the foundation of such a system.
What Is a Browser Automation Agent?
A traditional Playwright workflow is deterministic:
Open Browser
|
Navigate to URL
|
Find Element
|
Click
|
Fill Form
|
Submit
A browser automation agent introduces a decision layer:
User Goal
|
v
AI Agent
|
+----> Browser Tool
|
+----> Page Inspection
|
+----> Navigation
|
+----> Click / Fill
|
v
Task Result
The agent decides what to do next based on the current state of the browser.
This distinction is important.
Playwright provides browser control. The AI agent provides task-level decision making.
Playwright's .NET library exposes browser automation APIs for Chromium, Firefox, and WebKit, making it a useful foundation for implementing browser tools in a .NET application.
Why Use Playwright for Browser Agents?
Playwright provides several capabilities that are useful when building controlled browser automation:
Browser and context management
Page navigation
Locators
Element interaction
Screenshots
Network interception
Multiple browser engines
Automatic waiting around many interactions
Support for asynchronous .NET APIs
A deterministic Playwright operation might look like:
await page.GotoAsync("https://example.com");
await page.GetByRole(
AriaRole.Button,
new() { Name = "Sign in" })
.ClickAsync();
The challenge for an agent is deciding which page to open, which element to interact with, and whether the requested action is safe.
That is where the architecture becomes more important than the individual Playwright calls.
High-Level Architecture
A production-oriented browser agent can be divided into several layers:
+----------------------+
| User / Application |
+----------+-----------+
|
v
+----------------------+
| Agent Orchestrator |
+----------+-----------+
|
v
+----------------------+
| Tool Policy Layer |
+----------+-----------+
|
+-----+-----+
| |
v v
Browser Tools Read Tools
|
v
+----------------------+
| Playwright Runtime |
+----------+-----------+
|
v
Web Site
The policy layer is especially important.
The agent should not receive unrestricted access to every browser API.
Instead, expose a small set of well-defined tools.
Creating a .NET Project
Create a console application:
dotnet new console -n BrowserAgent
cd BrowserAgent
dotnet add package Microsoft.Playwright
Build the project:
dotnet build
Then install the Playwright browser binaries using the Playwright tooling appropriate to your project and operating environment.
Keep the Playwright package version pinned in production so that browser automation behavior is reproducible.
Launching a Browser
A basic Playwright setup looks like this:
using Microsoft.Playwright;
using var playwright = await Playwright.CreateAsync();
await using var browser =
await playwright.Chromium.LaunchAsync(
new BrowserTypeLaunchOptions
{
Headless = true
});
var page = await browser.NewPageAsync();
await page.GotoAsync("https://example.com");
Console.WriteLine(await page.TitleAsync());
For an agent, browser initialization should normally be handled by a dedicated service rather than recreated independently for every tool call.
Creating a Browser Service
Create an abstraction around Playwright:
public interface IBrowserService
{
Task NavigateAsync(
string url,
CancellationToken cancellationToken);
Task<string> GetPageTextAsync(
CancellationToken cancellationToken);
Task ClickAsync(
string selector,
CancellationToken cancellationToken);
Task FillAsync(
string selector,
string value,
CancellationToken cancellationToken);
}
Then implement it with Playwright:
public sealed class PlaywrightBrowserService
: IBrowserService
{
private readonly IPage _page;
public PlaywrightBrowserService(IPage page)
{
_page = page;
}
public async Task NavigateAsync(
string url,
CancellationToken cancellationToken)
{
await _page.GotoAsync(
url,
new PageGotoOptions
{
WaitUntil = WaitUntilState.DOMContentLoaded
});
}
public async Task<string> GetPageTextAsync(
CancellationToken cancellationToken)
{
return await _page.Locator("body")
.InnerTextAsync();
}
public Task ClickAsync(
string selector,
CancellationToken cancellationToken)
{
return _page.Locator(selector).ClickAsync();
}
public Task FillAsync(
string selector,
string value,
CancellationToken cancellationToken)
{
return _page.Locator(selector).FillAsync(value);
}
}
The abstraction gives the agent access to browser capabilities without exposing the entire Playwright API.
Why Tool Boundaries Matter
It may be tempting to expose a generic function such as:
ExecuteJavaScript(code)
That creates a very large security boundary.
A better design exposes narrowly scoped tools:
navigate
get_page_text
find_element
click
fill
take_screenshot
download_file
The agent can then operate within an explicit capability set.
For example:
public sealed record BrowserToolPolicy(
bool AllowNavigation,
bool AllowClick,
bool AllowFormInput,
bool AllowDownloads);
The policy can be evaluated before every tool invocation.
Creating Agent Tools
A browser agent needs tools that describe browser actions in language the model can understand.
A navigation tool might accept:
{
"url": "https://example.com"
}
A click tool might accept:
{
"target": "Sign in button"
}
A form tool might accept:
{
"field": "Email",
"value": "[email protected]"
}
The tool implementation is responsible for converting these high-level requests into safe Playwright operations.
Do not allow the model to construct arbitrary CSS or JavaScript unless the application explicitly requires it and the security model supports it.
Prefer Semantic Locators
When possible, use Playwright's semantic locator APIs.
For example:
await page.GetByRole(
AriaRole.Button,
new() { Name = "Continue" })
.ClickAsync();
This is generally easier to understand and maintain than a deeply nested CSS selector.
A browser agent also benefits from semantic descriptions because the tool can expose meaningful targets such as:
Button: Continue
Link: View invoice
Textbox: Email
Heading: Account Overview
rather than exposing the complete DOM.
Page State as Agent Context
An agent needs enough information about the current page to make its next decision.
A basic page-state object might be:
public sealed record BrowserState(
string Url,
string Title,
string VisibleText);
You can collect it with Playwright:
public async Task<BrowserState> GetStateAsync(
IPage page)
{
return new BrowserState(
page.Url,
await page.TitleAsync(),
await page.Locator("body").InnerTextAsync());
}
In production, sending the entire page text to an LLM can be inefficient and may expose sensitive information.
A better approach is to construct a compact representation containing only the information required for the next decision.
The Agent Loop
A simplified agent loop looks like this:
1. Receive goal
2. Inspect browser state
3. Ask model for next action
4. Validate requested action
5. Execute Playwright tool
6. Capture resulting state
7. Repeat until completed
In pseudocode:
while (!taskCompleted)
{
var state = await browser.GetStateAsync();
var action = await agent.DecideAsync(
goal,
state);
policy.Validate(action);
await tools.ExecuteAsync(action);
}
The loop needs a hard execution limit.
For example:
const int MaxSteps = 20;
for (var step = 0; step < MaxSteps; step++)
{
// Inspect, decide, validate and execute.
}
Without a limit, an agent can become trapped in repeated navigation or retry behavior.
Handling Authentication
Authentication is one of the most sensitive parts of browser automation.
Avoid giving the model direct access to passwords.
Instead, authentication should be handled by application-controlled infrastructure.
For example:
Application
|
v
Authenticated Browser Context
|
v
Agent
The agent can operate within an already authenticated context without receiving the underlying credentials.
Playwright browser contexts can also help isolate sessions.
For multi-user systems, each task should receive the appropriate isolated browser context rather than sharing one global session.
Session Isolation
A browser context can provide a separate environment for a task:
var context =
await browser.NewContextAsync();
var page =
await context.NewPageAsync();
This is useful when multiple users or tasks are processed concurrently.
The architecture should avoid accidentally sharing:
Cookies
Local storage
Session state
Authentication tokens
Download directories
between unrelated users.
Handling Downloads
Suppose an agent needs to download an invoice.
A controlled tool might look like:
var downloadTask =
page.WaitForDownloadAsync();
await page.GetByRole(
AriaRole.Link,
new() { Name = "Download Invoice" })
.ClickAsync();
var download = await downloadTask;
await download.SaveAsAsync(
Path.Combine(
outputDirectory,
download.SuggestedFilename));
The download destination should be controlled by the application.
Do not allow the model to arbitrarily choose filesystem paths.
Navigation Restrictions
A browser agent should have an allowlist where possible.
For example:
public bool IsAllowedUri(Uri uri)
{
return uri.Host.EndsWith(
".example.com",
StringComparison.OrdinalIgnoreCase);
}
Before navigation:
var uri = new Uri(requestedUrl);
if (!IsAllowedUri(uri))
{
throw new InvalidOperationException(
"Navigation target is not allowed.");
}
For enterprise applications, the allowlist can be more explicit than a domain suffix check.
Preventing Unsafe Actions
Not every browser action has the same risk.
A useful classification is:
| Action | Risk | Suggested Control |
|---|
| Read page | Low | Automatic |
| Navigate | Low/Medium | Domain policy |
| Click link | Medium | Target validation |
| Fill form | Medium/High | Field policy |
| Download | Medium | Controlled directory |
| Delete record | High | Human approval |
| Submit payment | Critical | Explicit approval |
| Change account settings | High | Explicit approval |
This distinction is essential for production systems.
A browser agent should not be treated as an ordinary automation script because the next action is dynamically generated.
Human Approval for High-Risk Actions
For sensitive operations, introduce an approval boundary:
Agent
|
v
Proposed Action
|
v
Risk Assessment
|
+---- Low Risk ---> Execute
|
+---- High Risk --> Human Approval
|
v
Execute
For example:
if (action.RiskLevel >= RiskLevel.High)
{
await approvalService.RequestApprovalAsync(
action,
cancellationToken);
}
The approval should occur before the irreversible action, not after it.
Handling Dynamic Websites
Modern web applications frequently update the DOM dynamically.
Avoid relying exclusively on fixed sleeps:
await Task.Delay(3000);
Instead, use Playwright's waiting and locator mechanisms.
For example:
await page
.GetByRole(
AriaRole.Button,
new() { Name = "Submit" })
.WaitForAsync();
Or wait for a meaningful page condition:
await page.WaitForURLAsync(
"**/dashboard");
This generally makes automation less dependent on arbitrary timing.
Error Handling
Browser agents need to distinguish between recoverable and non-recoverable failures.
Element not found
-> Inspect page again
Navigation timeout
-> Retry within limit
Authentication expired
-> Stop and re-authenticate
Forbidden action
-> Stop
Unexpected external domain
-> Stop
Repeated failed actions
-> Stop
A simple execution wrapper might look like:
try
{
await tool.ExecuteAsync(
action,
cancellationToken);
}
catch (PlaywrightException ex)
{
logger.LogWarning(
ex,
"Browser action failed: {Action}",
action.Name);
// Decide whether the action can be retried.
}
Do not automatically retry every browser action.
A click that submits an order is fundamentally different from a failed read operation.
Observability
Every agent step should be traceable.
Capture:
Task ID
Step Number
Current URL
Tool Name
Action Parameters
Execution Time
Result
Failure
Approval State
A structured log might look like:
logger.LogInformation(
"Browser agent step completed. " +
"TaskId={TaskId}, Step={Step}, Tool={Tool}, " +
"LatencyMs={Latency}",
taskId,
step,
toolName,
stopwatch.ElapsedMilliseconds);
Do not log passwords, session tokens, or unnecessary page content.
Screenshots can be useful for debugging, but they may also contain sensitive information and should be subject to appropriate retention and access controls.
Testing Browser Agents
Testing an agent requires more than testing Playwright selectors.
Separate testing into layers.
Unit Tests
Test:
URL policies
Action validation
Risk classification
Step limits
Domain allowlists
Browser Integration Tests
Test:
Navigation
Locators
Form interactions
Downloads
Authentication flows
Agent Evaluation
Test:
Whether the agent chooses the correct tool
Whether it stops when the goal is complete
Whether it recovers from expected failures
Whether it avoids prohibited actions
A useful evaluation dataset might contain:
Task 1: Find an invoice
Task 2: Search for an order
Task 3: Download a report
Task 4: Update a profile
Task 5: Attempt a prohibited operation
The last category is particularly important because a browser agent should demonstrate that it can refuse or escalate unsafe actions.
Production Architecture
A more complete deployment could look like:
+----------------+
| Application |
+-------+--------+
|
v
+---------------+
| Agent Service |
+-------+-------+
|
+-------v-------+
| Policy Engine |
+-------+-------+
|
+-------v-------+
| Tool Gateway |
+-------+-------+
|
+-------v-------+
| Playwright |
| Worker |
+-------+-------+
|
v
Web
For larger workloads, browser workers should be isolated from the main application process.
This provides better control over:
Browser Concurrency
Browsers can consume significant resources.
Do not create unlimited browser instances based on incoming request volume.
Instead, introduce a worker or concurrency limit:
var semaphore = new SemaphoreSlim(5);
await semaphore.WaitAsync(
cancellationToken);
try
{
await ExecuteBrowserTaskAsync(
task,
cancellationToken);
}
finally
{
semaphore.Release();
}
The appropriate limit depends on the workload and infrastructure. It should be determined through testing rather than copied from an arbitrary benchmark.
Advantages
Supports flexible browser workflows.
Can automate tasks that are difficult to express as fixed scripts.
Playwright provides mature browser-control primitives.
.NET provides strong application and dependency-injection support.
Tool boundaries can restrict what the agent is allowed to do.
Browser contexts can provide session isolation.
The same architecture can support multiple business workflows.
Disadvantages
Agent decisions are less deterministic than fixed automation.
Browser environments can change unexpectedly.
LLM calls add latency and cost.
Complex workflows require extensive testing.
Browser sessions consume significant resources.
Incorrect agent decisions can produce unintended actions.
Authentication and sensitive-data handling require careful design.
Common Mistakes
Giving the Agent Full Browser Access
Do not expose unrestricted browser APIs when a small set of controlled tools is sufficient.
Allowing Arbitrary JavaScript
Generic JavaScript execution significantly expands the agent's capabilities and security boundary.
Using Fixed Delays Everywhere
Prefer Playwright's waiting mechanisms and meaningful page conditions.
Sharing Browser Sessions
Shared contexts can leak authentication state and user data between tasks.
No Step Limit
Every agent execution should have a maximum number of actions.
Treating All Clicks as Safe
Some clicks trigger irreversible operations. Risk classification should happen before execution.
Logging Page Contents
Browser pages may contain passwords, personal information, tokens, financial details, and other sensitive content.
Best Practices
Keep browser control behind a dedicated service.
Expose narrow, purpose-built browser tools.
Use semantic Playwright locators where possible.
Enforce domain and navigation policies.
Isolate browser contexts between users and tasks.
Never provide raw credentials to the model.
Limit the number of agent steps.
Classify actions by risk.
Require approval for high-impact actions.
Use Playwright waiting mechanisms instead of arbitrary sleeps.
Limit browser concurrency.
Record structured telemetry for every agent step.
Protect screenshots, downloads, and logs as potentially sensitive data.
Test both successful workflows and unsafe-action scenarios.
Keep the browser worker isolated from unrelated application components.
Frequently Asked Questions
Is Playwright an AI agent framework?
No. Playwright is a browser automation library. An AI agent can use Playwright as a tool for interacting with websites.
Why use .NET for browser agents?
.NET provides a strong application ecosystem for APIs, dependency injection, background workers, logging, configuration, authentication, and enterprise integration, while Playwright provides the browser-control layer.
Should an agent control every browser action?
Not necessarily. A safer architecture limits the agent to approved tools and lets deterministic application code enforce navigation, permissions, and risk policies.
Can browser agents handle authenticated websites?
Yes, but authentication should be handled by application-controlled browser contexts or authentication infrastructure. Credentials and session secrets should not be exposed to the model.
How do I prevent an agent from performing dangerous actions?
Use tool permissions, domain restrictions, action risk classification, approval workflows, step limits, and deterministic validation before executing high-impact actions.
Should browser agents run in the same process as my API?
For small development workloads, they can. For production workloads, separating browser workers from the main API often provides better resource isolation and operational control.
Conclusion
Building a browser automation agent is not simply a matter of connecting an LLM to Playwright. The difficult part is creating a controlled boundary between an AI system that makes decisions and a browser that can perform real-world actions.
Playwright provides the low-level browser capabilities: navigation, locators, interaction, downloads, screenshots, and browser contexts. The agent layer adds task-level reasoning, while the policy layer determines what the agent is actually permitted to do.
A production architecture should therefore follow a clear principle:
Let the agent decide within a controlled capability set; let deterministic application code enforce the boundaries.
With isolated browser contexts, narrowly scoped tools, risk-based approvals, bounded execution, structured telemetry, and comprehensive testing, Playwright can serve as a practical browser-control layer for .NET-based AI agents without turning the browser into an unrestricted execution environment.