Webwright
Overview
Webwright is a code-as-action browser automation approach. Instead of clicking one button at a time, the agent writes full Playwright scripts, runs them, inspects screenshots and output, then iterates. The browser is disposable — the code and logs are the persistent artifact.
When to Use Webwright
Use Webwright when:
- Automating a web workflow (search, filter, form-fill, multi-step flow)
- Extracting structured data from a site that requires interaction
- Building a reusable, parameterized web automation CLI tool
- The user explicitly asks to "automate a website", "fill out a form", "search and extract", "build a web scraper", etc.
- A task needs screenshot evidence that specific filters/selections were applied
Do NOT use Webwright when:
- The target has a REST API — call the API directly instead
- The task is static HTML scraping with no interaction — use
httpx+beautifulsoup - The task only needs a web search — use Perplexity skill
- The task is browser testing (e.g., testing our own apps) — write standard Playwright tests
- The user just needs to read a webpage — use
curl/httpxto fetch it
Decision Flowchart
Need to interact with a website?
├─ Is there an API? → Use the API directly
├─ Just fetching static content? → httpx / curl
├─ Just searching the web? → Perplexity skill
├─ Testing our own app? → Standard Playwright tests
└─ Need to drive a real browser (forms, filters, clicks, multi-step)?
└─ → Webwright
Prerequisites (one-time)
pip install playwright && playwright install firefox
Firefox is preferred over Chromium — some sites (Akamai-protected, Cloudflare) reject Playwright Chromium with TLS/H2 fingerprinting but load cleanly under Firefox.
No API keys required. The agent itself provides visual verification (no external LLM calls).
Modes
- One-shot (default):
final_script.pysolves the task for the literal values provided. - CLI tool (
/webwright:craft):final_script.pyis a reusable, parameterized CLI withargparse. Use when the user says "make it reusable", "parameterize", "I want to run this again with different values".
Workspace Contract
Every Webwright run follows this structure:
WORKSPACE_DIR/ # e.g., outputs/<task_id>/
├── plan.md # Critical points checklist
├── screenshots/ # Exploration screenshots
├── final_script.py # The automation script
└── final_runs/
├── run_001/
│ ├── final_script.py
│ ├── final_script_log.txt
│ └── screenshots/
│ ├── final_execution_1_open_page.png
│ ├── final_execution_2_apply_filter.png
│ └── final_execution_3_results.png
└── run_002/ # If run_001 failed verification
└── ...
Rules:
- Work only inside
WORKSPACE_DIR - Each clean execution gets its own
final_runs/run_<id>/(incrementing integer) - Viewport is always
1280x1800. Never usefull_page=Truescreenshots - Browser is headless Firefox, launched fresh each run — no persistent session
Workflow
1. Plan
Parse the task into critical points (CPs) — every constraint, filter, sort, selection, or datum. Write to plan.md:
# Task
<task description>
# Critical Points
- [ ] CP1: <constraint>
- [ ] CP2: <constraint>
Each CP must be independently verifiable from a screenshot or log line.
2. Explore
Run scratch Playwright scripts to discover selectors and confirm controls exist. Use the browser launch skeleton from reference/playwright_patterns.md. Print ARIA snapshots, URLs, titles. Save exploration screenshots to WORKSPACE_DIR/screenshots/.
3. Author final_script.py
Place in final_runs/run_<id>/. Instrument it:
import asyncio
from pathlib import Path
from playwright.async_api import async_playwright
RUN_DIR = Path(__file__).parent
SCREENSHOTS = RUN_DIR / "screenshots"
SCREENSHOTS.mkdir(parents=True, exist_ok=True)
LOG = RUN_DIR / "final_script_log.txt"
LOG.write_text("") # reset
def log(step: int, msg: str) -> None:
line = f"step {step} action: {msg}\n"
LOG.open("a").write(line)
print(line, end="")
async def main():
async with async_playwright() as p:
browser = await p.firefox.launch(headless=True)
ctx = await browser.new_context(viewport={"width": 1280, "height": 1800})
page = await ctx.new_page()
await page.goto("<URL>", wait_until="domcontentloaded")
await page.screenshot(path=str(SCREENSHOTS / "final_execution_1_open.png"))
log(1, "opened start page")
# ... apply filters, click buttons, extract data ...
# One screenshot + log line per critical point
final_value = "<extracted result>"
LOG.open("a").write(f"\nFINAL_RESPONSE: {final_value}\n")
await browser.close()
asyncio.run(main())
4. Execute and Verify
Run the script. Then verify every CP against the screenshots and log:
- Identify screenshot/log evidence for each CP
- Read each PNG and confirm evidence is unambiguous
- Tick the CP only when evidence is concrete
- If any CP fails → diagnose, fix, re-run in
run_<id+1>/
5. Report
Report the final result to the user with evidence summary.
Hard Rules
- Use stable selectors (
get_by_role,aria-label) — never brittle CSS classes - If a site exposes a dedicated control for a requirement, you MUST use it (not search-box queries)
- Numeric/date/quantity constraints are exact — wider buckets are failures
- Ranking language ("cheapest", "best-selling") must use the site's actual sort/filter
- If a selected state becomes hidden (closed drawer, collapsed accordion), reopen it before capturing
- Prefer interactive form filling over deep-link URLs — deep links silently drop parameters
- Do not install extra packages —
playwright,httpx, etc. are already available
Reference Files
reference/playwright_patterns.md— browser launch skeleton, ARIA snapshot recipes, screenshot naming, targeting patternsreference/workflow.md— detailed plan → explore → final → verify walkthroughreference/cli_tool_mode.md— contract for parameterized CLI tool mode
Slash Commands
/webwright:run <task>— one-shot mode/webwright:craft <task>— reusable CLI tool mode