Record PR Demo
payloadcms/payload
A skill your agent uses when a Payload pull request needs a concise visual walkthrough for reviewers.
Record a narrated walkthrough video of a working Mendix app — the human-facing half of the journey/demo pair.
$ npx skills add mendixlabs/mxcli --skill record-narrated-demo -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install mendixlabs/mxcli record-narrated-demo --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/mendix/record-narrated-demo .claude/skills/record-narrated-demo && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "record-narrated-demo" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/record-narrated-demo into .claude/skills/record-narrated-demo/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "record-narrated-demo", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/record-narrated-demoType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add mendixlabs/mxcli --skill record-narrated-demo -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install mendixlabs/mxcli record-narrated-demo --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/mendix/record-narrated-demo .agents/skills/record-narrated-demo && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "record-narrated-demo" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/record-narrated-demo into .agents/skills/record-narrated-demo/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "record-narrated-demo", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add mendixlabs/mxcli --skill record-narrated-demo -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install mendixlabs/mxcli record-narrated-demo --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/mendix/record-narrated-demo .cursor/skills/record-narrated-demo && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "record-narrated-demo" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/record-narrated-demo into .cursor/skills/record-narrated-demo/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "record-narrated-demo", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/mendixlabs/mxcli.git --path .claude/skills/mendix/record-narrated-demo--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add mendixlabs/mxcli --skill record-narrated-demo -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install mendixlabs/mxcli record-narrated-demo --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/mendix/record-narrated-demo .gemini/skills/record-narrated-demo && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "record-narrated-demo" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/record-narrated-demo into .gemini/skills/record-narrated-demo/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "record-narrated-demo", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install mendixlabs/mxcli record-narrated-demoInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add mendixlabs/mxcli --skill record-narrated-demo -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/mendix/record-narrated-demo .github/skills/record-narrated-demo && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "record-narrated-demo" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/record-narrated-demo into .github/skills/record-narrated-demo/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "record-narrated-demo", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add mendixlabs/mxcli --skill record-narrated-demo -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install mendixlabs/mxcli record-narrated-demo --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/mendixlabs/mxcli.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/mendix/record-narrated-demo .opencode/skills/record-narrated-demo && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "record-narrated-demo" agent skill from https://github.com/mendixlabs/mxcli/tree/main/.claude/skills/mendix/record-narrated-demo into .opencode/skills/record-narrated-demo/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "record-narrated-demo", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
record-narrated-demoRecord a narrated walkthrough video of a working Mendix app — the human-facing half of the journey/demo pair.
Record Narrated Demo is an agent skill from mendixlabs/mxcli. Record a narrated walkthrough video of a working Mendix app — the human-facing half of the journey/demo pair. Use when asked to demo, show off, or produce a walkthrough recording, after the app's end-to-end journey already passes.
Its SKILL.md is about 6.8k tokens, which your agent loads only when the skill is triggered. The skill folder holds 3 other files (for example `cut-clips.js`, `narrate.js` and `take.js`).
The repository describes itself as: Mendix cli tool, a headless way to work with Mendix projects. Enables Mendix projects for use with 3rd party agentic coding tools like Claude Code and Copilot. Includes a… The licence is Apache-2.0.
5 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 5c321d0. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships script files (JavaScript), which the agent can run.
Shell commands in SKILL.md call:
apt-getnodeFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Record Narrated Demo loads about 6.8k tokens when it runs. Until then it costs about 63 tokens; SKILL.md has 4,270 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from mendixlabs/mxcli at commit 5c321d0, republished under its Apache-2.0 licence (© mendixlabs). 4,270 words, ~6,823 tokens.
.claude/skills/record-narrated-demo/SKILL.md (or your agent's skills folder). This skill also uses 3 other files; get the full folder from GitHub.Proving a feature works and showing it off are two different jobs, and one script cannot do both well. This skill covers the second. It assumes the first is done.
| Stage 1 — journey | Stage 2 — demo | |
|---|---|---|
| Script | journey-runner.js | narrated-walkthrough.js |
| Job | asserts | explains |
| Gates the build | yes | no |
| Optimised for | signal — no narration, no reading pauses | a viewer — human pace, on-screen narration |
| Verdict lives here | yes | never |
Both walk the same persona down the same path. The demo reuses the journey's shape and its OQL-backed data checks, so what it shows on screen is still true — but the PASS/FAIL judgment stays in the gating runner. A demo that can fail the build is a test with worse ergonomics; a test that narrates is a demo that misses regressions.
Write journeys/<Module>.journey.json first — one persona, one path, with
carried state, not a list of page stops. Its canonical definition is
skills/journey-proof.md in mxcli-project-toolkit; do not re-derive the
protocol here. In outline, each step asserts five independent rungs:
ready widget is visible. Without it every
later assertion silently runs against the previous page.textPresent / textAbsent. The backend can be
correct and the screen can still lie about it.mustNotFire.Every rung is proved falsifiable by re-running with one broken precondition each
(7 mutants — rungs 3 and 4 make two claims apiece) and requiring the targeted
check to fail. A rung nobody could break is UNPROVEN, which is a fault, not
a pass. Verdicts are PASS / FAIL / INVALID and never collapse into each
other: INVALID means the instrument did not run, which is a finding of its own.
Writing the journey first is what makes the demo cheap. The persona, the path, and the definition of success already exist by the time you record.
The failure this skill is most prone to, and it is not subtle. The first real
ContactBook capture was an empty grid reading 0 to 0 of 0 with a column header
rendering as colActions — which reads as a broken app, not a new one. The
journey passed; the app was live; the recording was worthless.
Before any take:
test1 / asdf / aaa.mxcli seeds this itself — see demo-data. Seeding is
part of recording, not a nicety before it.
The things that decide whether the video is watchable, and whether it looks like the rest of the catalogue. Each has a reason; none is a style preference.
A cursor moving at test speed reads as broken, not fast. The gating runner is tuned for signal and should stay that way — slow the demo script down on its own, and leave reading pauses where a viewer would actually need to read.
Two numbers, both from films that were re-cut for being too fast:
max(caption read time, screen read time), floor 2.5s.
Caption read time is roughly words ÷ 3.5 seconds — which is what narrate.js
computes. Screen read time is how long it takes a viewer to find the thing that
changed, and it is always longer than it feels while authoring. narrate.js
knows only the caption; when the screen is the slower of the two — or when a
narration line is longer than both — pass the measured duration as holdMs.And three the finished film is measured against, from the video system's
TONE-AND-SPEED.md — a product demonstration is 60–120s, 55–65% voice
density, ~135 wpm. Density is the one worth checking early: it is the tail
budget stated as a ratio, and a walk that fills 80% of its runtime with talking
is not a slow film that needs trimming, it is a fast one wearing a slow pace.
Playwright's video captures only frames the compositor actually produces. A genuinely idle screen during a reading pause can collapse to almost no video, so a 5-second pause plays back in a blink and the narration desyncs. Keep something continuously animating so idle time is recorded as idle time.
narrate.js does this with a hairline segment sweeping the caption plate's top
edge, on a loop, for the whole take. It used to be a spinning ring, and the ring
had to go for a reason worth keeping in mind whenever this element is redesigned:
the design language has no rounded corners, so a ring can only be a spinning
square. The keep-alive has to be expressible in the system's own vocabulary —
here, 1px structure travelling — rather than bolted on beside it. What it must
not become is per-caption: a hold between captions still has to produce frames,
so the animation runs continuously and is never keyed to a reveal.
Same steps, same assertions, nothing simplified for the smaller screen. A pass that trims the small-screen steps would report success on an app nobody can use on a phone. This is the pass that catches layout failures nothing static sees — an mxcli-built app recorded at 414×896 completed the whole flow while both DataGrid2 screens were unreadable: eight columns compressed to eight single characters, headers degraded to bare sort arrows. The app functioned on a phone and was not usable on one, and only the mobile recording showed the difference.
A narrow viewport is not a mobile profile. Mendix picks its navigation
profile from the user agent, not from the window size, so a take that only
shrinks viewport films the desktop app in a narrow window — the phone profile
is never routed to, and the pass cannot show the thing it exists to find while
looking entirely plausible. Pass the device through contextOptions:
const take = await openTake(browser, {
size: { width: 430, height: 932 },
contextOptions: {
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ' +
'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
isMobile: true,
hasTouch: true,
deviceScaleFactor: 3,
},
});viewport and recordVideo are set by openTake itself and win over anything
in contextOptions, because both are load-bearing for the cut — a device preset
carrying its own viewport would silently letterbox every take.
Also worth knowing before you read a mobile take as a layout bug: a Mendix page carries its own layout, and the layout names the navigation profile. The Phone profile controls the home page and the menu; it does not re-skin the pages a user reaches afterwards. A phone user routed to a page built on a desktop layout gets the desktop frame whatever profile routed them there — measured at 430×932, a 232 px rail on a 430 px screen with the row's action laid out 42 px past the right edge. That is a real defect and the take is right to fail on it, but the fix is per-page layouts, not a theme tweak.
recordVideo needs a Node script, not playwright-climxcli verify's browser checks run bash scripts against a persistent
playwright-cli session (see test-app). Video is a
context-creation option, so the demo script owns its own browser context via
the Playwright library instead. That is a second reason Stage 2 is a separate
script rather than a flag on the gating runner.
Browser and headless-shell setup is test-app's — do not
duplicate it here. Data assertions under the demo use
verify-with-oql. Boot the app with
run-local (mxcli run --local); for a still-image set
rather than a video, --screenshot with repeated --screenshot-url already does
that without any script.
narrate.jsnarrate.js sits beside this file and is copied into the project along with it.
Require it from the demo script rather than writing another one — the last three
projects each re-derived their own Stage 2 from this page's prose, which is why
no two demos look alike.
It asserts nothing and holds no selectors, so it is the same file in every project:
configure({ zoom, accent, ground, font }) | tokens and the zoom take.js is using — see below; call it before anything else |
say(page, text, step) | caption, held for max(2500, words * 280) ms — a fixed hold rushes long lines and stalls on short ones. Refuses a caption containing a tofu glyph |
point / unpoint | pulsing outline around an element's rect, drawn — a real click ring would move the cursor and the page under it. The film's one accent event |
clickSlowly | scroll in, mark, beat, click: a cursor that arrives and clicks in one frame reads as a glitch |
typeSlowly | per-key typing, then commit |
bringIntoView | includes horizontal scroll (inline: 'center'), for a grid whose action sits past a phone's right edge |
checkOverlay(page) | the design rules that are measurable in the page, as a check that throws. Run by install(), so it costs nothing to remember |
The sweeping hairline is the compositor fix described above, not decoration and not a loading indicator — it is what keeps a reading pause from collapsing to no frames. Removing it silently breaks the pause and any audio timed against it.
What stays per-project is the walk itself: the persona, the steps and the
selectors (narrated-walkthrough.js). Only the library is shared.
A capture is most of a Type A film's runtime, and the caption plate is the only
thing the film draws over it. Furniture that disagrees with the frames it cuts
against reads as two designs — so the overlay is built to
video-system/DESIGN-LANGUAGE.md, not to whatever looked reasonable in a
browser. What that changed, and why each one is a defect rather than a taste:
box-shadow was a glow.'Segoe UI' and friends do not exist on a clean
build machine and fall back to DejaVu Sans with nothing saying so, so the
plate now inherits the app's own computed body font. That is also the
seam-free choice: furniture set in a different face from the UI it wraps is
the one thing a viewer notices without being able to name it.!= into one glyph, and on a caption quoting the app that is
a claim rather than a typographic choice. say() refuses ≠ → ✓ ✗ and the
rest of the tofu set outright — derive the real set from the font's cmap when
you know the file (fontTools recipe in PRODUCTION.md §3).y896–1080 — and the safe-area gutter puts its text at x96. That band is
reserved across the whole catalogue, kept clear even in films that carry no
captions, so a plate that is 96px tall or floated somewhere else is not a
smaller caption: it is the one film whose bottom edge does not line up.Geometry is stated in video pixels and converted with the zoom, and the two
coordinate spaces do not agree. This is the part worth reading twice, because
it is invisible until the capture is cut against a composed frame. take.js
reaches a fixed-width layout with CSS zoom on html, so a plate declared
96px tall lands at 96 × zoom in the file. Measured at zoom 1.6842: the old
96px plate is 162 video px, not the 184 the band wants — and it is worse than
a wrong number, because everything about it looks right in the browser. So pass
the zoom to configure() and let the overlay do the division.
Then measuring it back has the opposite trap, also measured on Chromium 1194:
under html{zoom:1.6842} | reports |
|---|---|
getBoundingClientRect() | video px — a 109.25px-tall plate measures 183.98 |
getComputedStyle() | CSS px — the same plate reports 109.25px |
checkOverlay() compares the rect raw and multiplies the computed padding, and
it exists because getting that backwards produces a perfectly styled plate in the
wrong place. Its control is one line: build the overlay without telling it the
zoom, and it reports a 310px plate starting at y770.
The same mismatch had already broken point(), silently, for as long as anything
has used zoom: it read a rect (video px) and assigned it straight to
style.left (CSS px, zoomed on the way out), so the highlight landed at
position × zoom. Measured at 1.6842, a target at (168, 202) was ringed at
(274, 330) — a ring around the wrong control, or off the screen, in a take that
otherwise looks fine. Anything that reads a rect and writes a style has to divide.
recordVideo writes a silent track — voice is not a setting, it is a second
pipeline you build and mux in. It has been produced ad hoc in a session before,
which is the problem: re-improvised each time, it lands on a different voice, a
different pace and different levels, so the demo's sound quality is luck. Pin it.
The voice is Kokoro bm_george, and it is not a per-film decision. Every
film in the catalogue uses it; a demo that arrives in a different voice breaks
the family harder than any visual difference, because the viewer hears the change
before they can look for it. Pace varies by type, the voice does not.
from kokoro import KPipeline
pipe = KPipeline(lang_code='b') # British English
audio = np.concatenate([c.audio.numpy() for c in pipe(line, voice='bm_george', speed=1.0)])
sf.write(f"assets/voice/{n:02d}.wav", audio, 24000) # Kokoro emits 24 kHz monoWhere mxcli is named at all — in a Type A film that is the closing credit only —
write it phonetically in the script, em ex see ell eye, so the TTS spells
the five letters out instead of trying to pronounce it. On screen it stays
mxcli, lowercase. A copy pass will "correct" this back into a word if nobody
says why it is there.
ffmpeg and ffprobe are not guaranteed present — both were absent from a
fresh web container, and apt-get install ffmpeg failed there against a stale
package index (404s on superseded libva/mesa versions) until apt-get update
ran first. Check for them before promising audio.
Four things decide whether the result sounds professional. All are measured, not matters of taste:
loudnorm (measure with print_format=json, then feed the measured values
back) to I=-18.6:TP=-1.5, which is where the rest of the films sit; an
earlier pass here targeted -16 and would have made the demo the loudest thing
in the catalogue by two and a half LU. Resample to 48 kHz stereo at the same
time — 24 kHz mono is not what a video container wants.loudnorm=I=-30: TP=-3:LRA=7) so it sits well under the voice. Duck it to near-silence under
the beat that pays off.ffprobe rather than trusting any synthesis flag.voice duration + tail — not the bare voice
length, which is what every sync tool defaults to and which leaves zero
reading time. This is also why the keep-alive above is load-bearing rather
than cosmetic: if an idle pause collapses to almost no frames, a pre-rendered
voice track drifts against the picture no matter how good the synthesis is.
Confirm the recorded file's duration matches the script's wall-clock before
adding audio at all.narrate.js makes a recording watchable. Nothing in it — by design — checks that
what you filmed actually happened, or that the timestamp you cut on points at it.
Four failures, each of which cost a take and each of which looked like success:
It is wrong in two ways at once, and fixing only the first is the trap:
A constant offset that was right at the start was four seconds wrong by the end — the difference between cutting to the payoff screen and cutting to the one before it. Three rounds of cuts showed the wrong moment in every beat before this was found, and each one was plausible in isolation.
So: record both anchors and map linearly, video_t = A + B × mark_t. Then
verify by looking — one frame from the middle of every clip, tiled into a
contact sheet. Spot-checking two clips is exactly how the wrong offset survived
those three rounds.
A click can be swallowed while the previous action's request is still in flight, and the result looks fine: a board ended up full but not solved, so the payoff never arrived and the control that depended on it stayed disabled. Check the DOM for the state the beat is about — not that the click returned.
A beat that cannot be asserted is a beat you cannot trust. This is not a verdict about the app: the demo still never gates the build. It is a check on the recording, and it belongs here for the same reason a camera has a viewfinder.
Driving entries faster than the runtime committed them made two microflows overlap
and deadlock in Postgres. The UpdateConflictException surfaced as a modal dialog
that then swallowed every later click and killed the take. Two defences: never act
faster than a floor found experimentally per app, and detect-and-dismiss the error
dialog so one failure does not cost the session.
Test the dialog guard on visibility, not presence — Mendix ships the error dialog container in the DOM hidden, so a presence test fires on every click.
recordVideo.size pads; it does not scaleA viewport smaller than the video size lands in the top-left corner with grey
around it. For a fixed-width page — which most Mendix layouts are — set the
viewport to the video size and apply CSS zoom: the page then lays out at the
smaller effective width while Chromium rasterizes at full device resolution.
Sharp and full-frame, where a smaller viewport is soft and letterboxed. A
stylesheet does not survive a navigation, so re-apply it after every goto.
take.js and cut-clips.jsBoth sit beside this file and are copied into the project with it. CommonJS, like
narrate.js, and required the same way.
openTake(browser, opts) | a context with recordVideo, both clock anchors, the zoom fix |
take.mark(name) | a beat, timed from the settled first screen |
take.click / take.type | paced to minGap and guarded against the error dialog |
take.assertBeat(name, probe, why) | records whether the beat held; finish() throws if one did not |
take.finish() | closes the context, writes capture/beats.json with offset_s |
node cut-clips.js | cuts the raw take on the linear map, refuses implausible anchors, writes the contact sheet |
cut-clips.js reads a project-owned capture/clips.json edit list, so the script
is the same everywhere and only the edit is per-film. It refuses a clip
shorter than its target unless that clip is explicitly marked "freeze": true —
holding a final frame is legitimate on a static screen, never to stretch an
interaction, and every pad is reported.
What stays per-project is the walk and the selectors. Only the machinery is shared.
Narrate only what a viewer with no build context would understand.
Cut:
Keep it to the persona's own motivation: what they are trying to do, what they see, and what changed for them. Name them — "Sam", not "the user".
Unless the product is single-player. A puzzle, a calculator, a personal tool has no task-persona, and inventing one is affectation. Write about the thing in present tense instead.
This skill owns the capture. How a capture is framed, cut and scored into a
finished film is the video system's — video-system/ in ako/mxcli-intro-video,
which defines the product-demonstration type this skill feeds. Read it before a
film, in its own order, and treat it as normative over this page: the numbers
here are copied from it and go stale when it moves.
DESIGN-LANGUAGE.md | palette, type, grid, plane, motion, the closing lockup, the honesty rules — invariant across all three types |
TONE-AND-SPEED.md | everything that varies, as numbers. The comparison table is the working document |
types/product-demonstration.md | the type this skill feeds |
PRODUCTION.md | the hazards, several of which are the ones on this page |
Four boundaries worth keeping:
narrate.js's. One caption system per film; a
HyperFrames caption layered on in the edit reads as two designs. A film may
also run the capture with no captions at all and carry the narration in voice
alone (videos/sudoku-demo does) — but then the caption band stays clear,
which is the same rule, not an exemption from it.point() is allowed precisely because it is in the capture,
decided at record time with the app's state in front of you.take.js records a whole walk in one browser context, which is why cut-clips.js
has to fit an offset and a ~1.065 clock scale before it can trust a mark. The
alternative — one context per beat — removes that problem entirely rather than
modelling it: each recording starts at its own zero, and there is nothing to map.
It costs the session, so each beat has to re-enter the app (carry the login as
Playwright storageState) and it cannot film a continuous interaction across a
cut. Take it when the walk is genuinely a set of independent scenes; keep the
single take when the continuity is the point.
journeys/<Module>.journey.json exists and the journey run is PASSUNPROVENconfigure() was told the same zoom as openTake, and checkOverlay
passed — the plate fills the caption band, flat, accent-free, in the app's
own fontsay() refuses the
known set; derive the rest from the font's cmap)bm_george, mastered two-pass to I=-18.6:TP=-1.5, and
every line was verified individually — not by total duration© mendixlabs, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 3 other files in .claude/skills/mendix/record-narrated-demo of mendixlabs/mxcli.
Open the folder on GitHubat commit 5c321d0
Record Narrated Demo next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Record Narrated Demo this skillmendixlabs/mxcli | 129 | — | ~6.8k | Automated safety check: Pass | Apache-2.0 | |
| Record PR Demopayloadcms/payload | 45k | — | ~1k | Automated safety check: Pass | MIT | |
| Record Extension Demomengxi-ream/read-frog | 10k | — | ~2.8k | Automated safety check: Pass | GPL-3.0 | |
| Mobile Demo Recordingsuperset-sh/superset | 15k | — | ~2.2k | Automated safety check: Notes | Custom licence | |
| Record Demoapify/mcpc | 990 | — | ~3.3k | Automated safety check: Notes | Apache-2.0 | |
| Recordingcodewhale-hq/Codewhale | 41k | — | ~540 | Automated safety check: Pass | MIT |
payloadcms/payload
A skill your agent uses when a Payload pull request needs a concise visual walkthrough for reviewers.
mengxi-ream/read-frog
Record polished, evidence-backed demos of the Read Frog extension as MP4 (and optional GIF) by driving real Chrome with the built extension, following captioned scene scripts, asserting extension…
superset-sh/superset
Runs the Superset mobile app on a headless iOS simulator against the local stack, drives it with Maestro and captures clean screen recordings and screenshots.
apify/mcpc
Record or regenerate the mcpc demo GIFs (the README hero docs/images/mcpc-demo.gif and the focused tapes in docs/vhs/) with VHS.
codewhale-hq/Codewhale
Capture screenshots on registered computers, record on macOS or HarmonyOS, and manage saved captures.
bruin-data/bruin
Create, update, render, and visually verify polished Bruin CLI terminal demos with VHS.
mendixlabs/mxcli
Push OData query options into the SQL of a Mendix resource served by a read microflow, so $filter, $orderby, $top, $skip, $count and the key lookup reach the database instead of being silently…
mendixlabs/mxcli
Chart a Mendix app with Vega-Lite through a pluggable widget that takes the specification and the data as separate properties, so the model emits rows and never assembles a chart payload.
mendixlabs/mxcli
Run set-based INSERT, UPDATE and DELETE against Mendix entities through OQL statements, which the runtime supports and Studio Pro cannot author.
mendixlabs/mxcli
Author Mendix AI agent documents in MDL — Model, Knowledge Base, Consumed MCP Service and Agent, with variables, tools and multi-line prompts.
mendixlabs/mxcli
Find out what a running Mendix app actually does — logs, Prometheus metrics, OpenTelemetry traces and the model catalog, joined across sources.
mendixlabs/mxcli
Investigate an existing non-Mendix application (Java, .NET, Python, Node, PHP, …) and produce a structured migration assessment for Mendix.
Record a narrated walkthrough video of a working Mendix app — the human-facing half of the journey/demo pair. Record Narrated Demo is an agent skill from mendixlabs/mxcli. Record a narrated walkthrough video of a working Mendix app — the human-facing half of the journey/demo pair.
Record Narrated Demo fits situations like: produce a walkthrough recording; after the apps end-to-end journey already passes.
Run `npx skills add mendixlabs/mxcli --skill record-narrated-demo -a claude-code`. Or copy the skill folder (.claude/skills/mendix/record-narrated-demo in mendixlabs/mxcli) into .claude/skills/record-narrated-demo in your project. Claude Code loads it when a task matches its description.
Run `npx skills add mendixlabs/mxcli --skill record-narrated-demo -a codex`. Or copy the skill folder (.claude/skills/mendix/record-narrated-demo in mendixlabs/mxcli) into .agents/skills/record-narrated-demo in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add mendixlabs/mxcli --skill record-narrated-demo -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/record-narrated-demo, .gemini/skills/record-narrated-demo, .github/skills/record-narrated-demo and .opencode/skills/record-narrated-demo in your project.
Going by SKILL.md and its folder, Record Narrated Demo needs JavaScript for the scripts in its folder and the command-line tools its instructions call (apt-get and node). Our summary lists: Node.js.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Record Narrated Demo is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 6.8k tokens (SKILL.md is roughly 27k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Record Narrated Demo: Record PR Demo (payloadcms/payload, 45k stars), Record Extension Demo (mengxi-ream/read-frog, 10k stars), Mobile Demo Recording (superset-sh/superset, 15k stars) and Record Demo (apify/mcpc, 990 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
mendixlabs (a GitHub organization) maintains it in mendixlabs/mxcli, which has 129 GitHub stars. The repository holds 72 skills in this directory. The repository was last updated on October 8, 2026.
Source: mendixlabs/mxcli on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.