Spec reference

One YAML file describes the whole video. It is validated by a Zod schema in strict mode, so an unknown field is an error rather than a silently ignored knob — a misspelled option should never be indistinguishable from a feature that works badly.

Shape of a spec#

playhead: 1                     # required, the format version
title: Onboard a teammate       # required, the title card
subtitle: Account live in a minute

app:
  url: http://localhost:3000
  viewport: 1280x720

output:
  kind: walkthrough
  aspect: "16:9"
  audio: { narration: tts, provider: kokoro, voice: heart }

theme:
  accent: "#6d7cff"
  captions: bottom

masking:
  - target: 'testid=env-banner'

setup:                          # runs before recording, never filmed
  - click: 'role=button[name="Accept cookies"]'
    optional: true

scenes:
  - id: invite
    title: Add a teammate
    steps:
      - click: 'role=button[name="Add user"]'
        caption: Add a user
        narration: Adding a teammate takes one form.

app — what to film#

FieldMeaning
urlRequired. Where capture starts.
viewportCapture viewport, e.g. 1280x720. Omit it and the viewport is derived from output.aspect, so a portrait video is captured at a portrait viewport and the app reflows natively instead of being cropped.
storageStateA session file from playhead login, for authed screens.
assertLoggedInA locator that must match after the initial load. Stale auth aborts the capture instead of confidently filming your login page.
compareUrlA second app to capture the same flow against — the “before” in a before-after video.
dialogsaccept (default) or dismiss for window.confirm and alert. Use dismiss to film the cancel branch of a confirm-guarded flow.
dismissAuto-dismiss cookie banners and chat widgets before filming. Default true — set false when the messenger is what you are filming.
navigationload (default) or domcontentloaded. Ad-heavy pages may never fire load; the looser setting plus the settle heuristic makes them filmable instead of timing out.
settle{ idleMs, capMs } — settle tuning for slow backends and chatty apps.

app.environment — determinism#

Same spec, same pixels, regardless of where or when it runs. This is what makes --expect-sha a meaningful assertion.

app:
  environment:
    timezone: UTC
    locale: en-US
    colorScheme: dark
    fixedTime: "2026-01-01T12:00:00Z"   # "3 minutes ago" stops drifting
    reducedMotion: true

app.network — request interception#

app:
  network:
    block: ["*.googletagmanager.com", "*/analytics/*"]
    stub:
      - url: "*/api/usage"
        status: 200
        contentType: application/json
        body: '{"seats": 42}'
    har: fixtures/session.har

Blocking third-party noise is usually the single biggest improvement to a flaky capture.

output — how it is presented#

FieldValuesNotes
kindwalkthrough · demo · bug-repro · before-after · release · socialSets the whole presentation posture. Explained here →
aspect16:9 · 9:16 · 1:1Default 16:9 → 1920×1080. Portrait is 1080×1920, square 1080×1080.
resolution1920x1080Optional override; derived from the aspect otherwise.
fps24–60Default 30.
pacingrelaxed · normal · briskHow long steps hold and how hard idle time is trimmed.
dwellScale0.5–4Multiplier on per-step dwell. Raise it for teaching walkthroughs where each screen should linger.
camerawide · normal · tightFraming appetite. wide keeps the whole screen in view and waives the close-up legibility floor.
endCard{ title, subtitle }Compose defaults to title + host when omitted, so a video never ends on a freeze-frame.
audio{ narration, provider, … }Narration & voices →

theme#

accent is a #rrggbb hex colour and captions is bottom (default), top or off. There is deliberately no name field: exactly one theme exists today, and a knob that is accepted but ignored is a lie.

masking — keeping secrets off film#

masking:
  - target: 'testid=env-banner'
    style: solid        # or blur

Rules apply for the whole capture. Each overlay box is parented into the target’s own subtree, which is what lets it cover a field inside a <dialog> that paints in the browser top layer above any document-level overlay. For a single typed value, use mask: true on the step instead — the text is redacted to bullets before it ever enters the event log, and the real value exists only in your spec file.

setup — unfilmed groundwork#

Same step grammar as scenes, but it runs before recording starts and never appears on film: dismiss a consent banner, close a first-run tour, prime app state. Combine with optional: true for anything that might not be there.

scenes and steps#

A scene has a lowercase kebab-case id, an optional title that becomes an on-screen chapter, and at least one step. Ids must be unique — they form the stepRef (scene/index) used in verdicts, the step map and --from-scene.

The thirteen verbs#

VerbFormNotes
gotogoto: /settingsNavigate mid-flow.
clickclick: 'role=button[name="Save"]'The workhorse.
dblclickdblclick: 'text="Rename"'Double click.
hoverhover: 'text="Total revenue"'Tooltips, previews, hover menus.
typetype: { target, text, mask?, clear? }clear: true selects all and overwrites — without it, editing a pre-filled field produces “Janenew value”.
presspress: EnterAny key or chord, e.g. Control+K.
rightclickrightclick: 'testid=row-3'Context menus.
uploadupload: { target, file }Sets the file input directly; no native dialog is involved or filmable.
dragdrag: { target, to }Kanban cards, list reorder, sliders.
selectselect: { target, value }Native select elements.
scrollscroll: { by: { y: 400 } }Or scroll: { target } to bring an element into view.
expectexpect: { target, visible|text|count|value|url|disabled|checked }An assertion the verifier will prove. Needs a target, a url, or both.
waitwait: { for: <locator>, state: visible|hidden }The structured form. wait: 2000 is a blind sleep and is discouraged; exactly one of the two is allowed.

Extras available on every step#

KeyWhat it does
captionThe on-screen card. Numbered, laid out and overflow-checked at compose time.
narrationThe spoken line, free of the caption card’s size limit. Falls back to the caption, then to an auto line.
focustarget, wide, or a locator naming where the payoff appears — the act-then-react shot.
shotcut forces this step into its own shot; continue merges it into the previous one. Camera cuts stay decoupled from scene structure.
timeoutPer-step budget in ms for finding the target or meeting the expectation.
optionalA failing step is skipped with a warning instead of killing the capture — for cookie banners and A/B'd tooltips.
maskOn a type step: redact the typed value.

Reuse: vars and extends#

extends: ./base.yaml       # merged underneath this file, max 4 deep

vars:
  tenant: acme
  token: ${env.DEMO_TOKEN}
  region: ${env.REGION:-eu-west-1}

app:
  url: https://{{tenant}}.example.com

{{name}} is interpolated into every string in the spec, and an unknown variable is an error rather than an empty string. Values may read the environment as ${env.NAME} with an optional :-default. Base scenes and masking rules are appended first, so scene ids must stay unique across both files.

Errors point at lines. Validation failures carry the line and column from the YAML source, and unknown keys get a “did you mean” suggestion — including for a misspelled action verb like txet:.