examples & evidence

Real renders, with the receipts they came with.

Every video on this page was produced by the CLI on an ordinary laptop, and every number beside it is read straight out of the artifacts that render wrote — compose-manifest.json and verify/verdict.json. Including the runs that did not pass, because that is the part of the product that actually matters.

Login, sort, product page, cart, checkout — on a store nobody here controls

Real website, no setup1:25narrated publishable

Sauce Demo end to end: a real login, a native sort control, a product detail page, two add-to-carts, and a confirmed order. Filmed live on saucedemo.com with no fixtures.

step map23 steps
output
1:251920×1080 · 30fps · 5.5 MB
capture
169 frames65.0s of source · viewport 1280×800
steps
2325 camera keyframes · 8 click ripples
narration
23 lineskokoro · voice heart · −16 LUFS
verdict
publishable13/13 actions pixel-verified
video sha256
0b4ed39080fc7658770aac9a…playhead 0.4.0 · win32-x64
passencode — h264 1920x1080 @ 30fps, 84.9s
passtarget-in-frame — 17/17 targets fully framed, min rendered height 21px
passcursor-on-target — 8/8 clicks land on target; sprite verified on film at 2/2 sampled beat(s)
passaction-effect — 13/13 actions visibly effective
passoutput-pixels — 3 sampled output regions match their planned source
warnfrozen-blank — frames live, but 17 sampled frame(s) have a blank content region (mid-hydration footage?)
passcaptions — 23 captions fit at full size
passpacing — 23 steps paced ≥1.2s, total 84.9s
passaudio — 23 lines audible in their windows, aac, -16.1 LUFS
verdict: publishableexit 0
The spec that produced itsaucedemo-checkout.yaml
# Homepage flagship: Playhead capabilities on Sauce Demo.
# One cart item + taller viewport keep Finish framed (no bottom-edge ghost click).
playhead: 1
title: Product video from a spec, on a real store
subtitle: Playhead capabilities · filmed live on saucedemo.com
app:
  url: https://www.saucedemo.com/
  viewport: 1280x800
  navigation: domcontentloaded
  environment: { timezone: UTC, locale: en-US }
  settle: { idleMs: 400, capMs: 5000 }
output:
  kind: walkthrough
  aspect: "16:9"
  pacing: brisk
  dwellScale: 0.65
  camera: normal
  audio:
    narration: tts
    provider: kokoro
    voice: heart
    rate: 118
    sfx: true
  endCard:
    title: Filmed by Playhead
    subtitle: Spec in · verified MP4 out
theme:
  accent: "#6d7cff"
  captions: bottom

