For AI agents: a documentation index is available at /llms.txt
Skip to main content

OpenAI Computer Use Integration

OpenAI's computer use tool looks at screenshots and returns structured actions (click, type, scroll) that Playwright executes in a Browserless cloud browser. This guide builds that loop with the Responses API, so you can run form filling, web research, and data extraction without managing browser infrastructure yourself.

Prerequisites
  • Browserless API token (available in your account dashboard)
  • OpenAI API key (get it from OpenAI API Keys) for a project with access to a model that supports the computer tool
  • Node.js 22+ or Python 3.10+
computer-use-preview has been shut down

OpenAI shut down the computer-use-preview model on July 23, 2026. Requests that use it, or the computer_use_preview tool type, no longer work. This guide uses the generally available computer tool with gpt-6.1-sol. If you're updating an older integration, see OpenAI's migration guide.

How it works​

  1. Send the task: call the Responses API with the task and a computer tool.
  2. Execute actions: the model returns a computer_call with an ordered actions array. Run each action through Playwright.
  3. Return a screenshot: send a fresh screenshot back as a computer_call_output, chained with previous_response_id.
  4. Loop until the model stops returning computer_call items.

The computer tool runs client-side, so you write the loop. If you'd rather have OpenAI call Browserless tools server-side with no browser code at all, see the OpenAI SDK page.

Step-by-step setup​

