create-playwright
Skill67 starsChanged 2 months agoApache-2.0View on GitHub
Record a browser-session demo (MP4) using Playwright via a replay-first compile pipeline. A YAML spec drives the driver script, the recording, and the ElevenLabs voiceover — mirrors create-vhs for browser flows.
- Reads credentials
In this repository26 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- clawctl-lmworkSkill
- clawctlSkill
- clawctl-uatSkill
- create-vhsSkill
- itx-bug-newSkill
- itx-bug-updateSkill
- itx-executeSkill
- itx-issue-newSkill
- itx-issue-updateSkill
- itx-noteSkill
- itx-plan-createSkill
- itx-plan-scaffoldSkill
- itx-pr-statusSkill
- itx-releaseSkill
- itx-review-prSkill
- itx-triageSkill
- itx-verifySkill
- sync-upstreamSkill
- tddSkill
- blog-authorSkill
- daily-digestSkill
- docs-syncSkill
- issue-triageSkill
- release-watcherSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 1 is distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 67
MANDATORY: Before scaffolding any file, ask the user via AskUserQuestion.
- Very specific: line 114
Avoid wait: (raw sleeps) — Playwright's auto-wait via wait_for: is deterministic and faster.
- Very specific: line 116
Get explicit user approval on
scenes.yaml: before capturing HARs. - Very specific: line 144
Re-capture cadence: when the app's API shape or UI structure changes, when selectors used in scenes.yaml no longer resolve, or per release …
- Very specific: line 242
Cookie/auth secrets: NEVER commit storage_state.json.
- Distinctive: line 243
One demo = one app state: Don't try to demo "create then delete" in one HAR — the HAR captured at create-time doesn't have the delete …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Browser Demo Recording with Playwright — replay-first pipeline
Record browser demos using Playwright Python. Each demo lives in its own dated-slug folder docs/demos/YYYYMMDD-<slug>/ — the same parent as CLI demos produced by /create-vhs. The pipeline is replay-first: network responses are captured once via HAR on a live app, then a YAML spec drives driver generation, recording, and narration. Iteration is fast and timing is deterministic.
Pipeline
(one-time, slow)
scenes.yaml + live app --> outputs/*.har [HAR capture phase]
|
(instant) v
scenes.yaml + outputs/*.har --> pw_compile.py --> driver.py + cards/*.html + compiled.json
|
(~recording length) v
uv run driver.py --> recording.webm
|
(~5s, codec swap) v
ffmpeg -c:v libx264 ... --> recording.mp4
|
(~30s, cached per text) v
narrate.py compiled.json --> voice/*.mp3 + recording-narrated.mp4
|
(manual) v
upload to YouTubepw_compile.py, the driver, and narrate.py are all idempotent. The slow step is the initial HAR capture; everything downstream re-runs in seconds.
Output Layout
| Path | Committed? | Purpose |
docs/demos/lib/pw_compile.py | yes | scenes.yaml → driver.py + cards/*.html + compiled.json |
docs/demos/lib/pw_driver.py.j2 | yes | Jinja template for the generated driver |
docs/demos/lib/cards/title.html.j2 | yes | Title-card HTML template |
docs/demos/lib/cards/outro.html.j2 | yes | Outro-card HTML template |
docs/demos/lib/narrate.py | yes | Reused unchanged from /create-vhs |
docs/demos/lib/.env.example | yes | Reused unchanged — ElevenLabs credentials template |
docs/demos/lib/.env | no (gitignored) | Reused unchanged — real API key + voice/model IDs |
docs/demos/YYYYMMDD-<slug>/scenes.yaml | yes | Single source of truth — scene structure, actions, narration |
docs/demos/YYYYMMDD-<slug>/outputs/NN-<slug>.har | yes | Per-scene captured network responses (replayed by the driver) |
docs/demos/YYYYMMDD-<slug>/outputs/storage_state.json | no (gitignored) | Auth/cookie state captured via playwright codegen; contains tokens |
docs/demos/YYYYMMDD-<slug>/driver.py | yes | Generated by pw_compile.py; do not hand-edit |
docs/demos/YYYYMMDD-<slug>/cards/title.html | yes | Generated by pw_compile.py |
docs/demos/YYYYMMDD-<slug>/cards/outro.html | yes | Generated by pw_compile.py |
docs/demos/YYYYMMDD-<slug>/compiled.json | yes | Generated by pw_compile.py; absolute narration timestamps |
docs/demos/YYYYMMDD-<slug>/voice/*.mp3 | no (gitignored) | Per-beat TTS clips, cached by sha1(voice_id + text) |
docs/demos/YYYYMMDD-<slug>/recording.webm | no (gitignored) | Direct Playwright video output |
docs/demos/YYYYMMDD-<slug>/recording.mp4 | no (gitignored) | ffmpeg-transcoded H.264 |
docs/demos/YYYYMMDD-<slug>/recording-narrated.mp4 | no (gitignored) | Final video with voiceover muxed in |
storage_state.json MUST be gitignored — it can contain bearer tokens.
Arguments
<scenario-name>: Slug for the demo (e.g.gui-agent-create). Must match^[a-zA-Z0-9_-]+$. The skill prefixes today's date asYYYYMMDD→docs/demos/YYYYMMDD-<scenario-name>/.--description: Optional one-liner of what the demo should show.
Required Up-Front Questions
MANDATORY: Before scaffolding any file, ask the user via AskUserQuestion. Do not assume defaults.
- Target app & base URL — the URL the demo opens against (
base_url). - Auth model
- Anonymous — no login needed (most landing/marketing demos).
- Pre-authenticated session — operator runs
playwright codegen --save-storage outputs/storage_state.json <base_url>once; the driver loads it before scenes. - Live login flow — login is itself a scene (avoid for committed demos; secrets leak into HAR).
- Scene plan — ordered list of scene titles + actions. There is no fixed Scene 1 (unlike
/create-vhswhich anchors onclawctl --version); choose what anchors the recording. - HAR strategy per scene — for each scene whose network responses you want frozen, confirm a HAR file path. Scenes without
har:execute live. - Long-flow handling — for any scene whose interaction takes more than ~15 seconds on screen, confirm whether to split into sub-scenes or use the
speed_up:multiplier (post-record ffmpeg pass). - Any other unclear input — never invent prerequisites.
Universal Flow
Step 1 — Run the prereq validator
bash "$(git rev-parse --show-toplevel)/.claude/skills/create-playwright/scripts/check-prereqs.sh"Checks python3, uv, system ffmpeg, jq, a system Chrome binary (google-chrome / chromium), and that Playwright (>=1.40) can resolve plus its bundled ffmpeg is present at ~/.cache/ms-playwright/ffmpeg-*/. Stop immediately if any are missing.
Installation hints surfaced by the check:
playwright install ffmpegdownloads the bundled video-encoder ffmpeg (separate from system ffmpeg — Playwright needs its own forrecord_video_dir).- Ubuntu 26.04 isn't on Playwright's supported-OS matrix yet. Prepend
PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64to the install command; the 24.04 binaries run fine on 26.04. The prereq check detects this OS and emits the override automatically. - The driver intentionally launches system Chrome via
chromium.launch(channel="chrome")rather than Playwright's bundled Chromium. This sidesteps the OS-matrix problem for the browser binary itself and produces identical recordings. The only Playwright-bundled binary you still need isffmpeg.
Step 2 — Author scenes.yaml
Copy .claude/skills/create-playwright/templates/scenes.yaml.template to docs/demos/YYYYMMDD-<scenario-name>/scenes.yaml and fill in title, subtitle, base_url, intro/outro narration, and the scene list.
Action verbs supported by the driver (mapped 1:1 to Playwright Page methods):
| Action | YAML form | Playwright call |
| Navigate | goto: /path (relative to base_url) or full URL | page.goto() |
| Click | click: "<selector>" | page.locator(sel).click() |
| Fill | fill: { selector: "#x", text: "hello" } | page.locator(sel).fill(text) |
| Press a key | press: { selector: "#x", key: "Enter" } | page.locator(sel).press(key) |
| Hover | hover: "<selector>" | page.locator(sel).hover() |
| Wait for selector | wait_for: "<selector>" | page.locator(sel).wait_for() |
| Wait for URL | wait_for_url: "<glob>" | page.wait_for_url(pattern) |
| Sleep (last resort) | wait: <seconds> | page.wait_for_timeout(ms) |
| Highlight a region | highlight: "<selector>" | injects a CSS ring overlay |
| Scroll | scroll: { selector: "<sel>" } or scroll: { y: 400 } | scroll_into_view_if_needed() or mouse.wheel() |
| Escape hatch | eval: "page.locator(...).click()" | raw Python evaluated in driver scope |
Avoid wait: (raw sleeps) — Playwright's auto-wait via wait_for: is deterministic and faster.
Get explicit user approval on scenes.yaml before capturing HARs.
Step 3 — Capture HARs (one-time, on a live app)
For each scene that declares har:, run the capture pass. pw_compile.py --capture generates a stripped-down capture-only driver that emits HARs without recording video:
NAME=YYYYMMDD-<scenario-name>
mkdir -p docs/demos/$NAME/outputs
uv run docs/demos/lib/pw_compile.py docs/demos/$NAME --capture
uv run docs/demos/$NAME/driver_capture.pyThe capture pass opens a real Chromium window (headed by default), executes every scene's actions, and writes one HAR per scene to outputs/NN-<slug>.har.
Scrub secrets from HARs before committing:
- Bearer tokens in
Authorizationheaders - Session cookies
- PII in request/response bodies
A scripted jq pass works:
jq '(.log.entries[].request.headers[] | select(.name | ascii_downcase == "authorization")).value = "Bearer REDACTED"' \
docs/demos/$NAME/outputs/02-foo.har > /tmp/scrubbed.har && mv /tmp/scrubbed.har docs/demos/$NAME/outputs/02-foo.harRe-capture cadence: when the app's API shape or UI structure changes, when selectors used in scenes.yaml no longer resolve, or per release tag.
Step 4 — Compile
uv run docs/demos/lib/pw_compile.py docs/demos/YYYYMMDD-<scenario-name>uv resolves PEP 723 deps (pyyaml, jinja2) on first run. Outputs:
driver.py— self-contained Playwright Python script. Header banner says "GENERATED — DO NOT EDIT".cards/title.html,cards/outro.html— rendered from the YAML title/subtitle/outro.compiled.json— flat list of every narration beat with its absolute timestamp in the rendered recording.
The script prints the recording duration estimate and a per-beat timeline. Verify these match what you expect before recording.
Step 5 — Record
uv run docs/demos/YYYYMMDD-<scenario-name>/driver.pyThe driver:
- Opens Chromium at the configured viewport (
tape.width × tape.height). - Optionally loads
outputs/storage_state.jsonfor auth. - Plays the title card HTML for
intro.title_card.hold_seconds. - For each scene: applies
route_from_har(ifhar:set), executes actions, holds forscreen_seconds. - Plays the outro card.
- Closes the context; Playwright finalizes
recording.webm.
Then transcode to MP4:
ffmpeg -y -i docs/demos/YYYYMMDD-<scenario-name>/recording.webm \
-c:v libx264 -pix_fmt yuv420p -crf 20 -preset slow \
docs/demos/YYYYMMDD-<scenario-name>/recording.mp4The driver runs this transcode automatically at the end of its run; the standalone command above is for manual re-runs.
Step 6 — Layer narration (ElevenLabs)
Identical to /create-vhs — narrate.py is reused unchanged:
uv run docs/demos/lib/narrate.py docs/demos/YYYYMMDD-<scenario-name>
[--force] # regenerate ALL clips even if cached
[--skip-mux] # generate audio only; skip ffmpeg
[--input recording.mp4 --output recording-narrated.mp4]The cache key (sha1(voice_id + text)) and timeline contract (compiled.json) are unchanged.
Step 7 — Iterate
| Change | Re-run |
| Narration text only | pw_compile.py + narrate.py (~30s; driver not needed) |
| Scene order / new scene / new action | pw_compile.py + driver + narrate.py |
| App API shape changed | re-do Step 3 HAR capture + Step 4+ |
| Visual / card text | pw_compile.py + driver + narrate.py |
Step 8 — Upload (out of skill scope)
You upload recording-narrated.mp4 to YouTube. The skill does not push.
Configuration
narrate.py configuration is unchanged from /create-vhs (.env-based, ElevenLabs).
scenes.yaml tape: block:
| Key | Default | Notes |
framerate | 30 | Driver does not enforce — informational; ffmpeg transcode uses source fps |
width | 1400 | Viewport width; Playwright records WebM at this width |
height | 900 | Viewport height |
device_scale | 1 | 2 for HiDPI — doubles output dimensions |
base_url | — | Required when scenes use relative goto: paths |
color_scheme | light | light / dark / no-preference |
reduced_motion | reduce | Recommend reduce for deterministic UI animations |
Cards as HTML
Title and outro cards are full-screen HTML rendered in the same Playwright context. The Jinja templates at docs/demos/lib/cards/{title,outro}.html.j2 receive:
title(string)subtitle(string)outro.line_1,outro.line_2(strings, outro only)theme— currently fixed to "dark"; matches Catppuccin Mocha palette used in CLI demos for brand parity.
You can hand-edit the rendered cards/*.html for one-off visual tweaks, but a recompile overwrites them. Prefer editing the .j2 template or YAML fields.
Production Quality
reduced_motion: reducekills CSS transitions/animations the browser would render non-deterministically.- HAR replay catches 99% of HTTP traffic but does NOT capture WebSocket frames (Playwright limitation as of writing). Apps with WS-heavy UI (live dashboards, chat) should record live or mock WS via
page.route+ custom handler — call this out at scene authoring time. - Custom webfonts may fall back to default fonts in headless mode. Run headed if pixel-perfect type matters, or pre-load fonts via
page.add_style_tag(). - Cookie/auth secrets: NEVER commit
storage_state.json. NEVER let live-login HAR ship — the Authorization header will be in the captured response chain. - One demo = one app state. Don't try to demo "create then delete" in one HAR — the HAR captured at create-time doesn't have the delete responses. Split into two scenes with separate HARs, or run that scene live.
- Viewport ≥ 1400×900 for desktop apps; smaller viewports require scenes designed for them (
tape.width: 1024etc.).
Troubleshooting
| Problem | Solution |
playwright: command not found or "Chromium not installed" | The driver uses system Chrome (channel="chrome"); install Google Chrome via your OS, not via playwright install chromium |
Executable doesn't exist at ...ffmpeg-linux | uv run --with 'playwright>=1.40' -- playwright install ffmpeg — prepend PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64 on Ubuntu 26.04 |
ERROR: Playwright does not support ... on ubuntu26.04-x64 | OS too new for Playwright's matrix. Use PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64 for the install command. Binaries are forward-compatible |
| HAR replay returns 404 for an asset | HAR was captured against a different URL; re-do Step 3. Or set route_from_har(not_found="fallback") (default) and live-fetch missing assets |
| Locator timeout in a scene | The selector changed; update scenes.yaml. Use playwright codegen to find a stable selector |
| Recording is too long | Lower screen_seconds per scene; remove wait: calls in favor of wait_for: |
| Narration drifts from on-screen action | All beats are absolute timestamps from pw_compile.py. If drift is real, edit screen_seconds / offset_seconds in scenes.yaml + recompile |
| WebM → MP4 transcode artifacts | Increase CRF quality (-crf 18) or change preset (-preset veryslow) |
| Auth fails in record run | storage_state.json expired; re-capture via playwright codegen --save-storage |
| WS-driven UI updates missing on replay | WebSocket isn't in HAR; record that scene live, or mock via page.route |
File Organization
docs/demos/
├── lib/ # committed — shared, all demos
│ ├── cards.sh # used by create-vhs CLI demos
│ ├── cards/ # used by create-playwright (this skill)
│ │ ├── title.html.j2
│ │ └── outro.html.j2
│ ├── compile.py # create-vhs compiler
│ ├── pw_compile.py # THIS skill — scenes.yaml -> driver.py + cards/*.html + compiled.json (PEP 723; uv run)
│ ├── pw_driver.py.j2 # THIS skill — Jinja template for the generated driver
│ ├── narrate.py # REUSED — compiled.json -> voice/*.mp3 + recording-narrated.mp4
│ ├── .env.example # REUSED — ElevenLabs credentials template
│ └── .env # GITIGNORED — real ELEVENLABS_API_KEY etc.
└── YYYYMMDD-<slug>/ # one folder per demo (CLI or browser)
├── scenes.yaml # committed — SOURCE OF TRUTH
├── outputs/ # committed except storage_state.json
│ ├── 01-landing.har
│ ├── 02-create-agent.har
│ └── storage_state.json # GITIGNORED — contains auth tokens
├── driver.py # committed (generated) — Playwright script
├── cards/
│ ├── title.html # committed (generated)
│ └── outro.html # committed (generated)
├── compiled.json # committed (generated) — narration timeline
├── voice/ # GITIGNORED — per-beat TTS clips
│ └── scene-NN-beat-MM-<sha8>.mp3
├── recording.webm # GITIGNORED — Playwright direct output
├── recording.mp4 # GITIGNORED — ffmpeg-transcoded H.264
└── recording-narrated.mp4 # GITIGNORED — final with voiceoverSymmetry with /create-vhs
| Concern | /create-vhs (CLI) | /create-playwright (Browser) |
| Spec | scenes.yaml | scenes.yaml (same key shapes, different scene body) |
| Action unit | command: + output_file: | actions: (list) + har: |
| Frozen capture | outputs/*.txt (stdout) | outputs/*.har (network) |
| Render engine | vhs tape.tape | uv run driver.py (Playwright) |
| Compiler | compile.py → tape.tape | pw_compile.py → driver.py |
| Cards | ANSI in lib/cards.sh | HTML in lib/cards/*.html.j2 |
| Narration | narrate.py (shared) | narrate.py (shared) |
| Final mux | ffmpeg adelay+amix (shared) | ffmpeg adelay+amix (shared) |
References
External best-practice sources consulted:
- Playwright Python docs — video recording
- Playwright Python docs — network replay via
route_from_har - Playwright Python docs — codegen + storage state
- HAR 1.2 spec
- ElevenLabs Python SDK
- PEP 723 — inline script metadata
Prompt Logging
REQUIRED: Append prompt log to .itx/<N>/03_DEMO.md if recording as part of an issue.
See AGENTS.md for format specification.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

