/onboarding-videos
Add, replace, and optimize the looping demo videos in the onboarding "welcome" bento grid (packages/ui/src/features/onboarding). Covers the ffmpeg compression workflow (the scripts/optimize-onboarding-videos.mjs wrapper), the faststart + right-size best practices that keep
$ npx -y skills add posthog/posthog --skill onboarding-videos --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/onboarding-videos
Context preview
The summary Claude sees to decide when to auto-load this skill.
Add, replace, and optimize the looping demo videos in the onboarding "welcome" bento grid (packages/ui/src/features/onboarding). Covers the ffmpeg compression workflow (the scripts/optimize-onboarding-videos.mjs wrapper), the faststart + right-size best practices that keep
SKILL.md
onboarding-videos.SKILL.mdname: onboarding-videos
description: Add, replace, and optimize the looping demo videos in the onboarding "welcome" bento grid (packages/ui/src/features/onboarding). Covers the ffmpeg compression workflow (the scripts/optimize-onboarding-videos.mjs wrapper), the faststart + right-size best practices that keep playback smooth, the first-frame poster convention, and how to wire a new clip into WelcomeScreen. Use when onboarding demo clips feel laggy, when a clip is being added/swapped/re-recorded, or when posters and videos drift out of sync.
allowed-tools: Bash(node scripts/optimize-onboarding-videos.mjs:*), Bash(pnpm optimize:onboarding-videos:*), Bash(ffmpeg:*), Bash(ffprobe:*), Bash(npx biome check:*), Bash(pnpm --filter @posthog/ui typecheck:*)
Onboarding bento demo videos
The first onboarding step (`WelcomeScreen`) shows a bento grid of feature cards. A card with a `media` entry plays a short, muted, looping screen recording; cards without one show a static placeholder. The featured (large, top-left) card is the only one that plays at a time — hover just moves the highlight.
| Thing | Where | | --- | --- | | Clips + posters | `packages/ui/src/features/onboarding/assets/<feature>-<light\|dark>.{mp4,jpg}` | | Wiring (`MEDIA` map, `startTime`) | `packages/ui/src/features/onboarding/components/WelcomeScreen.tsx` | | `<video>` element + play/seek logic | `packages/ui/src/features/onboarding/components/FeatureBentoCard.tsx` | | Optimizer script | `scripts/optimize-onboarding-videos.mjs` (`pnpm optimize:onboarding-videos`) |
Why clips need optimizing (the "it feels laggy" fix)
Raw screen recordings hitch in this UI for two reasons, both fixed by re-encoding:
1. **`moov` atom at the end of the file.** Without faststart the player must read to EOF before it can start, so first-play and every seek stutter. We seek on mount and on every loop (`FeatureBentoCard` parks the clip on `videoStartTime`), so this bites constantly. Fix: `-movflags +faststart`. 2. **Resolution far larger than it renders.** The featured slot is never wider than ~500 CSS px (the grid is `max-w-[760px]`). Even at 2× retina that's ~1000px, but recordings come in at ~1876px. Decoding huge frames and downscaling them — on every play and during the framer-motion slot reflow — is wasted work. Fix: cap width at **1000px** (which also matches the poster width exactly).
Bitrate/size are usually already modest; **decode cost and startup are the lag**, not bytes. Don't chase file size at the expense of width/faststart.
Canonical encode target
H.264 · `yuv420p` · **≤1000px wide** (keep aspect, even height) · CRF 23 · `+faststart` · no audio. These live as constants at the top of `scripts/optimize-onboarding-videos.mjs` — change them there, not ad-hoc.
Optimize existing clips
pnpm optimize:onboarding-videos # encode any clip not already optimized
pnpm optimize:onboarding-videos --dry-run # show what would change
pnpm optimize:onboarding-videos --force # re-encode all (after changing the target constants)
Requires ffmpeg (`brew install ffmpeg`). The script tags each output with a `comment` marker, so re-runs skip already-optimized files — it's safe to run any time, including right after dropping in a new clip. It rewrites files in place; review the `git diff --stat` and the printed before/after sizes.
Add or replace a clip
1. **Record** light + dark variants at **≥1000px wide**. The optimizer downscales to 1000px but never upscales, so anything narrower ships soft — resolution is the one thing it can't fix for you. Keep it short (~10–13s); it loops. 2. **Name + drop** the files as `assets/<feature>-light.mp4` and `assets/<feature>-dark.mp4`. Two things bite every time:
- Recordings almost always arrive with the **light variant unsuffixed**
(`foo.mp4`, only `foo-dark.mp4` is tagged). Rename it to `foo-light.mp4`.
- The source filename (e.g. whatever's in `~/Downloads`) is irrelevant — the
`<feature>` prefix must be the **media id** you'll use in `WelcomeScreen` and follow the existing convention, not whatever the file was called.
No `assets.d.ts` change needed — `*.mp4`/`*.jpg` are wildcard modules. 3. **Optimize**: `pnpm optimize:onboarding-videos`. 4. **Make the poster** — the still shown before play. **Use the clip's first frame** and leave `startTime` at 0, so the poster, the first played frame, and the loop point are all the same with nothing to keep in sync:
ffmpeg -y -i assets/<feature>-<theme>.mp4 \
-frames:v 1 -vf "scale=1000:-2:flags=lanczos" -q:v 3 \
assets/<feature>-<theme>.jpg5. **Wire it up** in `WelcomeScreen.tsx`. For a *new* media id, four edits, all keyed by the same slug: import the `.mp4` + `.jpg`, add the id to the `MediaId` union, add its entry to the `MEDIA` map (`startTime: 0`), and set `media: "<id>"` on the target `FeatureDef`.
Replacing an existing clip
Keep the **same asset filename** and the wiring is untouched — none of step 5 applies. Drop the new file over `assets/<feature>-<theme>.mp4`, run the optimizer (a fresh drop carries no skip-marker, so it re-encodes without `--force`), and **regenerate that poster** (step 4). If you replace only one theme, re-check that light and dark still share an aspect ratio — a clip with stray padding frames differently from its sibling, and the gap shows when the user toggles theme.
Poster = first frame (and startTime = 0)
`FeatureBentoCard` shows the poster while a card rests, then seeks the `<video>` to `MEDIA[id].startTime` on mount and loops back there (not necessarily to 0). The simple, default contract: **the poster is the clip's first frame and `startTime` is 0**, so the still, the first played frame, and the loop point are all identical — nothing to keep in sync. Keep the poster the same pixel width as the clip (1000px) so the poster→video swap is seamless.
`startTime` can start/loop mid-cl
Read more
name: onboarding-videos description: Add, replace, and optimize the looping demo videos in the onboarding "welcome" bento grid (packages/ui/src/features/onboarding). Covers the ffmpeg compression workflow (the scripts/optimize-onboarding-videos.mjs wrapper), the faststart + right-size best practices that keep playback smooth, the first-frame poster convention, and how to wire a new clip into WelcomeScreen. Use when onboarding demo clips feel laggy, when a clip is being added/swapped/re-recorded, or when posters and videos drift out of sync. allowed-tools: Bash(node scripts/optimize-onboarding-videos.mjs:*), Bash(pnpm optimize:onboarding-videos:*), Bash(ffmpeg:*), Bash(ffprobe:*), Bash(npx biome check:*), Bash(pnpm --filter @posthog/ui typecheck:*)
Onboarding bento demo videos
The first onboarding step (`WelcomeScreen`) shows a bento grid of feature cards. A card with a `media` entry plays a short, muted, looping screen recording; cards without one show a static placeholder. The featured (large, top-left) card is the only one that plays at a time — hover just moves the highlight.
| Thing | Where | | --- | --- | | Clips + posters | `packages/ui/src/features/onboarding/assets/<feature>-<light\|dark>.{mp4,jpg}` | | Wiring (`MEDIA` map, `startTime`) | `packages/ui/src/features/onboarding/components/WelcomeScreen.tsx` | | `<video>` element + play/seek logic | `packages/ui/src/features/onboarding/components/FeatureBentoCard.tsx` | | Optimizer script | `scripts/optimize-onboarding-videos.mjs` (`pnpm optimize:onboarding-videos`) |
Why clips need optimizing (the "it feels laggy" fix)
Raw screen recordings hitch in this UI for two reasons, both fixed by re-encoding:
1. **`moov` atom at the end of the file.** Without faststart the player must read to EOF before it can start, so first-play and every seek stutter. We seek on mount and on every loop (`FeatureBentoCard` parks the clip on `videoStartTime`), so this bites constantly. Fix: `-movflags +faststart`. 2. **Resolution far larger than it renders.** The featured slot is never wider than ~500 CSS px (the grid is `max-w-[760px]`). Even at 2× retina that's ~1000px, but recordings come in at ~1876px. Decoding huge frames and downscaling them — on every play and during the framer-motion slot reflow — is wasted work. Fix: cap width at **1000px** (which also matches the poster width exactly).
Bitrate/size are usually already modest; **decode cost and startup are the lag**, not bytes. Don't chase file size at the expense of width/faststart.
Canonical encode target
H.264 · `yuv420p` · **≤1000px wide** (keep aspect, even height) · CRF 23 · `+faststart` · no audio. These live as constants at the top of `scripts/optimize-onboarding-videos.mjs` — change them there, not ad-hoc.
Optimize existing clips
pnpm optimize:onboarding-videos # encode any clip not already optimized pnpm optimize:onboarding-videos --dry-run # show what would change pnpm optimize:onboarding-videos --force # re-encode all (after changing the target constants)
Requires ffmpeg (`brew install ffmpeg`). The script tags each output with a `comment` marker, so re-runs skip already-optimized files — it's safe to run any time, including right after dropping in a new clip. It rewrites files in place; review the `git diff --stat` and the printed before/after sizes.
Add or replace a clip
1. **Record** light + dark variants at **≥1000px wide**. The optimizer downscales to 1000px but never upscales, so anything narrower ships soft — resolution is the one thing it can't fix for you. Keep it short (~10–13s); it loops. 2. **Name + drop** the files as `assets/<feature>-light.mp4` and `assets/<feature>-dark.mp4`. Two things bite every time:
- Recordings almost always arrive with the **light variant unsuffixed**
(`foo.mp4`, only `foo-dark.mp4` is tagged). Rename it to `foo-light.mp4`.
- The source filename (e.g. whatever's in `~/Downloads`) is irrelevant — the
`<feature>` prefix must be the **media id** you'll use in `WelcomeScreen` and follow the existing convention, not whatever the file was called.
No `assets.d.ts` change needed — `*.mp4`/`*.jpg` are wildcard modules. 3. **Optimize**: `pnpm optimize:onboarding-videos`. 4. **Make the poster** — the still shown before play. **Use the clip's first frame** and leave `startTime` at 0, so the poster, the first played frame, and the loop point are all the same with nothing to keep in sync:
ffmpeg -y -i assets/<feature>-<theme>.mp4 \
-frames:v 1 -vf "scale=1000:-2:flags=lanczos" -q:v 3 \
assets/<feature>-<theme>.jpg5. **Wire it up** in `WelcomeScreen.tsx`. For a *new* media id, four edits, all keyed by the same slug: import the `.mp4` + `.jpg`, add the id to the `MediaId` union, add its entry to the `MEDIA` map (`startTime: 0`), and set `media: "<id>"` on the target `FeatureDef`.
Replacing an existing clip
Keep the **same asset filename** and the wiring is untouched — none of step 5 applies. Drop the new file over `assets/<feature>-<theme>.mp4`, run the optimizer (a fresh drop carries no skip-marker, so it re-encodes without `--force`), and **regenerate that poster** (step 4). If you replace only one theme, re-check that light and dark still share an aspect ratio — a clip with stray padding frames differently from its sibling, and the gap shows when the user toggles theme.
Poster = first frame (and startTime = 0)
`FeatureBentoCard` shows the poster while a card rests, then seeks the `<video>` to `MEDIA[id].startTime` on mount and loops back there (not necessarily to 0). The simple, default contract: **the poster is the clip's first frame and `startTime` is 0**, so the still, the first played frame, and the loop point are all identical — nothing to keep in sync. Keep the poster the same pixel width as the clip (1000px) so the poster→video swap is seamless.
`startTime` can start/loop mid-cl
:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.
Repo: posthog/posthog
Other skills on posthog.
- /analyzing-expensive-users
Analyze the most expensive users in AI observability and explain why they cost so much. Use when the user asks about top spenders, expensive users, per-user LLM cost, user-level cost drivers, or patterns behind high AI observability spend.
Open skill - /creating-online-evaluations
Author continuously-running online evaluations in PostHog AI observability, grounded in real failure modes you've identified. Use when the user wants evaluations that automatically score new generations or whole traces going forward — "create an eval to catch X", "continuously
Open skill - /exploring-ai-failures
Find where an AI/LLM application is failing in production and surface the failure patterns, working from real traces. Use when someone wants to understand what's going wrong with an AI feature, find and categorize failure modes, triage errors, or investigate quality issues
Open skill - /exploring-llm-clusters
Investigate AI observability clusters — understand usage patterns in AI/LLM traffic, compare cluster behavior, compute cost/latency metrics, and drill into individual traces within clusters.
Open skill - /exploring-llm-costs
Investigate LLM spend in PostHog — total cost over time, cost by model, provider, user, trace, or custom dimension, token and cache-hit economics, and cost regressions. Use when the user asks "how much are we spending on LLMs?", "which model / user / feature is most expensive?",
Open skill - /exploring-llm-evaluations
Investigate AI observability evaluations — `hog` (deterministic code-based), `llm_judge` (LLM-prompt-based), and `sentiment` (user-message sentiment). Find existing evaluations, inspect their configuration, run them against specific generations, query individual results, and
Open skill