In this guide you'll build an example that navigates to Bing, searches for "Browserless.io", and returns a summary of what the company does. We use stealth mode to avoid bot detection.

  1. Set your API keys

    Grab your Browserless token from your account dashboard and your OpenAI key from OpenAI API Keys.

    BROWSERLESS_API_KEY=your-browserless-token
    OPENAI_API_KEY=your-openai-key
  2. Install dependencies

    npm install openai playwright-core
    npm install -D tsx @types/node

    You don't need to install a local browser. Playwright only acts as the client for the Browserless browser.

  3. Connect to Browserless

    Put the snippets from this step onward in one file, agent.mts or agent.py. In Python, put them all inside one async def main() function, and end the file with import asyncio and asyncio.run(main()), since the snippets use await. Connect over CDP with stealth mode (recommended for avoiding bot detection):

    import { chromium, Page } from "playwright-core";
    import OpenAI from "openai";

    // Reads OPENAI_API_KEY from the environment.
    const client = new OpenAI();
    const MODEL = "gpt-6.1-sol";

    const browser = await chromium.connectOverCDP(
    `wss://production-sfo.browserless.io/chromium/stealth?token=${process.env.BROWSERLESS_API_KEY}`,
    { timeout: 60000 }
    );
    // OpenAI reports strong results at 1440x900. The model returns coordinates in
    // screenshot pixels, so keep the viewport and screenshots the same size.
    const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
    const page = await context.newPage();
  4. Navigate and add a screenshot helper

    Open the start page and define the helper the loop uses to capture screenshots. Over remote WebSocket connections, standard Playwright screenshots can time out, so the helper falls back to CDP:

    await page.goto("https://www.bing.com", { waitUntil: "domcontentloaded" });

    async function getScreenshot(page: Page): Promise<string> {
    try {
    const buffer = await page.screenshot({ type: "png", timeout: 10000 });
    return buffer.toString("base64");
    } catch {
    // Fall back to CDP directly when the Playwright screenshot times out.
    const cdp = await page.context().newCDPSession(page);
    // Raw CDP captures in device pixels, which the stealth route scales up from the
    // viewport size. Scale the capture back so it matches the viewport the model clicks in.
    const { cssLayoutViewport, cssVisualViewport, layoutViewport } = await cdp.send("Page.getLayoutMetrics");
    // Device pixels per CSS pixel, rounded so a scrollbar's width doesn't skew it.
    const deviceScale = Math.round((layoutViewport.clientWidth / cssLayoutViewport.clientWidth) * 100) / 100;
    const { width, height } = page.viewportSize()!;
    const result = await cdp.send("Page.captureScreenshot", {
    format: "png",
    clip: { x: cssVisualViewport.pageX, y: cssVisualViewport.pageY, width, height, scale: 1 / deviceScale },
    });
    await cdp.detach();
    return result.data;
    }
    }
  5. Send the initial request to the model

    Send the task with the computer tool. You don't need to attach a screenshot: if the model needs to see the page first, its first computer_call contains a single screenshot action.

    const task = "Search for 'Browserless.io' and tell me what the company does";

    let response = await client.responses.create({
    model: MODEL,
    tools: [{ type: "computer" }],
    input: task,
    });
  6. Process actions and loop

    Each computer_call holds an ordered actions array. Run every action in order, capture one screenshot, and send it back with the matching call_id. Repeat until no computer_call appears (task complete).

    note

    The model sends key names like CTRL, ENTER, or CMD, and the mouse button wheel. Playwright expects Control, Enter, Meta, and middle. The examples below map them.

    // Map model key names to Playwright key names.
    const keyMap: Record<string, string> = {
    enter: "Enter", return: "Enter", esc: "Escape", escape: "Escape",
    tab: "Tab", space: "Space", backspace: "Backspace", delete: "Delete",
    home: "Home", end: "End", pageup: "PageUp", pagedown: "PageDown",
    up: "ArrowUp", down: "ArrowDown", left: "ArrowLeft", right: "ArrowRight",
    arrowup: "ArrowUp", arrowdown: "ArrowDown", arrowleft: "ArrowLeft", arrowright: "ArrowRight",
    capslock: "CapsLock", insert: "Insert",
    ctrl: "Control", control: "Control", shift: "Shift", alt: "Alt", option: "Alt",
    cmd: "Meta", command: "Meta", meta: "Meta", super: "Meta", win: "Meta",
    };
    const toKey = (key: string) => keyMap[key.toLowerCase()] ?? key;
    // Playwright has no "back" or "forward" mouse button, so runAction maps those to history navigation.
    const buttonMap: Record<string, "left" | "right" | "middle"> = { left: "left", right: "right", wheel: "middle" };

    type Action = {
    type: string;
    x?: number; y?: number; button?: string; keys?: string[] | null;
    text?: string; scroll_x?: number; scroll_y?: number;
    path?: { x: number; y: number }[];
    };

    // Mouse actions can carry modifier keys (e.g. shift-click) in `keys`.
    async function withModifiers(keys: string[] | null | undefined, run: () => Promise<void>) {
    const modifiers = (keys ?? []).map(toKey);
    for (const key of modifiers) await page.keyboard.down(key);
    try {
    await run();
    } finally {
    for (const key of modifiers.reverse()) await page.keyboard.up(key);
    }
    }

    async function runAction(action: Action) {
    switch (action.type) {
    case "click": {
    // The browser's back and forward mouse buttons navigate history.
    // Pages restored from the back-forward cache don't fire load events again,
    // so wait for the document to be parsed instead.
    if (action.button === "back" || action.button === "forward") {
    await (action.button === "back" ? page.goBack({ waitUntil: "commit" }) : page.goForward({ waitUntil: "commit" }));
    await page.waitForFunction(() => document.readyState !== "loading");
    break;
    }
    const button = buttonMap[action.button ?? "left"];
    if (!button) throw new Error(`Unsupported mouse button: ${action.button}`);
    await withModifiers(action.keys, () => page.mouse.click(action.x!, action.y!, { button }));
    break;
    }
    case "double_click":
    await withModifiers(action.keys, () => page.mouse.dblclick(action.x!, action.y!));
    break;
    case "move":
    await withModifiers(action.keys, () => page.mouse.move(action.x!, action.y!));
    break;
    case "drag": {
    const [start, ...rest] = action.path ?? [];
    if (!start || rest.length === 0) throw new Error("drag needs at least two path points");
    await withModifiers(action.keys, async () => {
    await page.mouse.move(start.x, start.y);
    await page.mouse.down();
    for (const point of rest) await page.mouse.move(point.x, point.y);
    await page.mouse.up();
    });
    break;
    }
    case "scroll":
    // Wheel events scroll whatever is under the cursor, including inner scroll containers.
    await withModifiers(action.keys, async () => {
    await page.mouse.move(action.x!, action.y!);
    await page.mouse.wheel(action.scroll_x ?? 0, action.scroll_y ?? 0);
    });
    break;
    case "keypress":
    await page.keyboard.press((action.keys ?? []).map(toKey).join("+"));
    break;
    case "type":
    await page.keyboard.type(action.text ?? "");
    break;
    case "wait":
    await page.waitForTimeout(2000);
    break;
    case "screenshot":
    // The screenshot is captured after the batch, below.
    break;
    default:
    throw new Error(`Unsupported action: ${action.type}`);
    }
    }

    const MAX_TURNS = 20;
    // The extra pass reads the response to the last turn's actions before giving up.
    for (let turn = 0; turn <= MAX_TURNS; turn++) {
    const computerCall = response.output.find(
    (item): item is OpenAI.Responses.ResponseComputerToolCall => item.type === "computer_call"
    );

    if (!computerCall) {
    // Task complete. Print the model's answer.
    console.log(response.output_text);
    break;
    }

    // Safety checks need a human decision, so this example stops instead of continuing.
    if (computerCall.pending_safety_checks?.length) {
    console.log("Stopped on safety check:", computerCall.pending_safety_checks);
    break;
    }

    if (turn === MAX_TURNS) {
    console.log(`Stopped after ${MAX_TURNS} turns without a final answer; the task may be incomplete.`);
    break;
    }

    let failed = false;
    for (const action of computerCall.actions ?? []) {
    try {
    await runAction(action as Action);
    } catch (err) {
    // computer_call_output carries only a screenshot, so the model can't be told why an
    // action failed. Stop the run cleanly instead of letting it retry the action blind.
    console.error(`Action ${action.type} failed, stopping:`, err);
    failed = true;
    break;
    }
    }
    if (failed) break;

    const screenshot = await getScreenshot(page);

    response = await client.responses.create({
    model: MODEL,
    // Tools aren't stored with the response, so send them on every call.
    tools: [{ type: "computer" }],
    previous_response_id: response.id,
    input: [
    {
    type: "computer_call_output",
    call_id: computerCall.call_id,
    output: {
    type: "computer_screenshot",
    image_url: `data:image/png;base64,${screenshot}`,
    // "original" keeps full resolution, which OpenAI recommends for click accuracy.
    detail: "original",
    },
    },
    ],
    });
    }

    await browser.close();
  7. Run it

    npx tsx --env-file=.env agent.mts

    The .mts extension makes the file an ES module, which the top-level await calls need.

    The model searches Bing, reads the results, and prints a summary:

    Browserless is a cloud browser automation platform that runs headless Chrome for …

