← Blog

How to Build a Browser Automation Agent with Claude

2026-10-30 · 5 min read · SubToAPI Team

Building a browser automation agent with Claude means pairing Claude's tool-use capability with a browser control library like Playwright or Puppeteer, then running a loop where Claude decides which action to take next (click, type, navigate, read the page) based on what it sees. Claude doesn't control a browser natively — you give it a set of tools (functions) that represent browser actions, and Claude calls them in sequence until the task is done.

This is different from simple scraping. A scraper follows a fixed script. A browser automation agent reasons about the page state, adapts when a layout changes, handles unexpected popups, and can follow instructions like "log in, find the invoice for March, download it" without you hardcoding every selector. Here's how to actually build one.

The core architecture

A Claude-driven browser agent has four pieces:

  1. A browser instance — Playwright or Puppeteer running headless or headed Chromium
  2. A tool schema — the set of actions Claude is allowed to call (navigate, click, type, screenshot, extract text)
  3. An observation loop — after each action, you feed the result back to Claude (usually a screenshot or DOM snippet)
  4. A stopping condition — Claude signals it's done, or you cap the number of steps

The loop looks like this conceptually:

1. Send task + current page state to Claude
2. Claude responds with a tool call (e.g. click selector "#login")
3. You execute that action in the real browser
4. You capture the new state (screenshot/text)
5. Send the result back to Claude as a tool_result
6. Repeat until Claude says "done" or you hit a step limit

Defining the tools

Keep the tool set small and composable. Trying to give Claude one giant "do anything" tool makes it less reliable than four or five well-scoped actions.

[
  {
    "name": "navigate",
    "description": "Go to a URL",
    "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": "get_page_text",
    "description": "Return visible text content of the current page",
    "input_schema": { "type": "object", "properties": {} }
  }
]

This is standard Claude tool-use syntax. If you're new to the format, the tools docs cover the input_schema structure and how tool_result blocks get sent back.

Wiring Playwright to the tool calls

Each tool name maps to an actual Playwright call. The key part is capturing enough page state after every action so Claude can reason about what happened.

import { chromium } from "playwright";

async function executeTool(page, toolName, input) {
  switch (toolName) {
    case "navigate":
      await page.goto(input.url);
      break;
    case "click":
      await page.click(input.selector);
      break;
    case "type_text":
      await page.fill(input.selector, input.text);
      break;
    case "get_page_text":
      return await page.innerText("body");
  }
  // Return a short snapshot after every action
  return await page.innerText("body").then(t => t.slice(0, 3000));
}

Truncating the page text matters. Dumping an entire HTML document into every turn burns tokens fast and confuses the model with noise like scripts and styles. Send visible text or a targeted selector, not raw HTML.

The agent loop

With tools defined and Playwright wired up, the loop that talks to Claude's Messages API looks roughly like this:

let messages = [{ role: "user", content: "Log into the dashboard and find the current invoice total." }];

for (let step = 0; step < 15; step++) {
  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,
      messages,
      tools
    })
  }).then(r => r.json());

  const toolUse = response.content.find(c => c.type === "tool_use");
  if (!toolUse) break; // Claude returned plain text — task likely finished

  const result = await executeTool(page, toolUse.name, toolUse.input);

  messages.push({ role: "assistant", content: response.content });
  messages.push({
    role: "user",
    content: [{ type: "tool_result", tool_use_id: toolUse.id, content: result }]
  });
}

This uses the Messages API format directly — if you're routing requests through SubToAPI, your existing Claude subscription becomes a standard HTTPS endpoint, so the same request shape works whether you're prototyping locally or running this agent in a production backend with usage tracking and team seats attached. The quickstart walks through getting an API key if you haven't set one up yet.

Guardrails that make or break reliability

Browser agents fail in predictable ways. A few rules that save you debugging time:

When to reach for this vs. plain scraping

If the site structure is stable and you know exactly what you need, a hardcoded Playwright script is faster and cheaper to run than a Claude-driven loop — every turn costs tokens and latency. Browser automation agents earn their complexity when the task involves judgment: interpreting ambiguous UI states, handling sites that change layout frequently, or following natural-language instructions from non-technical users who don't know CSS selectors.

Can Claude actually see and click on web pages?

Claude doesn't control a browser directly. You give it tools (navigate, click, type) backed by a real browser engine like Playwright, and Claude decides which tool to call based on the page state you send it — text or screenshots.

Which browser library works best with Claude?

Playwright is the most common choice because it supports multiple browsers, has reliable selector and screenshot APIs, and handles modern JavaScript-heavy sites well. Puppeteer works similarly but is Chromium-only.

How many steps should a browser agent take before stopping?

Cap it at 10–20 steps for most tasks. If Claude hasn't completed the goal by then, the task is likely too ambiguous or a selector is failing — better to surface an error than let the loop run indefinitely.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →