# Automate a cloud browser with Playwright
URL: /cookbook/playwright

---
title: Automate a cloud browser with Playwright
description: Use Steel with Playwright in TypeScript for cloud browser automation.
---

<RecipeJsonLd slug="playwright" title={"Automate a cloud browser with Playwright"} description={"Use Steel with Playwright in TypeScript for cloud browser automation."} authors={[{"handle":"hussufo","name":"Hussien Hussien"}, {"handle":"junhsss","name":"Jun Ryu"}]} datePublished="2024-11-19" dateModified="2026-06-24" sourceUrl="https://github.com/steel-dev/steel-cookbook/tree/a3ea30d764f623478c0bb93f68956827e8185672/examples/playwright-ts" />

<Tabs items={['TypeScript', 'Python', 'Go']} groupId="lang" persist updateAnchor className="cookbook-concept-tabs">

<Tab id="typescript" className="cookbook-concept-tab">

<RecipeMeta href="https://github.com/steel-dev/steel-cookbook/tree/a3ea30d764f623478c0bb93f68956827e8185672/examples/playwright-ts" path="examples/playwright-ts" authors={[{"handle":"hussufo","name":"Hussien Hussien","avatar":"https://github.com/hussufo.png?size=40"}, {"handle":"junhsss","name":"Jun Ryu","avatar":"https://github.com/junhsss.png?size=40"}]} updated="2026-04-24" />

<RecipeQuickstart slug="playwright-ts" />

Playwright exposes `chromium.connectOverCDP()`, which attaches to any Chrome speaking the Chrome DevTools Protocol. Steel sessions expose one over a websocket. Connect them and your local code drives a remote browser with stealth, proxies, and a live viewer. No Chrome on your machine required.

```typescript
session = await client.sessions.create();

const browser = await chromium.connectOverCDP(
  `${session.websocketUrl}&apiKey=${STEEL_API_KEY}`,
);

const page = browser.contexts()[0].pages()[0];
```

A few lines. Steel returns a context with a page already open, so skip `newContext()` / `newPage()`. Everything after is plain Playwright: selectors, `page.evaluate`, `waitForSelector`, tracing.

## Run it

```bash
cd examples/playwright-ts
cp .env.example .env          # set STEEL_API_KEY
npm install
npm start
```

Get a key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys). The script prints a session viewer URL as it starts. Open it in another tab to watch the browser run live.

Your output varies. Structure looks like this:

```text
Creating Steel session...
Steel Session created!
View session at https://app.steel.dev/sessions/ab12cd34…

Connected to browser via Playwright
Navigating to Hacker News...

Top 5 Hacker News Stories:

1. Claude 4.7 Opus released today
   Link: https://news.ycombinator.com/item?id=43218921
   Points: 892

2. Show HN: A browser extension for reading on slow connections
   Link: https://github.com/user/project
   Points: 401

…

Releasing session...
Session released
Done!
```

A run costs a few cents of browser time. Steel bills per session-minute, so the `finally` block that calls `client.sessions.release()` isn't optional. Forgetting it keeps the browser running until the default 5-minute timeout.

## Make it yours

- **Swap the target.** Replace the `page.goto` URL and the `page.evaluate` body in `index.ts`. Session setup, auth, and cleanup stay the same.
- **Turn on stealth.** Uncomment `useProxy`, `solveCaptcha`, or `sessionTimeout` in the `sessions.create()` call for sites with anti-bot.
- **Persist login.** Reuse cookies and local storage across runs via [credentials](/cookbook/credentials).

## Related

