← Blog

Claude API Browser Automation Agent Example

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

If you're searching for a Claude API browser automation agent example, you want working code: how to wire Claude's tool-use capability to a real browser (Playwright or Puppeteer) so the model can navigate pages, click elements, read content, and decide what to do next. This article walks through exactly that — a minimal but functional agent loop, the tool schema Claude needs, and the practical issues you'll hit (timeouts, DOM noise, infinite loops) along with how to fix them.

The core idea is simple: Claude doesn't touch the browser directly. You give it a set of tools (navigate, click, type, read_page, screenshot), it decides which one to call and with what arguments, your code executes that action against a real browser instance, and you feed the result back. Claude reasons over the loop until it reports the task is done.

The Agent Loop

A browser automation agent built on Claude follows this cycle:

  1. Send the user's goal plus the current page state to Claude
  2. Claude responds with a tool call (e.g., click on a specific selector)
  3. Your code executes that action in Playwright
  4. You send the result (new page content, error, confirmation) back to Claude
  5. Repeat until Claude signals completion or hits a step limit

This is the same pattern used for any multi-step tool-use workflow, just applied to a browser instead of an internal API. The browser becomes an external "hands" for the model.

Defining the Tools

Claude needs tool definitions that map to real Playwright actions. Keep the schema tight — vague parameters lead to vague tool calls.

const tools = [
  {
    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 matching a 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: "read_page",
    description: "Return the visible text content of the current page",
    input_schema: { type: "object", properties: {} }
  }
];

Executing Tool Calls Against Playwright

The implementation side maps each tool name to an actual Playwright call, catches failures, and returns something Claude can reason about.

import { chromium } from "playwright";

const browser = await chromium.launch();
const page = await browser.newPage();

async function runTool(name, input) {
  try {
    switch (name) {
      case "navigate":
        await page.goto(input.url, { waitUntil: "domcontentloaded" });
        return { status: "ok", url: page.url() };
      case "click":
        await page.click(input.selector, { timeout: 5000 });
        return { status: "ok" };
      case "type_text":
        await page.fill(input.selector, input.text);
        return { status: "ok" };
      case "read_page":
        const text = await page.innerText("body");
        return { status: "ok", content: text.slice(0, 4000) };
      default:
        return { status: "error", message: "unknown tool" };
    }
  } catch (err) {
    return { status: "error", message: err.message };
  }
}

Truncating read_page output matters — full-page text on complex sites can blow past token budgets fast and derail the model's attention.

Wiring It to Claude

Here's the loop that drives the conversation, using SubToAPI's endpoint since it exposes the same tool-use contract as the native Claude API, just behind your own API key:

async function askClaude(messages) {
  const res = 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,
      messages
    })
  });
  return res.json();
}

let messages = [
  { role: "user", content: "Go to example.com, find the pricing link, and tell me the plan names." }
];

for (let step = 0; step < 10; step++) {
  const response = await askClaude(messages);
  messages.push({ role: "assistant", content: response.content });

  const toolUse = response.content.find(c => c.type === "tool_use");
  if (!toolUse) break; // Claude is done, final answer is in response.content

  const result = await runTool(toolUse.name, toolUse.input);
  messages.push({
    role: "user",
    content: [{
      type: "tool_result",
      tool_use_id: toolUse.id,
      content: JSON.stringify(result)
    }]
  });
}

The step limit (step < 10) is not optional. Browser agents can loop — clicking the same broken selector repeatedly — and a hard cap prevents runaway token usage and runaway browser sessions.

Practical Issues You'll Hit

Selectors that don't exist. Claude will sometimes guess a CSS selector based on read_page text rather than actual DOM structure. Returning the error message from Playwright (not just "failed") lets Claude self-correct on the next turn.

Stale page state. If a click triggers navigation, the previous read_page snapshot is now wrong. Always re-read the page after any action that might change the DOM before asking Claude to decide the next step.

Token cost of full-page text. Dense pages (navigation menus, footers, cookie banners) waste tokens. Stripping or truncating read_page output keeps the agent focused and keeps your cost per task predictable.

Timeouts. Set explicit Playwright timeouts (5–10s) on every action. A hung selector wait will stall the whole agent loop otherwise.

If you're running this in production and want streaming responses so users see the agent's reasoning live, the request shape is nearly identical — see /docs/streaming for the SSE format. For the full tool-use contract, including multi-tool calls in a single turn, check /docs/tools.

Scaling Beyond a Prototype

A single-script agent is fine for testing, but once you're running browser agents for multiple users or team workflows, you'll want per-user API keys, usage tracking, and seat management rather than one shared credential buried in an env file. SubToAPI wraps your existing Claude access into application keys (sub_live_...) with usage metadata per key, which makes it straightforward to run one browser-automation agent per customer or per internal team without sharing credentials. Plans start at €9/month for solo use, with team and scale tiers at /pricing, and there's a free trial at /signup if you want to test the tool-use flow against a real key before committing.

For the request/response shapes used in the examples above, /docs/messages covers the base Messages API and /docs/quickstart gets you from zero to a working key in a few minutes.

questions

Can Claude actually control a browser directly? No. Claude has no execution environment of its own. It outputs structured tool calls (like click or navigate), and your code is responsible for running those actions against a real browser via Playwright, Puppeteer, or similar.

Which browser automation library works best with Claude tool use? Playwright is the most common choice because of its reliable selectors, built-in waiting, and multi-browser support. Puppeteer works similarly — the Claude-side tool schema doesn't change, only your runTool implementation does.

How do I stop the agent from getting stuck in a loop? Enforce a hard step limit in your agent loop (e.g., 8–15 turns) and return detailed error messages from failed tool calls so Claude can adjust its approach instead of repeating the same failing action.

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 →