scenes:
  - id: signin
    title: Real app, real login
    steps:
      - expect:
          target: 'role=button[name="Login"]'
          visible: true
        caption: "YAML in · verified video out"
        narration: "A YAML spec becomes a finished product video on a real public store."
        focus: wide

      - type:
          target: 'placeholder=Username'
          text: standard_user
        caption: "Grounded locators"
        narration: "Locators are grounded in the live DOM. Keystrokes are real."
        focus: target

      - type:
          target: 'placeholder=Password'
          text: secret_sauce
        caption: "Real validation"
        narration: "The app validates the input — nothing is painted onto frames."
        focus: target

      - click: 'role=button[name="Login"]'
        caption: "Real navigation"
        narration: "A real browser navigation, not a faked slide."
        focus: target
        shot: cut

      - wait:
          for: 'text="Products"'
          state: visible
        timeout: 20000
        caption: "Structured wait"
        narration: "Wait on the screen you need — never a guessed sleep."
        focus: wide

  - id: browse
    title: Native controls
    steps:
      - select:
          target: 'testid=product-sort-container'
          value: Price (high to low)
        caption: "Native select"
        narration: "Playhead drives the real native select behind the styled menu."
        focus: target

      - click: 'text="Sauce Labs Backpack"'
        caption: "Visible effect required"
        narration: "Verification requires a visible change, not just a dispatched click."
        focus: target
        shot: cut

  - id: product
    title: Capture once
    steps:
      - wait:
          for: 'testid=add-to-cart'
          state: visible
        timeout: 20000
        caption: "Cross-page capture"
        narration: "Capture is the only stage that touches the product."
        focus: wide

      - click: 'testid=add-to-cart'
        caption: "Add to cart"
        narration: "Re-compose later for new captions or voice — never re-open the store."
        focus: target

      - click: 'testid=shopping-cart-link'
        caption: "Test id when unnamed"
        narration: "No accessible name? Use the test id from playhead explore."
        focus: target
        shot: cut

  - id: cart
    title: Cart
    steps:
      - wait:
          for: 'testid=checkout'
          state: visible
        timeout: 20000
        caption: "App state on film"
        narration: "Cart state is the app's — not an overlay."
        focus: wide

      - click: 'testid=checkout'
        caption: "Checkout"
        narration: "One capture, many cuts — docs, PR, or release notes."
        focus: target
        shot: cut

  - id: details
    title: Forms
    steps:
      - wait:
          for: 'testid=firstName'
          state: visible
        timeout: 20000
        caption: "A real form"
        narration: "The app's validation still applies."
        focus: wide

      - type:
          target: 'testid=firstName'
          text: Priya
        caption: "First name"
        narration: "Real keystrokes into first name."
        focus: target

      - type:
          target: 'testid=lastName'
          text: Raman
        caption: "Last name"
        narration: "Last name."
        focus: target

      - type:
          target: 'testid=postalCode'
          text: "560001"
        caption: "Postal code"
        narration: "Postal code."
        focus: target

      - click: 'testid=continue'
        caption: "Continue"
        narration: "Totals are filmed from the app, never invented."
        focus: target
        shot: cut

  - id: overview
    title: Frame the click
    steps:
      # Wait on copy that proves the overview painted — Finish alone can resolve on a
      # still-white shell and get filmed as a blank content region.
      - wait:
          for: 'text=Total:'
          state: visible
        timeout: 20000
        caption: "Overview painted"
        narration: "Overview is on screen with the store's own totals."
        focus: wide

      - wait:
          for: 'testid=finish'
          state: visible
        timeout: 20000
        caption: "Finish in view"
        narration: "Finish is framed — target-in-frame is a hard check."
        focus: 'testid=finish'

      - hover: 'testid=finish'
        caption: "Camera on the button"
        narration: "Camera tracks measured geometry so the click is readable."
        focus: target

      - click: 'testid=finish'
        caption: "Place the order"
        narration: "Cursor-on-target confirms the hit in the delivered pixels."
        focus: target

  - id: done
    title: Publishable verdict
    steps:
      - wait:
          for: 'text="Thank you for your order!"'
          state: visible
        timeout: 25000
        caption: "Confirmation"
        narration: "Order confirmed on the live store."
        focus: wide

      - expect:
          target: 'text="Thank you for your order!"'
          visible: true
        caption: "Verdict: publishable"
        narration: "Ten checks on the delivered video. Exit zero only when publishable — product video as a build artifact."
        focus: wide
The contact sheet verification wroteverify/contact-sheet.png
Annotated contact sheet of key moments from the saucedemo render
Key moments as an annotated grid, so the output can be reviewed without watching it end to end. Its hash is recorded in the verdict.
What each check assertedplayhead/checks@4
encodethe file is a real, decodable H.264 video of the planned length
target-in-frameevery action target was fully inside the frame, and large enough to read
cursor-on-targetthe rendered cursor was on the element at the moment of each click
action-effecteach action visibly changed the UI, compared frame to frame
output-pixelsthe delivered pixels match the region the plan promised
frozen-blankno frozen stretches and no blank content regions
captionsevery caption fit its card without being shrunk or truncated
pacingno step is too fast to follow
masksmasked regions are opaque in the output and absent from the log
audioeach spoken line is audible inside its own window at target loudness

A product flow with a modal, a masked field and a toast

Product flow1:12narrated publishable

