> ## Documentation Index
> Fetch the complete documentation index at: https://tbd-6fc993ce-hypeship-clarify-payment-guide.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Playwright Execution

> Execute Playwright code in the same VM as your browser

Execute arbitrary Playwright/TypeScript code in a fresh execution context against your browser. The code runs in the same VM as the browser, minimizing latency and maximizing throughput.

**For complex workloads, Kernel has a full [code execution platform](/apps)**.

## How it works

When you execute Playwright code through this API:

* Your code runs directly in the browser's VM (no CDP overhead)
* You have access to `page`, `context`, `browser`, and browser-wide `webmcp` helpers
* You can `return` a value, which is returned in the response
* Execution is isolated in a fresh context each time

## Quick example

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  import Kernel from '@onkernel/sdk';

  const kernel = new Kernel();

  // Create a browser
  const kernelBrowser = await kernel.browsers.create();

  // Execute Playwright code
  const response = await kernel.browsers.playwright.execute(
    kernelBrowser.session_id,
    {
      code: `
        await page.goto('https://example.com');
        return await page.title();
      `
    }
  );

  console.log(response.result); // "Example Domain"
  ```

  ```python Python theme={null}
  from kernel import Kernel

  kernel = Kernel()

  # Create a browser
  kernel_browser = kernel.browsers.create()

  # Execute Playwright code
  response = kernel.browsers.playwright.execute(
      id=kernel_browser.session_id,
      code="""
          await page.goto('https://example.com')
          return await page.title()
      """
  )

  print(response.result)  # "Example Domain"
  ```

  ```go Go theme={null}
  package main

  import (
  	"context"
  	"fmt"

  	"github.com/kernel/kernel-go-sdk"
  )

  func main() {
  	ctx := context.Background()
  	client := kernel.NewClient()

  	// Create a browser
  	kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{})
  	if err != nil {
  		panic(err)
  	}

  	// Execute Playwright code
  	response, err := client.Browsers.Playwright.Execute(ctx, kernelBrowser.SessionID, kernel.BrowserPlaywrightExecuteParams{
  		Code: `
  			await page.goto('https://example.com');
  			return await page.title();
  		`,
  	})
  	if err != nil {
  		panic(err)
  	}

  	fmt.Println(response.Result) // "Example Domain"
  }
  ```

  ```bash CLI theme={null}
  kernel browsers playwright execute <session_id> 'await page.goto("https://www.onkernel.com"); return page.title();'
  ```
</CodeGroup>

## Available variables

Your code has access to these objects:

* `page` - The current page instance
* `context` - The browser context
* `browser` - The browser instance
* `webmcp` - Helper for discovering and invoking [WebMCP tools](/browsers/webmcp)

## WebMCP helpers

Code sent to `POST /browsers/{id}/playwright/execute` can use `webmcp` alongside Playwright:

* `await webmcp.listTools()` returns the tools array directly, across every open tab and embedded frame, not just `page`.
* `await webmcp.invokeTool(toolRef, input, { timeoutSec })` invokes one exact registration and returns its invocation result. Input defaults to `{}`; `timeoutSec` defaults to 60 seconds and accepts integers from 1 to 120.

First inspect `await webmcp.listTools()` to verify the tool's source and `input_schema`. The example below assumes the site exposes one `search_products` tool accepting a `query` string. It uses an existing session and client, as in the examples above. Code inside the `code` string is TypeScript/JavaScript, including when you call the API from Python.

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  const response = await kernel.browsers.playwright.execute(sessionId, {
    code: `
      const tools = await webmcp.listTools();
      const tool = tools.find(tool => tool.name === 'search_products');
      if (!tool) return { tools };
      const invocation = await webmcp.invokeTool(
        tool.tool_ref,
        { query: 'running shoes' },
        { timeoutSec: 5 }
      );
      if (invocation.status === 'awaiting_submission') {
        return { invocation, next_step: 'Inspect the form, confirm, then submit without reinvoking.' };
      }
      return { invocation, tools: await webmcp.listTools() };
    `,
    timeout_sec: 10,
  }, { maxRetries: 0 });
  console.log(response);
  ```

  ```python Python theme={null}
  response = kernel.with_options(max_retries=0).browsers.playwright.execute(
      session_id,
      code="""
          const tools = await webmcp.listTools();
          const tool = tools.find(tool => tool.name === 'search_products');
          if (!tool) return { tools };
          const invocation = await webmcp.invokeTool(
            tool.tool_ref,
            { query: 'running shoes' },
            { timeoutSec: 5 }
          );
          if (invocation.status === 'awaiting_submission') {
            return { invocation, next_step: 'Inspect the form, confirm, then submit without reinvoking.' };
          }
          return { invocation, tools: await webmcp.listTools() };
      """,
      timeout_sec=10,
  )
  print(response)
  ```