Supported actions​

ActionPropertiesDescription
clickx, y, button, keys (optional modifiers)Click at coordinates. button is left, right, wheel, back, or forward
double_clickx, y, keys (optional)Double-click at coordinates
movex, y, keys (optional)Move the cursor
dragpath (array of {x, y}), keys (optional)Drag along a path of points
scrollx, y, scroll_x, scroll_y, keys (optional)Scroll by a pixel delta at a position
keypresskeys[]Press a key or key combination
typetextType text
wait—Pause so the page can settle
screenshot—Request a fresh screenshot

Advanced configuration​

Without stealth mode​

If you don't need anti-detection and just want a managed cloud browser:

const browser = await chromium.connectOverCDP(
`wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}`
);

Residential proxies​

Route traffic through real residential IPs for additional anti-detection:

const browser = await chromium.connectOverCDP(
`wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}&proxy=residential&proxyCountry=us`
);

Regional endpoints​

Connect to the closest region for lower latency. See Connection URLs for all available endpoints.

Resources​

FAQ & Troubleshooting​

The request fails with a model or tool type error

You're probably still sending the preview request shape. computer-use-preview was shut down on July 23, 2026. Use a model that supports the computer tool (this guide uses gpt-6.1-sol), set the tool to { type: "computer" } without display_width, display_height, or environment, and drop truncation: "auto". Read computer_call.actions (an array) instead of computer_call.action. See OpenAI's migration guide.

The loop stops with "Stopped on safety check"

The model attached pending_safety_checks to a computer_call, which means the next action needs a human decision. The example stops rather than continuing on its own. In production, show the check to a user and only continue if they approve. See OpenAI's confirmation and consent guidance.

The screenshot fallback still times out

Increase the Playwright connection timeout, or check your network connection to Browserless:

const browser = await chromium.connectOverCDP(url, { timeout: 120000 });
The model returns key names Playwright doesn't recognize

The keyMap / key_map in the action loop covers the most common mismatches (CTRL → Control, CMD → Meta, etc.). If you encounter new ones, add them to the map. The Playwright keyboard API docs list all valid key names.

Clicks land in the wrong place

The model returns coordinates in the pixel space of the screenshot you sent. Keep the screenshot the same size as the viewport, and send it with detail: "original" so it keeps full resolution. If you downscale screenshots to save tokens, scale the model's coordinates back up before you click.

A site blocks the browser mid-task

Add residential proxies to the CDP connection URL, and confirm you're still using the /stealth route from Connect to Browserless. See Advanced configuration above for both examples.

Next steps​

Was this page helpful?