The pipeline's own demo app, built to exercise the hard parts: a dialog in the browser top layer, a redacted password, network latency and a success toast. Cursor and click now land on the same beat as the UI reaction.

step map12 steps
output
1:121920×1080 · 30fps · 3.1 MB
capture
378 frames38.5s of source · viewport 1280×720
steps
125 camera keyframes · 4 click ripples
narration
12 lineskokoro · voice heart · −16 LUFS
verdict
publishable6/7 actions pixel-verified
video sha256
51533ce2968922f23d20c193…playhead 0.4.0 · win32-x64
passencode — h264 1920x1080 @ 30fps, 72.0s
passtarget-in-frame — 11/11 targets fully framed, min rendered height 38px
passcursor-on-target — 4/4 clicks land on target; sprite verified on film at 2/2 sampled beat(s)
passaction-effect — 6/6 actions visibly effective (1 masked skipped)
passoutput-pixels — 6 sampled output regions match their planned source
passfrozen-blank — 144 sampled output frames live and non-blank
passcaptions — 12 captions fit at full size
passpacing — 12 steps paced ≥1.2s, total 71.9s
passmasks — 2 solid region(s) verified opaque, 1/1 re-verified in the output video; log clean
passaudio — 12 lines audible in their windows, aac, -16.0 LUFS
verdict: publishableexit 0
The spec that produced itacme-onboarding.yaml
# The product-flow example: a modal in the browser top layer, a redacted
# password that never reaches a frame, a masked environment banner, and a
# success toast — narrated, with the camera framing each action.
playhead: 1
title: Onboard a teammate in Acme Admin
subtitle: Account live in under a minute
app:
  url: http://localhost:4173
  viewport: 1280x720
  environment: { timezone: UTC, locale: en-US, fixedTime: "2026-01-01T12:00:00Z" }
output:
  kind: walkthrough
  aspect: "16:9"
  pacing: normal
  dwellScale: 1.1
  audio:
    narration: tts
    provider: kokoro
    voice: heart
    rate: 105
    sfx: true
  endCard:
    title: Acme Admin
    subtitle: Onboarding, filmed from the real app
theme:
  accent: "#6d7cff"
  captions: bottom
masking:
  # Redacted in the DOM before any pixel is painted — the staging banner is
  # never in a frame to begin with, so it cannot leak through a bad crop.
  - target: 'testid=env-banner'
    style: solid
scenes:
  - id: dashboard
    title: Where you start
    steps:
      - expect:
          target: 'role=heading[name="Dashboard"]'
          visible: true
        caption: "The admin dashboard"
        narration: "Here is Acme Admin, the real application, with the staging banner masked out before a single pixel is painted."
        focus: wide

  - id: users
    title: Open Users
    steps:
      - click: 'role=link[name="Users"]'
        caption: "Open Users"
        narration: "We start where an administrator starts — the list of people who already have access."
        focus: 'role=heading[name="Users"]'
      - expect:
          target: 'role=heading[name="Users"]'
          visible: true
        caption: "The team directory"
        narration: "Verification checks that this heading really arrived, not merely that a click was dispatched."

  - id: invite
    title: Add a teammate
    steps:
      - click: 'role=button[name="Add user"]'
        # The dialog fills most of the screen, so there is no close-up that can
        # hold both the button and its result: say "wide" rather than making the
        # planner discover that per frame and fall back.
        focus: wide
        shot: cut
        caption: "Add user"
        narration: "The invite form opens in a dialog, which the browser paints in its top layer above everything else."
      - type:
          target: 'label=Full name'
          text: Jane Doe
        caption: "Type the full name"
        narration: "Every field is filled with real keystrokes into the real input."
      - type:
          target: 'label=Email'
          text: jane@acme.dev
        caption: "Enter the work email"
        narration: "The work email — this is what the invitation will be sent to."
      - type:
          target: 'label=Temporary password'
          text: "s3cret-Pass!"
          mask: true
        caption: "Set a temporary password"
        narration: "The temporary password is masked at capture time. The real value exists only in the spec file, never in a frame and never in the event log."
      - click: 'role=button[name="Create user"]'
        caption: "Create the account"
        narration: "One click, and the account is live."
        focus: 'text="User added"'

  - id: confirm
    title: Confirm it worked
    steps:
      - expect:
          target: 'text="User added"'
          visible: true
        caption: "Account created"
        narration: "The toast confirms it, and the check for a visible effect confirms the toast."
        # A toast is too small to fill a 16:9 close-up: zooming to it framed several seconds
        # of empty panel and tripped the frozen-blank check. Hold the page instead.
        focus: wide
      - expect:
          target: 'text="Jane Doe"'
          visible: true
        caption: "Jane is in the directory"
        narration: "Jane is now in the directory — the outcome an administrator actually cares about."
        focus: wide

  - id: audit
    title: The audit trail
    steps:
      - click: 'role=link[name="Activity"]'
        caption: "Open Activity"
        narration: "And because this is the real app, the action left a real audit trail."
        focus: wide
      - scroll: { by: { y: 220 } }
        caption: "The new entry"
        narration: "Change the wording of this walkthrough and you re-compose it in seconds — the application is never touched again."
        focus: wide
