Automate a cloud browser with Playwright
Use Steel with Playwright in TypeScript for cloud browser automation.
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-tscp .env.example .env # set STEEL_API_KEYnpm installnpm 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 PlaywrightNavigating to Hacker News...Top 5 Hacker News Stories:1. Claude 4.7 Opus released todayLink: https://news.ycombinator.com/item?id=43218921Points: 8922. Show HN: A browser extension for reading on slow connectionsLink: https://github.com/user/projectPoints: 401…Releasing session...Session releasedDone!
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.gotoURL and thepage.evaluatebody inindex.ts. Session setup, auth, and cleanup stay the same. - Turn on stealth. Uncomment
useProxy,solveCaptcha, orsessionTimeoutin thesessions.create()call for sites with anti-bot. - Persist login. Reuse cookies and local storage across runs via credentials.
Related
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-pycp .env.example .env # set STEEL_API_KEYuv 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 PlaywrightNavigating to Hacker News...Top 5 Hacker News Stories:1. Claude 4.7 Opus released todayLink: https://news.ycombinator.com/item?id=43218921Points: 8922. Show HN: A browser extension for reading on slow connectionsLink: https://github.com/user/projectPoints: 401…Releasing session...Session releasedDone!
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 inmain.py. Replacepage.gotoand thestory_rowsloop with your own selectors. Session setup, auth, and teardown stay the same. - Harden for anti-bot. Uncomment
use_proxy,solve_captcha, orsession_timeoutinsideclient.sessions.create()for sites that fingerprint or challenge headless traffic. - Go async. If you need parallel pages, switch
from playwright.sync_api import sync_playwrighttoplaywright.async_apiand rewritemain()asasync def. The Steel connection call is identical, just awaited. - Persist login. Carry cookies and local storage between runs with credentials.
Related
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-gocp .env.example .env # set STEEL_API_KEYgo mod tidygo 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/ab12cd34Connected to browser via playwright-goNavigating to Hacker News...Top 5 Hacker News Stories:1. A compiler that fits in a tweetLink: https://example.com/tiny-compilerPoints: 5212. Show HN: I mapped every CDP command to a Go methodLink: https://news.ycombinator.com/item?id=43990011Points: 274Saved screenshot to hackernews.pngReleasing 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.GotoURL and theextractTopStoriesscript. 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")returnsElementHandlevalues withTextContentandGetAttribute, 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.
SessionCreateParamscarriesUseProxy,SolveCaptcha, andTimeoutfor sites with anti-bot defenses. Set them on the struct you pass toSessions.Create. - Add steps. Playwright auto-waits on actionability, so
page.Click,page.Fill, andpage.WaitForSelectorare reliable without manual sleeps when you fill forms or paginate before extracting.
Related
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.
Related recipes
Run a Steel browser job with Trigger.dev
Queue a Trigger.dev task that creates a Steel session, drives Playwright over CDP, saves artifacts, and releases the browser in cleanup.
Run a durable browser workflow with Temporal
Build a Temporal TypeScript Workflow that schedules retryable Steel browser Activities to capture page summaries, screenshots, and Markdown artifacts.
Automate a cloud browser with headless_chrome
Use Steel with headless_chrome, the synchronous Rust equivalent of Puppeteer, to connect over CDP and scrape quotes with element handles.