[Python version](/cookbook/playwright) · [Playwright docs](https://playwright.dev)

</Tab>

<Tab id="python" className="cookbook-concept-tab">

<RecipeMeta href="https://github.com/steel-dev/steel-cookbook/tree/a3ea30d764f623478c0bb93f68956827e8185672/examples/playwright-py" path="examples/playwright-py" authors={[{"handle":"hussufo","name":"Hussien Hussien","avatar":"https://github.com/hussufo.png?size=40"}, {"handle":"junhsss","name":"Jun Ryu","avatar":"https://github.com/junhsss.png?size=40"}]} updated="2026-04-24" />

<RecipeQuickstart slug="playwright-py" />

Playwright's Python API ships a CDP attach point, `chromium.connect_over_cdp()`. Point it at the websocket URL a Steel session hands back and your `Page`, `Locator`, and `expect` calls drive a remote browser instead of a local one. No `playwright install`, no headful display, no Chrome on your machine.

The whole connection is three lines inside a `with sync_playwright()` block:

```python
session = client.sessions.create()

playwright = sync_playwright().start()
browser = playwright.chromium.connect_over_cdp(
    f"{session.websocket_url}&apiKey={STEEL_API_KEY}"
)

page = browser.contexts[0].new_page()
```

Two Python-specific details worth calling out. First, this starter uses the **sync API**. Easier to read top-to-bottom and fine for one script at a time; swap in `async_playwright` if you need to fan out concurrent pages. Second, Steel returns a session with a context already attached, so you reuse `browser.contexts[0]` rather than calling `new_context()`. Everything downstream is plain Playwright: `page.locator`, `page.goto(url, wait_until="networkidle")`, XPath selectors.

## Run it

```bash
cd examples/playwright-py
cp .env.example .env          # set STEEL_API_KEY
uv run main.py
```

Grab a key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys). As the script boots it prints a session viewer URL. Open it in a second tab to watch the browser click through Hacker News in real time.

Your output varies. Structure looks like this:

```text
Creating Steel session...
Steel Session created successfully!
You can view the session live at https://app.steel.dev/sessions/ab12cd34…

Connected to browser via Playwright
Navigating to Hacker News...

Top 5 Hacker News Stories:

1. Claude 4.7 Opus released today
   Link: https://news.ycombinator.com/item?id=43218921
   Points: 892

2. Show HN: A browser extension for reading on slow connections
   Link: https://github.com/user/project
   Points: 401

…

Releasing session...
Session released
Done!
```

One run costs a few cents of session time. Steel bills per session-minute, which is why `main()` wraps the script in `try / finally` and calls `client.sessions.release(session.id)` on exit. If you skip that, the session sits idle until the default 5-minute timeout burns through.

## Make it yours

- **Swap the target.** The scraping logic lives between the `Your Automations Go Here!` banner comments in `main.py`. Replace `page.goto` and the `story_rows` loop with your own selectors. Session setup, auth, and teardown stay the same.
- **Harden for anti-bot.** Uncomment `use_proxy`, `solve_captcha`, or `session_timeout` inside `client.sessions.create()` for sites that fingerprint or challenge headless traffic.
- **Go async.** If you need parallel pages, switch `from playwright.sync_api import sync_playwright` to `playwright.async_api` and rewrite `main()` as `async def`. The Steel connection call is identical, just awaited.
- **Persist login.** Carry cookies and local storage between runs with [credentials](/cookbook/credentials).

## Related

[TypeScript version](/cookbook/playwright) · [Playwright docs](https://playwright.dev/python)

</Tab>

<Tab id="go" className="cookbook-concept-tab">

<RecipeMeta href="https://github.com/steel-dev/steel-cookbook/tree/a3ea30d764f623478c0bb93f68956827e8185672/examples/playwright-go" path="examples/playwright-go" authors={[{"handle":"junhsss","name":"Jun Ryu","avatar":"https://github.com/junhsss.png?size=40"}]} updated="2026-06-24" />

<RecipeQuickstart slug="playwright-go" />

playwright-go exposes `pw.Chromium.ConnectOverCDP`, which attaches to any Chrome speaking the DevTools Protocol. A Steel session is exactly that: a remote Chrome reachable over a websocket. Hand the connect call the session's websocket URL with your key appended and the rest is ordinary Playwright, running against a browser in Steel's cloud with stealth, proxies, and a live viewer.

```go
cdpURL := fmt.Sprintf("%s&apiKey=%s", sess.WebsocketURL, apiKey)
browser, err := pw.Chromium.ConnectOverCDP(cdpURL)

page := browser.Contexts()[0].Pages()[0]
```

Steel returns a context with a page already open, so there is no `NewContext` / `NewPage` ceremony: reach into `Contexts()[0].Pages()[0]` and start driving. Everything after, `Goto`, `Evaluate`, `QuerySelectorAll`, `Screenshot`, is the same Playwright API the JavaScript and Python bindings expose.

## The driver, not the browser

playwright-go is not a pure-Go CDP client the way [chromedp](/cookbook/chromedp) and [Rod](/cookbook/rod) are. It drives the same Node-based Playwright driver the other language bindings use, so that driver has to exist on disk before `playwright.Run()` will start. The program installs it on the first line of `run`:

```go
if err := playwright.Install(&playwright.RunOptions{SkipInstallBrowsers: true}); err != nil {
    return fmt.Errorf("install driver: %w", err)
}
pw, err := playwright.Run()
```

`SkipInstallBrowsers: true` is the part that matters for Steel. A normal Playwright install also downloads Chromium, Firefox, and WebKit, hundreds of megabytes you never run, because the browser lives in Steel's cloud, not on your machine. The flag fetches the driver alone. Drop it and the first run still works, but it pulls three browser engines you will never launch. Calling `Install` in code is convenient for a one-file example; in a larger project you would run `go run github.com/playwright-community/playwright-go/cmd/playwright install` once at build time instead and let `Run` assume the driver is present.

The extraction reads the way it does in [chromedp](/cookbook/chromedp) for the same reason. `page.Evaluate` returns an `interface{}`, and a slice of structs does not cross that boundary cleanly, so the page-side script `JSON.stringify`s its result and the Go side `json.Unmarshal`s the string into a typed `[]story`. The `extractTopStories` constant holds that script: it pulls title, link, and points from the top five `tr.athing` rows.

## Run it

```bash
cd examples/playwright-go
cp .env.example .env          # set STEEL_API_KEY
go mod tidy
go run .
```

Get a key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys). The first `go run` downloads the Playwright driver, which takes a moment; later runs reuse it. The program prints a session viewer URL as it starts. Open it in another tab to watch the page load live, and it writes `hackernews.png` to the working directory on the way out.

