Claude API Browser Automation Agent: Build Guide
A browser automation agent built on the Claude API is a program that lets Claude control a real web browser: navigating pages, clicking elements, filling forms, reading content, and taking screenshots, then deciding what to do next based on what it sees. Instead of hardcoding selectors and scripts, you give Claude a set of "browser tools" and let it reason about the page state at each step.
This pattern is useful for tasks that are too variable for traditional scrapers or scripted automation: filling out forms on sites you don't control, QA testing flows across UI changes, extracting data from pages with inconsistent structure, or running multi-step research tasks that involve clicking through several pages. The rest of this guide covers the architecture, the tool definitions you need, and a working example using Playwright.
Why Claude for browser automation
Traditional browser automation (Selenium, Playwright scripts) requires you to know the exact DOM structure in advance. That breaks the moment a site redesigns, and it can't handle pages it's never seen. Claude changes this by closing the loop: the agent reads the current page state (as text, accessibility tree, or screenshot), decides on an action, executes it through a tool call, observes the result, and repeats.
Claude's vision capability is particularly relevant here. You can send a screenshot of the current viewport along with the DOM text, and Claude can identify buttons, form fields, or content even when selectors are messy or dynamically generated.
Core architecture
A browser automation agent has four moving parts:
- Browser runtime — Playwright or Puppeteer running headless or headful, controlled server-side.
- Tool layer — functions like
navigate,click,type,screenshot,extract_textexposed to Claude as tool definitions. - Agent loop — sends the conversation plus tool results back to Claude until it signals it's done.
- State capture — screenshots and/or simplified DOM snapshots fed back after every action so Claude can reason about the new page state.
Defining browser tools
Here's a minimal tool schema for a Claude-driven browser agent:
[
{
"name": "navigate",
"description": "Go to a URL in the browser",
"input_schema": {
"type": "object",
"properties": { "url": { "type": "string" } },
"required": ["url"]
}
},
{
"name": "click",
"description": "Click an element by CSS selector",
"input_schema": {
"type": "object",
"properties": { "selector": { "type": "string" } },
"required": ["selector"]
}
},
{
"name": "type_text",
"description": "Type text into an input field",
"input_schema": {
"type": "object",
"properties": {
"selector": { "type": "string" },
"text": { "type": "string" }
},
"required": ["selector", "text"]
}
},
{
"name": "screenshot",
"description": "Capture the current viewport as an image",
"input_schema": { "type": "object", "properties": {} }
}
]
The agent loop with Playwright
import { chromium } from "playwright";
async function runStep(toolName, input, page) {
switch (toolName) {
case "navigate":
await page.goto(input.url);
return "navigated";
case "click":
await page.click(input.selector);
return "clicked";
case "type_text":
await page.fill(input.selector, input.text);
return "typed";
case "screenshot":
const buffer = await page.screenshot();
return buffer.toString("base64");
}
}
async function agentLoop(task) {
const browser = await chromium.launch();
const page = await browser.newPage();
let messages = [{ role: "user", content: task }];
while (true) {
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
tools: browserTools,
messages
})
}).then(r => r.json());
const toolUse = response.content.find(b => b.type === "tool_use");
if (!toolUse) break; // Claude is done
const result = await runStep(toolUse.name, toolUse.input, page);
messages.push({ role: "assistant", content: response.content });
messages.push({
role: "user",
content: [{ type: "tool_result", tool_use_id: toolUse.id, content: result }]
});
}
await browser.close();
}
This is the same request/response shape as any other Claude tool use workflow — the browser is just the environment the tools act on. If you're new to tool calling in general, see /docs/tools for the full schema reference.
Feeding back page state
The biggest practical challenge isn't the tool calls themselves — it's giving Claude enough context to make good decisions without blowing the context window. A few approaches that work well in combination:
- Screenshots for visual layout and when selectors are unreliable (pass them as image content blocks).
- Simplified DOM (just visible text + interactive elements, not full HTML) for dense pages.
- Explicit step limits — cap the agent at 15-20 tool calls per task to avoid runaway loops on sites that behave unexpectedly.
- Trimming history — once a page is navigated away from, you can usually drop its screenshot from the conversation to save tokens; see /docs/messages for how message history is structured.
Running this through SubToAPI
If your team is already paying for Claude access through a shared subscription, routing the agent loop above through SubToAPI gives you an API key (sub_live_...) instead of managing raw Claude credentials directly. That matters for browser agents specifically because they tend to be run from backend services or CI pipelines where you want per-service keys, usage visibility, and the ability to revoke access without touching your main account. Streaming support (/docs/streaming) is also useful if you're surfacing agent progress in a UI while it works through a multi-step browser task.
Setup takes the same shape as any other integration — grab a key after /signup, swap the base URL, and the request/response format above works unchanged. Plans start at €9/month on the Solo tier, with Team and Scale tiers adding seats for multiple services or engineers; see /pricing for details.
Practical limits to plan for
Browser automation agents are slower and more expensive per task than text-only agents because every step typically includes a screenshot and a round trip to the model. Budget for:
- Latency: each tool call cycle involves browser action + screenshot + a full model round trip, often 2-5 seconds per step.
- Token cost: screenshots consume meaningful tokens; downscale images before sending when fine detail isn't needed.
- Failure handling: build retries for selector misses and add a fallback where Claude can request a fresh screenshot if an action didn't produce the expected result.
Start with /docs/quickstart to get a key working end to end, then layer the browser tool loop on top once basic requests are confirmed.
questions
Can Claude directly control a browser without Playwright or Puppeteer? No. Claude doesn't execute code or control hardware — it only returns tool_use requests. You need a browser automation library to actually perform the navigate/click/type actions and report results back.
How many steps can a browser agent take before it gets too expensive? There's no hard limit, but most practical agents cap at 15-30 tool calls per task. Beyond that, costs and latency usually indicate the task should be broken into smaller sub-tasks.
Does Claude need screenshots, or can it work from DOM text alone? Both work, and combining them is often best. DOM text is cheaper and precise for selectors; screenshots help when layout, visual state, or ambiguous elements matter more than exact markup.