The contact sheet verification wroteverify/contact-sheet.png
Annotated contact sheet of key moments from the crm render
Key moments as an annotated grid, so the output can be reviewed without watching it end to end. Its hash is recorded in the verdict.
What each check assertedplayhead/checks@4
encodethe file is a real, decodable H.264 video of the planned length
target-in-frameevery action target was fully inside the frame, and large enough to read
cursor-on-targetthe rendered cursor was on the element at the moment of each click
action-effecteach action visibly changed the UI, compared frame to frame
output-pixelsthe delivered pixels match the region the plan promised
frozen-blankno frozen stretches and no blank content regions
captionsevery caption fit its card without being shrunk or truncated
pacingno step is too fast to follow
masksmasked regions are opaque in the output and absent from the log
audioeach spoken line is audible inside its own window at target loudness

A $48,200 refund the UI called successful

Bug repro1:22narrated publishable

A watchable bug report: numbered steps, a live console feed that leaves before the last slide, a closing report card, and bug-report.md. The toast says the money came back. The ledger and the stack say it did not.

step map11 steps
output
1:221920×1080 · 30fps · 3.7 MB
capture
246 frames30.7s of source · viewport 1280×720
steps
111 camera keyframes · 3 click ripples
narration
11 lineskokoro · voice michael · −16 LUFS
verdict
publishable3/3 actions pixel-verified
video sha256
4735d2b61e7f85d789efd76a…playhead 0.4.0 · win32-x64
passencode — h264 1920x1080 @ 30fps, 82.0s
passtarget-in-frame — 8/8 targets fully framed, min rendered height 24px
passcursor-on-target — 3/3 clicks land on target; sprite verified on film at 2/2 sampled beat(s)
passaction-effect — 3/3 actions visibly effective
passoutput-pixels — 4 sampled output regions match their planned source
passfrozen-blank — 164 sampled output frames live and non-blank
passcaptions — 11 captions fit at full size
passpacing — 11 steps paced ≥1.2s, total 82.0s
passaudio — 11 lines audible in their windows, aac, -16.5 LUFS
verdict: publishableexit 0
The spec that produced ithelios-refund-bug.yaml
# Bug-repro: a refund that the UI calls successful while the API throws.
# The film is literal (no cinematic zoom). Console errors are painted on the
# output from capture/console.json so the ticket includes the stack, not just the toast.
playhead: 1
title: "Repro: refund reports success, charge stays captured"
subtitle: Helios billing · $48,200 Northwind seat expansion
app:
  url: http://localhost:4173/helios.html
  viewport: 1280x720
  environment: { timezone: UTC, locale: en-US }
output:
  kind: bug-repro
  aspect: "16:9"
  pacing: normal
  dwellScale: 1.2
  audio:
    narration: tts
    provider: kokoro
    voice: michael
    rate: 100
    sfx: true
  endCard:
    title: Helios Ops
    subtitle: Bug-repro · billing refund
