Command Line

Build a whole demo

Describe a whole demo video in one storyboard file — the site to record, the 3D device, camera moves, graphics, narration and music — and build it with one command that also checks the result.

Recording, styling, effects and titles each have their own command. A storyboard puts all of them in one JSON file, and screenify demo build runs them in order: record the site, add the intro and outro spaces, the 3D device, camera moves, graphics, titles, narration and music, export every aspect ratio you ask for, and review each video.

screenify demo schema --json                  # the format, a full example, and the steps an AI agent follows
screenify demo build demo.json --dry-run      # check the storyboard without recording
screenify demo build demo.json                # record, build and review

The working project is saved next to the video as <name>.screenify, so you can open it in Screenify Studio and keep editing. Build again with --force to replace a project that demo build made.

A storyboard

{
  "capture": {
    "web": "https://your-app.com",
    "idleTrim": false,
    "actions": [
      { "type": "click", "role": "link", "name": "^Docs$", "afterMs": 2000 },
      { "type": "goto", "url": "/docs/features", "afterMs": 2000 },
      { "type": "scroll", "dy": 600, "afterMs": 1800 },
      { "type": "goto", "url": "/pricing", "afterMs": 2500 }
    ]
  },
  "space": { "intro": 3, "outro": 3.5 },
  "stage": { "mockup": "macbook-pro-16:silver", "environment": "studio-warm", "desk": "wood", "wallpaper": "orange" },
  "camera": [
    { "motion": "slow-orbit", "start": 3, "end": 10 },
    { "motion": "drift", "start": 10, "end": "end" }
  ],
  "scenes": [
    { "graphic": "launch-hero", "at": "intro",
      "fields": { "badge": "NEW", "title": "Your product,", "emphasis": "one click away." },
      "say": "Meet the new release." },
    { "graphic": "lower-third", "at": "@goto:features+0.5", "duration": 3,
      "fields": { "logo": "A", "title": "Features", "subtitle": "Everything in one place" } },
    { "text": "Free to try", "at": "@goto:pricing+0.5", "duration": 2.5 },
    { "graphic": "end-card", "at": "outro",
      "fields": { "title": "Try it today.", "button": "Download free", "url": "your-app.com" } }
  ],
  "audio": { "music": "upbeat", "voice": "af_heart" },
  "output": { "path": "~/Desktop/demo.mp4", "aspects": ["wide", "vertical"], "platform": "x" }
}
KeyWhat it holds
captureweb + actions (a web demo script), or web + prompt to let an AI drive the browser, or project to start from a recording you already have (it is copied, never changed). auth reuses a saved login
spaceEmpty full-frame space before (intro) and after (outro) the recording, for a title or end card
stageThe look — the same values as the export flags: mockup, environment, desk, lighting, wallpaper, background, padding, realistic, browser …
cameraDevice Motion shots on a 3D device: motion, start, end. "end": "end" runs a shot to the last second of the recording
scenesGraphics (graphic + fields) and titles (text), each with at and duration. Add "say": "…" to narrate a scene
audiomusic (a mood such as calm or upbeat, or a specific track) · voice and speed for the narration
effectsExtra effect steps, written as you would after --plan
outputpath, aspects (one file per aspect), fps, duration, platform, watermark, review

Tie each scene to what is on screen

Pages load at different speeds, so a scene placed at "second 12" drifts away from the page it was meant for. Tie it to an event of the recording instead:

atShows the scene
"@goto:pricing+0.5"half a second after a page whose address contains pricing opens — by a goto step, a click, or as the first page
"@click:Upgrade"when the button or link labelled Upgrade is clicked
"@type:email" · "@scroll" · "@event:…"when typing, scrolling or any matching event happens
"intro" · "outro"in the empty space before or after the recording
12at a fixed second

The same anchors work in any effect plan: screenify effects demo.screenify --plan "graphic pricing … start=@goto:pricing+0.5".

Narration and music

"say" reads the line with the same AI voices as the app and puts it on the Voiceover track, starting with the scene. Pick the voice with audio.voice (e.g. af_heart, am_michael) and the pace with audio.speed (0.85, 1, 1.2). Narration lines must not overlap. The voice engine is the one the app uses — it needs Screenify Studio installed, and the voice model downloads once.

audio.music takes a mood (calm, energetic …) or a genre (upbeat, cinematic, lofi …) and picks a matching Screenify Originals track, or an object { "track": "…", "volume": 0.7, "fadein": 0.5, "fadeout": 2 }.

Check before you record

--dry-run prints the plan and a time table, and checks every graphic and title — the template exists, the field names are right, and no field still holds the template's sample text — without opening a browser. Links are checked when the real build starts: a broken link stops it before anything is recorded, with the address and a hint.

Every build is reviewed

After each export, demo build runs read review and returns {pass, failures, warnings} per file, plus a contact sheet image next to the video. Fix each failure in the storyboard and build again. Warnings are worth a look too.

From an AI assistant

Claude Desktop, Cursor and other MCP apps send the same storyboard to the record_web_demo tool. They read the format with get_schemas (which: "storyboard") and can check a storyboard first with dryRun: true. Ask in plain words — "Make a 30s demo of our pricing page with narration" — and the assistant reads your site, writes the storyboard, builds it and reviews the result.

Good demos, in practice

  • Use only facts from your own pages — real names, numbers and prices. Leave out a stat or quote card you have no source for.
  • Show something happening: a click that changes the page beats scrolling alone.
  • Effects that aim at clicks (3D zoom, spotlight, callout) need clicks that stay on the page — a click that opens another page gives them nothing to aim at.
  • When the brief names a length, set "idleTrim": false so the waits you wrote stay in, and end the last camera shot at "end".