agentleFS
ric03uecric03uec/clawrium/.claude/skills/create-playwright/SKILL.md

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

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
  1. Very specific:

    MANDATORY: Before scaffolding any file, ask the user via AskUserQuestion.

    line 67
  2. Very specific:

    Avoid wait: (raw sleeps) — Playwright's auto-wait via wait_for: is deterministic and faster.

    line 114
  3. Very specific:

    Get explicit user approval on scenes.yaml: before capturing HARs.

    line 116
  4. Very specific:

    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 …

    line 144
  5. Very specific:

    Cookie/auth secrets: NEVER commit storage_state.json.

    line 242
  6. Distinctive:

    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 …

    line 243

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 YouTube

pw_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

PathCommitted?Purpose
docs/demos/lib/pw_compile.pyyesscenes.yaml → driver.py + cards/*.html + compiled.json
docs/demos/lib/pw_driver.py.j2yesJinja template for the generated driver
docs/demos/lib/cards/title.html.j2yesTitle-card HTML template
docs/demos/lib/cards/outro.html.j2yesOutro-card HTML template
docs/demos/lib/narrate.pyyesReused unchanged from /create-vhs
docs/demos/lib/.env.exampleyesReused unchanged — ElevenLabs credentials template
docs/demos/lib/.envno (gitignored)Reused unchanged — real API key + voice/model IDs
docs/demos/YYYYMMDD-<slug>/scenes.yamlyesSingle source of truth — scene structure, actions, narration
docs/demos/YYYYMMDD-<slug>/outputs/NN-<slug>.haryesPer-scene captured network responses (replayed by the driver)
docs/demos/YYYYMMDD-<slug>/outputs/storage_state.jsonno (gitignored)Auth/cookie state captured via playwright codegen; contains tokens
docs/demos/YYYYMMDD-<slug>/driver.pyyesGenerated by pw_compile.py; do not hand-edit
docs/demos/YYYYMMDD-<slug>/cards/title.htmlyesGenerated by pw_compile.py
docs/demos/YYYYMMDD-<slug>/cards/outro.htmlyesGenerated by pw_compile.py
docs/demos/YYYYMMDD-<slug>/compiled.jsonyesGenerated by pw_compile.py; absolute narration timestamps
docs/demos/YYYYMMDD-<slug>/voice/*.mp3no (gitignored)Per-beat TTS clips, cached by sha1(voice_id + text)
docs/demos/YYYYMMDD-<slug>/recording.webmno (gitignored)Direct Playwright video output
docs/demos/YYYYMMDD-<slug>/recording.mp4no (gitignored)ffmpeg-transcoded H.264
docs/demos/YYYYMMDD-<slug>/recording-narrated.mp4no (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 as YYYYMMDD → 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.

  1. Target app & base URL — the URL the demo opens against (base_url).
  2. 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).
  3. Scene plan — ordered list of scene titles + actions. There is no fixed Scene 1 (unlike /create-vhs which anchors on clawctl --version); choose what anchors the recording.
  4. HAR strategy per scene — for each scene whose network responses you want frozen, confirm a HAR file path. Scenes without har: execute live.
  5. 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).
  6. 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 ffmpeg downloads the bundled video-encoder ffmpeg (separate from system ffmpeg — Playwright needs its own for record_video_dir).
  • Ubuntu 26.04 isn't on Playwright's supported-OS matrix yet. Prepend PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64 to 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 is ffmpeg.

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):

ActionYAML formPlaywright call
Navigategoto: /path (relative to base_url) or full URLpage.goto()
Clickclick: "<selector>"page.locator(sel).click()
Fillfill: { selector: "#x", text: "hello" }page.locator(sel).fill(text)
Press a keypress: { selector: "#x", key: "Enter" }page.locator(sel).press(key)
Hoverhover: "<selector>"page.locator(sel).hover()
Wait for selectorwait_for: "<selector>"page.locator(sel).wait_for()
Wait for URLwait_for_url: "<glob>"page.wait_for_url(pattern)
Sleep (last resort)wait: <seconds>page.wait_for_timeout(ms)
Highlight a regionhighlight: "<selector>"injects a CSS ring overlay
Scrollscroll: { selector: "<sel>" } or scroll: { y: 400 }scroll_into_view_if_needed() or mouse.wheel()
Escape hatcheval: "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.py

The 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 Authorization headers
  • 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.har

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 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.py

The driver:

  1. Opens Chromium at the configured viewport (tape.width × tape.height).
  2. Optionally loads outputs/storage_state.json for auth.
  3. Plays the title card HTML for intro.title_card.hold_seconds.
  4. For each scene: applies route_from_har (if har: set), executes actions, holds for screen_seconds.
  5. Plays the outro card.
  6. 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.mp4

The 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

ChangeRe-run
Narration text onlypw_compile.py + narrate.py (~30s; driver not needed)
Scene order / new scene / new actionpw_compile.py + driver + narrate.py
App API shape changedre-do Step 3 HAR capture + Step 4+
Visual / card textpw_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:

KeyDefaultNotes
framerate30Driver does not enforce — informational; ffmpeg transcode uses source fps
width1400Viewport width; Playwright records WebM at this width
height900Viewport height
device_scale12 for HiDPI — doubles output dimensions
base_url—Required when scenes use relative goto: paths
color_schemelightlight / dark / no-preference
reduced_motionreduceRecommend 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: reduce kills 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: 1024 etc.).

Troubleshooting

ProblemSolution
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-linuxuv 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-x64OS 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 assetHAR 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 sceneThe selector changed; update scenes.yaml. Use playwright codegen to find a stable selector
Recording is too longLower screen_seconds per scene; remove wait: calls in favor of wait_for:
Narration drifts from on-screen actionAll beats are absolute timestamps from pw_compile.py. If drift is real, edit screen_seconds / offset_seconds in scenes.yaml + recompile
WebM → MP4 transcode artifactsIncrease CRF quality (-crf 18) or change preset (-preset veryslow)
Auth fails in record runstorage_state.json expired; re-capture via playwright codegen --save-storage
WS-driven UI updates missing on replayWebSocket 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 voiceover

Symmetry with /create-vhs

Concern/create-vhs (CLI)/create-playwright (Browser)
Specscenes.yamlscenes.yaml (same key shapes, different scene body)
Action unitcommand: + output_file:actions: (list) + har:
Frozen captureoutputs/*.txt (stdout)outputs/*.har (network)
Render enginevhs tape.tapeuv run driver.py (Playwright)
Compilercompile.py → tape.tapepw_compile.py → driver.py
CardsANSI in lib/cards.shHTML in lib/cards/*.html.j2
Narrationnarrate.py (shared)narrate.py (shared)
Final muxffmpeg adelay+amix (shared)ffmpeg adelay+amix (shared)

References

External best-practice sources consulted:

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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.