Your output varies with the site. Structure looks like this:

```text
Creating Steel session...
Session created. Watch it live at https://app.steel.dev/sessions/ab12cd34
Connected to browser via playwright-go
Navigating to Hacker News...

Top 5 Hacker News Stories:

1. A compiler that fits in a tweet
   Link: https://example.com/tiny-compiler
   Points: 521

2. Show HN: I mapped every CDP command to a Go method
   Link: https://news.ycombinator.com/item?id=43990011
   Points: 274

Saved screenshot to hackernews.png
Releasing session...
```

A run costs a few cents of browser time. Steel bills per session-minute, so the deferred `client.Sessions.Release` is not optional. The `defer` sits right after the create call, so the session is released whether `run` returns clean or errors partway through. Drop it and the browser stays up until the default five-minute timeout, on your dime.

## Make it yours

- **Swap the target.** Change the `page.Goto` URL and the `extractTopStories` script. The JSON-string bridge works for any shape: define a matching Go struct and unmarshal. Session setup and cleanup stay identical.
- **Skip the JS.** `page.QuerySelectorAll("tr.athing")` returns `ElementHandle` values with `TextContent` and `GetAttribute`, if you would rather query node by node than evaluate a script. It is more round-trips and easier to debug one selector at a time.
- **Turn on stealth.** `SessionCreateParams` carries `UseProxy`, `SolveCaptcha`, and `Timeout` for sites with anti-bot defenses. Set them on the struct you pass to `Sessions.Create`.
- **Add steps.** Playwright auto-waits on actionability, so `page.Click`, `page.Fill`, and `page.WaitForSelector` are reliable without manual sleeps when you fill forms or paginate before extracting.

## Related

[chromedp](/cookbook/chromedp) and [Rod](/cookbook/rod) are the pure-Go options: both speak CDP directly with no Node driver to install, so compare their `main.go` against this one to decide whether the Playwright API is worth the extra dependency. The [TypeScript](/cookbook/playwright) and [Python](/cookbook/playwright) starters connect to Steel the same way through the official Playwright bindings. See the [playwright-go docs](https://pkg.go.dev/github.com/playwright-community/playwright-go) for the full page, locator, and screenshot API.

</Tab>

</Tabs>

## Related recipes

<RecipeGrid>
<RecipeCard slug="headless-chrome" title={"Automate a cloud browser with headless_chrome"} description={"Use Steel with headless_chrome, the synchronous Rust equivalent of Puppeteer, to connect over CDP and scrape quotes with element handles."} topics={['Browser automation']} languages={['Rust']} date="2026-06-24" />
<RecipeCard slug="chromedp" title={"Automate a cloud browser with chromedp"} description={"Use Steel with chromedp to connect over CDP, navigate to Hacker News, extract the top stories, and capture a screenshot."} topics={['Browser automation']} languages={['Go']} date="2026-06-23" />
<RecipeCard slug="rod" title={"Automate a cloud browser with Rod"} description={"Use Steel with Rod's fluent, chainable API to connect over CDP and scrape quotes.toscrape.com from a cloud browser."} topics={['Browser automation']} languages={['Go']} date="2026-06-23" />
</RecipeGrid>
