| name | browser |
| description | Automate a web browser — use when asked to click buttons, fill forms, navigate multi-step flows, extract data from JS-rendered pages, log into a site, record user interactions, or test a web app in a real browser. Use for anything beyond simple article scraping. |
Browser Automation
Drive a real browser via Playwright to interact with pages — click, fill forms, extract data from SPAs, run multi-step flows, or record user interactions.
Use this skill for anything beyond simple article scraping: JS-heavy data extraction, form filling, multi-step auth flows, codegen recording, or E2E testing. For static article→markdown use scrape; for pure image capture use screenshot.
Step 1: Verify Playwright
Run npx playwright --version. If it fails, stop and tell the user: Playwright required — install with: npx playwright install chromium. Do not attempt workarounds.
Step 2: Parse the Request
- Target URL(s) — starting page (validate per Rules before launching)
- Actions — navigate, click, fill, extract, wait
- Data — what fields, what format (JSON usually)
- Auth — are credentials needed? Read from env, never hardcode
- Repeatability — one-off throwaway, or save script for reuse?
If the request is vague ("scrape this site"), clarify which fields, whether login is needed, and whether it'll be re-run.
Step 3: Choose the Right Mode
Mode A — Codegen (recording)
When the user wants to figure out selectors or hand off a repeatable flow:
npx playwright codegen {url}
Opens a browser. User interacts. Playwright prints the equivalent script. Best for:
- Complex pages where selectors aren't obvious
- Building reusable flows
- Teaching users how Playwright works
Mode B — Inline script (one-off)
Write the script to a file that reads args from process.argv, then pass values via shell variables:
cat > /tmp/devkit-browser-flow.mjs <<'EOF'
import { chromium } from 'playwright';
const url = process.argv[2];
if (!url) { console.error('usage: node devkit-browser-flow.mjs <url>'); process.exit(2); }
const browser = await chromium.launch();
let exitCode = 0;
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
// Example: extract items from a JS-rendered list
const data = await page.evaluate(() =>
Array.from(document.querySelectorAll('.item')).map(el => ({
title: el.querySelector('h3')?.textContent?.trim(),
link: el.querySelector('a')?.href,
}))
);
console.log(JSON.stringify(data, null, 2));
} catch (err) {
console.error('flow failed:', err.message);
exitCode = 1;
} finally {
try { await browser.close(); } catch (e) { /* suppress close errors so the real error propagates */ }
}
process.exit(exitCode);
EOF
URL=https://example.com node /tmp/devkit-browser-flow.mjs "$URL"
Mode C — Persistent test (for web apps)
If the user is building a web app and wants E2E tests:
npm init playwright@latest
npx playwright test
Then write test specs in tests/*.spec.ts.
Mode D — Form fill + auth flow
Same pattern as Mode B, plus page.fill / page.click for the login form. Credentials come from env, never from hardcoded strings or the command line:
cat > /tmp/devkit-browser-auth.mjs <<'EOF'
import { chromium } from 'playwright';
const loginUrl = process.argv[2];
const targetUrl = process.argv[3];
if (!loginUrl || !targetUrl) {
console.error('usage: node devkit-browser-auth.mjs <login_url> <target_url>');
process.exit(2);
}
const browser = await chromium.launch();
let exitCode = 0;
try {
const page = await browser.newPage();
await page.goto(loginUrl);
await page.fill('input[name="email"]', process.env.EMAIL);
await page.fill('input[name="password"]', process.env.PASSWORD);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
await page.goto(targetUrl);
const content = await page.content();
console.log(content);
} catch (err) {
console.error('auth flow failed:', err.message);
exitCode = 1;
} finally {
try { await browser.close(); } catch (e) { /* suppress close errors */ }
}
process.exit(exitCode);
EOF
EMAIL="$EMAIL" PASSWORD="$PASSWORD" node /tmp/devkit-browser-auth.mjs "$LOGIN_URL" "$TARGET_URL"
Step 4: Report
After running, report:
- What was done (pages visited, actions performed)
- What was extracted (or where it was saved)
- Any failures (timeouts, missing elements, auth errors)
- The script file path (if saved for reuse)
Rules
- URL validation — only accept
http:// and https://. Reject file://, ftp://, data:, and all other schemes. Reject private/reserved IPs: localhost, 127.0.0.1, 0.0.0.0, 169.254.x.x (cloud metadata — AWS/GCP/Azure), 10.x.x.x, 172.16-31.x.x, 192.168.x.x, [::1] — unless the user is explicitly testing localhost. Reject URLs containing @ (credential-in-URL attacks). On rejection: report the exact reason and stop — never silently skip.
- Credentials — never hardcode. Read from env or ask the user. Never log them.
- No raw interpolation — write scripts to files that read args from
process.argv, then pass values via shell variables. Never build via -e "..." with interpolated user input.
- Always close the browser safely — use
try/catch/finally with the close() call in its own try/catch, so a close-time error never masks the real error.
- Headless by default — only use
headless: false when user explicitly wants it (e.g., codegen, debugging).
- Respect the site — no CAPTCHA bypass, no hammering, no scraping the user doesn't own.
- Default timeout 30s — if slow, use
page.waitForSelector() rather than longer fixed waits, and explain why.
Troubleshooting
| Symptom | Fix |
|---|
"Executable doesn't exist" | npx playwright install chromium |
| Timeout exceeded | Report the timeout to the user first with the URL and selector — do not retry silently. Then investigate: wrong selector, page slow, content gated by JS that never fires. Use waitForSelector() with a sensible timeout rather than longer fixed waits. |
| Element not visible | await locator.scrollIntoViewIfNeeded() before action |
| Site blocks headless | Try user agent override; last resort: headless: false |
| Flaky data extraction | Use networkidle wait condition instead of domcontentloaded |