theme:
  accent: "#ef4444"
  captions: bottom

scenes:
  - id: open
    title: Overview
    steps:
      - expect:
          target: 'role=heading[name="Overview"]'
          visible: true
        caption: "Helios Ops"
        narration: "Helios Ops, the payments console. Northwind Trading was charged forty-eight thousand two hundred dollars for a seat expansion."
        focus: wide

  - id: billing
    title: Billing
    steps:
      - click: 'role=link[name="Billing"]'
        caption: "Open Billing"
        narration: "Billing is where the charge should be able to move to Refunded."
        focus: 'role=heading[name="Recent charges"] >> nth=0'
      - expect:
          target: 'role=heading[name="Recent charges"] >> nth=0'
          visible: true
        caption: "Recent charges"
        narration: "Northwind is still captured. The outstanding balance is forty-eight thousand two hundred."
        focus: wide
      - expect:
          target: 'text="Northwind outstanding · $48,200" >> nth=0'
          visible: true
        caption: "Outstanding $48,200"
        narration: "That figure is the ledger. After a real refund it has to drop to zero."
        focus: wide

  - id: refund
    title: Issue refund
    steps:
      - click: 'role=button[name="Refund Northwind Trading"]'
        caption: "Refund Northwind Trading"
        narration: "Refund. The confirm dialog states the contract: status becomes Refunded, balance becomes zero."
        focus: wide
        shot: cut
      - expect:
          target: 'role=heading[name="Issue refund?"]'
          visible: true
        caption: "Confirm the refund"
        narration: "Issue refund. This is the click a support engineer would make."
        focus: wide
      - click: 'role=button[name="Issue refund"]'
        caption: "Issue refund"
        narration: "The toast says the money came back. Watch the row — and watch the console."
        focus: wide
      - wait:
          for: 'text=refund.failed'
          state: visible
        timeout: 8000
        caption: "The webhook says failed"
        narration: "The last webhook is refund.failed. idempotencyKey is undefined. The API threw a 500."
        focus: wide

  - id: lie
    title: The UI lied
    steps:
      - expect:
          target: 'text="Northwind outstanding · $48,200" >> nth=0'
          visible: true
        caption: "Balance unchanged"
        narration: "The outstanding balance did not move. The charge is still captured."
        focus: wide
      - expect:
          target: 'text="Captured"'
          visible: true
        caption: "Still captured"
        narration: "That is the bug: a success toast on a thrown TypeError, forty-eight thousand dollars still on the ledger, and the stack in the console."
        focus: wide
      - wait: { ms: 1800 }
        caption: "Hold on the evidence"
        narration: "Attach this clip, the verdict, and console.json to the ticket. The recording is the report."
        focus: wide
The contact sheet verification wroteverify/contact-sheet.png
Annotated contact sheet of key moments from the bug render
Key moments as an annotated grid, so the output can be reviewed without watching it end to end. Its hash is recorded in the verdict.

bug-report.md — the pasteable write-up: URL, steps with timecodes, and the captured console.

What each check assertedplayhead/checks@4
encodethe file is a real, decodable H.264 video of the planned length
target-in-frameevery action target was fully inside the frame, and large enough to read
cursor-on-targetthe rendered cursor was on the element at the moment of each click
action-effecteach action visibly changed the UI, compared frame to frame
output-pixelsthe delivered pixels match the region the plan promised
frozen-blankno frozen stretches and no blank content regions
captionsevery caption fit its card without being shrunk or truncated
pacingno step is too fast to follow
masksmasked regions are opaque in the output and absent from the log
audioeach spoken line is audible inside its own window at target loudness
reproduce it

Every one of these is two commands away.

The specs above are the complete input. Validate the locators against your own app, render, and read the verdict — no account, no API key, nothing leaves your machine.

npx playhead init
playhead validate specs/tour.yaml     # every locator, against the live app
playhead render specs/tour.yaml -o out