Playhead

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

FormMatches
role=button[name="Save"]role plus accessible name — the most resilient form
label=Emaila form control by its label
text="User added"visible text
placeholder=Searchan input by placeholder
testid=add-userdata-testid
css=#legacy .thingescape hatch — warned, because it breaks when styles change
… >> nth=1pick 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:

FieldWhat it does
captionthe card shown on screen
narrationthe spoken line — free of the caption’s size limits
focustarget, wide, or a locator. Act here, land the camera there.
shotcut forces a new camera shot; continue keeps the current one
timeoutper-step budget in ms
maskon 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.