Spec reference
A spec is small and reviewable on purpose. Elements are addressed by role, label and text — never brittle CSS — which is also what lets Playhead write the captions for you.
A complete spec
playhead: 1
title: "Add a user in Acme Admin"
subtitle: "Admin in 30 seconds"
vars:
base: ${env.APP_URL:-http://localhost:4173}
app:
url: "{{base}}"
viewport: 1280x720
storageState: session.json # from `playhead login`
assertLoggedIn: 'role=link[name="Deals"]'
navigation: load # or domcontentloaded for ad-heavy pages
environment: { timezone: UTC, locale: en-US, fixedTime: "2026-01-01T12:00:00Z" }
network:
block: ["**analytics**"]
output:
kind: demo # walkthrough | demo | bug-repro | before-after | release | social
aspect: "16:9" # 16:9 | 9:16 | 1:1
audio: { narration: tts, provider: kokoro, sfx: true }
endCard: { title: "Acme Admin", subtitle: "acme.dev" }
masking:
- target: 'testid=env-banner' # redacted at capture; never reaches a frame
style: solid
setup: # runs BEFORE recording starts — never filmed
- click: 'role=button[name="Accept all"]'
scenes:
- id: add-user
title: "Add a user"
steps:
- click: 'role=button[name="Add user"]'
- type: { target: 'label=Full name', text: "Jane Doe" }
- type: { target: 'label=Temporary password', text: "s3cret!", mask: true }
- click: 'role=button[name="Create user"]'
caption: "Create it."
narration: "One click, and the account is live."
focus: 'text="User added"' # act→react: click here, camera lands THERE
- expect: { target: 'text="User added"', visible: true }
- wait: { for: 'text="Jane Doe"', state: visible }Locators
| Form | Matches |
|---|---|
role=button[name="Save"] | role plus accessible name — the most resilient form |
label=Email | a form control by its label |
text="User added" | visible text |
placeholder=Search | an input by placeholder |
testid=add-user | data-testid |
css=#legacy .thing | escape hatch — warned, because it breaks when styles change |
… >> nth=1 | pick one of several matches |
frame=#preview >> role=button[name="Save"] | pierce into an iframe (chainable) |
Steps
Actions: goto, click, dblclick, hover, type, press, select, scroll, expect, wait. Every step also accepts:
| Field | What it does |
|---|---|
caption | the card shown on screen |
narration | the spoken line — free of the caption’s size limits |
focus | target, wide, or a locator. Act here, land the camera there. |
shot | cut forces a new camera shot; continue keeps the current one |
timeout | per-step budget in ms |
mask | on type: the value is redacted before it enters the event log |
Prefer wait: { for: … } to a blind sleep. On a slow-hydrating app, a step that starts too early films a loading spinner — and verification will warn you that the content region of a frame was blank.
Composition
extends: ./base.yaml merges a base spec underneath this one, so a suite of videos can share an app block, masking rules and theme.