Browser Automation skill for Codex
Automate web chores without storing passwords. Codex scripts repetitive website chores with Playwright: the user signs in once and the session is reused, locators target roles and labels instead of brittle CSS, fixed sleeps become waits on real page state, and irreversible clicks sit behind a dry run and a confirm flag. Every failed run leaves a trace file you can replay step by step.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- browser-automation Installs to ~/.agents/skills/browser-automation/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install browser-automation The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project browser-automation Installs to .agents/skills/browser-automation/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/browser-automation.zipunzip -o browser-automation.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) browser-automation PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $browser-automation, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes Playwright scripts (Python or Node) that automate real web tasks - logging in with a saved session, filling forms, clicking through multi-step flows, downloading reports and exporting data from dashboards that have no API - using role-based locators, auto-waiting, storage state and traces for debugging. Use when asked to automate a website chore, download files from a portal on a schedule, fill a web form from a spreadsheet, or fix a flaky Playwright or Selenium script.
Use it when
- A website has no API and you repeat the same clicks every week
- Reports or files must be downloaded from a portal on a schedule
- A batch of records must be entered through a web form
- An existing browser script is flaky because of sleeps and brittle selectors
Use something else when
- You are writing end-to-end tests for your own application: use playwright-e2e
- The data is in static HTML or a JSON endpoint: use web-scraping
- You only need page screenshots or social cards: use screenshot-automation
How to check the result
Watch one headed dry run end to end, then run headless three times in a row and confirm each produces the expected file and exits zero.
The mistake it prevents
Hardcoding the user's password and fixed sleeps into the script, which leaks a credential and fails whenever the site is a little slower than usual.
A browser script is done when it runs headless unattended, waits on page state rather than on timers, never handles the user’s password in code, stops before any irreversible click unless the user has approved it, and leaves a trace you can open when it fails.
Decide first
| Situation | Choice |
|---|---|
| The site has an API or the page calls a JSON endpoint | Use HTTP instead ($web-scraping); faster and sturdier |
| Writing automated tests for your own app | $playwright-e2e (test runner, fixtures, CI) |
| Screenshots or social images at scale | $screenshot-automation |
| Chores on third-party sites: export, upload, form fill, download | This skill, Playwright library API |
| Existing Selenium script that works | Keep it; migrate only when flakiness justifies the rewrite |
Default to Python sync API for one-off chores and Node for projects already on Node. Read
AGENTS.md for the project’s runtime. Install needs network (pip install playwright then
playwright install chromium, or npm i -D playwright then npx playwright install chromium);
ask for approval if the Codex sandbox blocks it. Running the browser against a remote site
also needs network.
Workflow
- [ ] 1. Record the flow once: npx playwright codegen https://portal.example.com
- [ ] 2. Have the user log in by hand in a headed browser; save storage state
- [ ] 3. Rewrite codegen output with role/label locators and explicit assertions
- [ ] 4. Add a dry-run flag that stops before the final submit / send / delete
- [ ] 5. Run headed once with the user watching, then headless with tracing on
- [ ] 6. Schedule it (cron / launchd / CI) and alert on failure
Authentication without touching passwords
Never write a password into the script, a prompt or a log. Let the user sign in once in a headed browser, then reuse the session:
# save_session.py - run once, interactively, by the user
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
ctx = browser.new_context()
page = ctx.new_page()
page.goto("https://portal.example.com/login")
input("Log in in the browser window (including MFA), then press Enter here... ")
ctx.storage_state(path=".auth/portal.json")
browser.close()
Add .auth/ to .gitignore: the file holds live session cookies. When the session
expires, the script should detect the login page and exit with a clear message asking
the user to rerun save_session.py, not attempt to log in itself.
The task script
import argparse, sys
from datetime import date
from pathlib import Path
from playwright.sync_api import sync_playwright, expect, TimeoutError as PWTimeout
ap = argparse.ArgumentParser()
ap.add_argument("--headed", action="store_true")
ap.add_argument("--dry-run", action="store_true")
args = ap.parse_args()
out_dir = Path("downloads"); out_dir.mkdir(exist_ok=True)
with sync_playwright() as p:
browser = p.chromium.launch(headless=not args.headed)
ctx = browser.new_context(storage_state=".auth/portal.json", accept_downloads=True)
ctx.tracing.start(screenshots=True, snapshots=True, sources=True)
page = ctx.new_page()
page.set_default_timeout(15_000)
ok = False
try:
page.goto("https://portal.example.com/reports")
if "login" in page.url:
sys.exit("Session expired: run save_session.py again")
page.get_by_label("Period").select_option("last-month")
page.get_by_role("button", name="Generate").click()
expect(page.get_by_role("status")).to_have_text("Ready", timeout=120_000)
if args.dry_run:
print("dry run: stopping before download"); ok = True
else:
with page.expect_download() as dl:
page.get_by_role("link", name="Download CSV").click()
target = out_dir / f"report-{date.today():%Y-%m}.csv"
dl.value.save_as(target)
print(f"saved {target}"); ok = True
except PWTimeout as e:
print(f"timed out: {e}", file=sys.stderr)
finally:
ctx.tracing.stop(path=None if ok else "trace-failed.zip")
browser.close()
sys.exit(0 if ok else 1)
Open a failure with playwright show-trace trace-failed.zip: it shows DOM snapshots,
network and console for every step.
Locator rules
| Use | Instead of |
|---|---|
get_by_role("button", name="Save") | page.click("#btn-7f3a") |
get_by_label("Email"), get_by_placeholder(...) | XPath from codegen |
get_by_test_id(...) when the site has data-testid | nth-child chains |
locator("tr").filter(has_text="Invoice 42") | Index into a table by position |
expect(locator).to_be_visible() / to_have_text() | time.sleep(3) / wait_for_timeout |
Playwright actions auto-wait for the element to be visible, enabled and stable. Replace
every fixed sleep with a wait on the condition you actually need: page.wait_for_url,
expect(...), or page.wait_for_response(lambda r: "/api/report" in r.url).
Irreversible actions
Submitting payments, sending messages, deleting records, publishing posts and accepting terms are outward-facing. The script must:
- default to
--dry-runbehaviour until the user has seen one headed run end to end; - print exactly what will be submitted (recipient, amount, record ids) before the click;
- require an explicit
--confirmflag (or interactive “yes”) for the final action.
Never automate CAPTCHAs, bot checks or MFA prompts. If one appears, stop and report.
Bulk form filling from a file
- Read rows with
csv.DictReader; validate every row before opening the browser. - Process one row per page load; after submit, assert the confirmation text and record the row id plus confirmation number in a results CSV.
- On failure, screenshot (
page.screenshot(path=f"fail-{row_id}.png", full_page=True)) and continue; rerunning skips rows already in the results file. - Throttle (a second or two between rows) so the site does not rate-limit or lock the account.
Gotchas
- Codegen locators are a starting point: it often emits brittle CSS for unlabelled controls.
- New tabs and popups: wrap the click in
with page.expect_popup() as pop:and usepop.value. - Iframes need
page.frame_locator("iframe#payment")before locating inside them. - File inputs:
get_by_label("Upload").set_input_files("file.pdf"); no native dialog. - Headless runs may get a different layout or a bot wall. Set a realistic viewport
(
viewport={"width": 1440, "height": 900}) and locale; if the site still blocks automation, stop rather than adding stealth plugins. - Storage state does not include
sessionStorage; apps that keep tokens there need a fresh login each run. - macOS launchd and cron have no display: always run headless when scheduled.
Verification
- Run headed once with the user watching every step of the dry run.
- Run headless three times in a row; all succeed and produce identical output names.
- Expire the session (delete the state file copy) and confirm the script exits with the “run save_session.py” message instead of hanging.
- Confirm downloaded files are non-empty and parse (open the CSV, check the header row).
Report
**Automates:** monthly CSV export from portal.example.com/reports
**Run:** `python export_report.py` (add --headed to watch, --dry-run to stop before download)
**Auth:** saved session in .auth/portal.json (gitignored); rerun save_session.py when it expires
**Output:** downloads/report-YYYY-MM.csv
**On failure:** exit code 1 plus trace-failed.zip, open with `playwright show-trace` What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Browser Automation"
short_description: "Automate web chores without storing passwords"
default_prompt: "$browser-automation Write a script that logs into our vendor portal and downloads last month's invoices" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.