Automate a cloud browser with Playwright

Use Steel with Playwright in TypeScript for cloud browser automation.

examples/playwright-ts
Contributors: , Updated
Terminal

Scaffolds a starter project locally. Requires the Steel CLI.

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.

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

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. 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:

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.

Python version · Playwright docs

examples/playwright-py
Contributors: , Updated
Terminal

Scaffolds a starter project locally. Requires the Steel CLI.

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:

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

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. 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:

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.

TypeScript version · Playwright docs

examples/playwright-go
Contributors: Updated
Terminal

Scaffolds a starter project locally. Requires the Steel CLI.

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.

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 and 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:

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 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.stringifys its result and the Go side json.Unmarshals 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

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. 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:

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.

chromedp and 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 and Python starters connect to Steel the same way through the official Playwright bindings. See the playwright-go docs for the full page, locator, and screenshot API.