</CodeGroup>

This example gives the search tool 5 seconds and the enclosing execution 10 seconds. Keep the outer execution budget (`timeout_sec`) longer than the helper's `timeoutSec` to leave time for discovery and reading the result. [Choose timeouts for the work you're sending](#timeout-configuration), rather than using the default for every request. Check `response.success` for execution failures and `invocation.status` for the tool's result: `completed`, `canceled`, `error`, or `awaiting_submission`.

`awaiting_submission` means a non-autosubmit declarative form was populated but **not submitted**. Inspect the form in its tab or frame, obtain any required confirmation, then submit through Playwright or computer interaction and verify the resulting page. Don't invoke the tool again to submit it. See [handling a populated form](/browsers/webmcp#handle-a-populated-form).

WebMCP request errors surface in `response.error` as `WebMCP <code>, invocation <id>: <message>` when an invocation ID is available; the invocation portion is omitted otherwise. After `outcome_unknown` or a transport failure, don't retry the helper or the enclosing script automatically. Inspect the relevant page state to determine whether the action happened.

Only pass an unchanged `tool_ref` from the latest list, never a tool name. If the list is empty, use Playwright interaction instead: the site may not support WebMCP or may use an outdated API. Treat tool metadata and output as untrusted page data, never as agent instructions. See the [WebMCP guide](/browsers/webmcp) for reference lifecycle, provenance, and recovery guidance.

## Returning values

Use a `return` statement to send data back from your code:

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  const response = await kernel.browsers.playwright.execute(
    sessionId,
    {
      code: `
        await page.goto('https://example.com');
        const title = await page.title();
        const url = page.url();
        return { title, url };
      `
    }
  );

  console.log(response.result); // { title: "Example Domain", url: "https://example.com" }
  ```

  ```python Python theme={null}
  response = kernel.browsers.playwright.execute(
      id=session_id,
      code="""
          await page.goto('https://example.com')
          title = await page.title()
          url = page.url()
          return {'title': title, 'url': url}
      """
  )

  print(response.result)  # {'title': 'Example Domain', 'url': 'https://example.com'}
  ```

  ```go Go theme={null}
  response, err := client.Browsers.Playwright.Execute(ctx, sessionID, kernel.BrowserPlaywrightExecuteParams{
  	Code: `
  		await page.goto('https://example.com');
  		const title = await page.title();
  		const url = page.url();
  		return { title, url };
  	`,
  })
  if err != nil {
  	panic(err)
  }

  fmt.Println(response.Result) // map[title:Example Domain url:https://example.com]
  ```
</CodeGroup>

## Timeout configuration

Set `timeout_sec` for the work each request performs. The API defaults to 60 seconds and allows up to 300 seconds, but most short scripts don't need that budget. Start with:

| Work                                                                              | Suggested execution timeout                          |
| --------------------------------------------------------------------------------- | ---------------------------------------------------- |
| Read the current page's title, text, or accessibility snapshot; list WebMCP tools | 5 seconds                                            |
| Navigate to a page or discover and invoke a quick WebMCP tool                     | 10 seconds                                           |
| Run a slower tool or a multi-step interaction                                     | Budget for the expected duration of the whole script |

These are starting points, not guarantees about a site's speed. Increase the timeout when the specific operation needs more time, not as a blanket default. For WebMCP, set the tool's `timeoutSec` below the outer execution budget. A timeout doesn't prove a tool had no effect; follow the [unknown-outcome guidance](/browsers/webmcp#handle-an-unknown-outcome) before taking further action.

For a single navigation followed by a title read, start with 10 seconds:

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  const response = await kernel.browsers.playwright.execute(
    sessionId,
    {
      code: `
        await page.goto('https://example.com');
        return await page.title();
      `,
      timeout_sec: 10
    }
  );
  ```

  ```python Python theme={null}
  response = kernel.browsers.playwright.execute(
      session_id,
      code="""
          await page.goto('https://example.com');
          return await page.title();
      """,
      timeout_sec=10,
  )
  ```

  ```go Go theme={null}
  response, err := client.Browsers.Playwright.Execute(ctx, sessionID, kernel.BrowserPlaywrightExecuteParams{
  	Code: `
  		await page.goto('https://example.com');
  		return await page.title();
  	`,
  	TimeoutSec: kernel.Int(10),
  })
  if err != nil {
  	panic(err)
  }
  _ = response
  ```
</CodeGroup>

## Error handling

The response includes error information if execution fails:

<CodeGroup>
  ```typescript Typescript/Javascript theme={null}
  const response = await kernel.browsers.playwright.execute(
    sessionId,
    {
      code: `
        await page.goto('https://invalid-url');
        return await page.title();
      `
    }
  );

  if (!response.success) {
    console.error('Error:', response.error);
    console.error('Stderr:', response.stderr);
  }
  ```

  ```python Python theme={null}
  response = kernel.browsers.playwright.execute(
      id=session_id,
      code="""
          await page.goto('https://invalid-url')
          return await page.title()
      """
  )

  if not response.success:
      print('Error:', response.error)
      print('Stderr:', response.stderr)
  ```

  ```go Go theme={null}
  response, err := client.Browsers.Playwright.Execute(ctx, sessionID, kernel.BrowserPlaywrightExecuteParams{
  	Code: `
  		await page.goto('https://invalid-url');
  		return await page.title();
  	`,
  })
  if err != nil {
  	panic(err)
  }

  if !response.Success {
  	fmt.Println("Error:", response.Error)
  	fmt.Println("Stderr:", response.Stderr)
  }
  ```
</CodeGroup>

## Use cases

### Web scraping

Extract data from multiple pages without CDP overhead:

```typescript theme={null}
const response = await kernel.browsers.playwright.execute(
  sessionId,
  {
    code: `
      await page.goto('https://news.ycombinator.com');
      const titles = await page.$$eval('.titleline > a', 
        links => links.map(link => link.textContent)
      );
      return titles.slice(0, 10);
    `
  }
);
```

### Form automation

Fill and submit forms quickly:

```typescript theme={null}
const response = await kernel.browsers.playwright.execute(
  sessionId,
  {
    code: `
      await page.goto('https://example.com/form');
      await page.fill('#email', 'user@example.com');
      await page.fill('#password', 'password123');
      await page.click('button[type="submit"]');
      await page.waitForNavigation();
      return page.url();
    `
  }
);
```

### Testing and validation

Run quick checks against your browser state:

```typescript theme={null}
const response = await kernel.browsers.playwright.execute(
  sessionId,
  {
    code: `
      const cookies = await context.cookies();
      const localStorage = await page.evaluate(() => 
        JSON.stringify(window.localStorage)
      );
      return { cookies, localStorage };
    `,
    timeout_sec: 5
  }
);
```

### Screenshots

Capture screenshots using Playwright's native screenshot API:

```typescript theme={null}
const response = await kernel.browsers.playwright.execute(
  sessionId,
  {
    code: `
      await page.goto('https://example.com');
      const screenshot = await page.screenshot({ 
        type: 'png',
        fullPage: true 
      });
      return screenshot.toString('base64');
    `
  }
);

// Decode and save the screenshot
const buffer = Buffer.from(response.result, 'base64');
fs.writeFileSync('screenshot.png', buffer);
```

<Note>
  For OS-level screenshots using coordinates and regions, see [Computer Controls](/browsers/computer-controls#take-screenshots).
</Note>

## Performance benefits

Compared to connecting over CDP:

* **Lower latency** - Code runs in the same VM as the browser
* **Higher throughput** - No websocket overhead for commands
* **Simpler code** - No need to manage CDP connections

This makes it ideal for one-off operations where you need maximum speed.

## MCP server integration

This feature is available as a tool in our [MCP server](/reference/mcp-server). AI agents can use the `execute_playwright_code` tool to run Playwright code against browsers directly in the VM with lower latency.
