How I built an animated product video systemThe Claude Code harness, Remotion DSL, prompts, and review process behind Sim Academy's animated explainers.
How the system works
I built a nearly fully automated explainer-video production system for Sim Academy. Claude Code agents operate a production harness and write animations through a TypeScript DSL I designed on top of Remotion.
Agents explore competing concepts, build complete films in parallel, review them, and combine the strongest parts. They also draft narration and generate voiceover. I direct the explanation and screen the results, sometimes delegating comparison and selection to the lead agent.
Sim is a visual builder for AI workflows. The DSL represents its blocks, configuration, and execution as a shared world, so a run can drive the canvas, chat, and logs together. Remotion renders the React components at each frame.
How you use it
I give Claude Code the product behavior to explain, the audience, and the intended understanding. The agent reads the repository's instructions, product references, and annotated examples before proposing a visual script.
A request can be informal because the session already contains that context. This was my request for alternatives to the invoice film. “PH” refers to an earlier Product Hunt video:
btw I think we should be showing an email inbox opening up and an attachment like we did the PH video, we need to show an zoom animation / flyby of showing the
inbox opening an inbox / -- i think you need to help me ideate 6 variations of this video by trying lots of e2e ideas using subagents pleaseThe agent turns that request into a brief and dispatches complete builds. I review the MP4s, give corrections, and ask for another variation or a final synthesis. Source, narration, and generated audio remain with each film so the same process can revise it later.
The phases of production: diverge and converge
The shared brief fixes the product mechanics and teaching goal. Candidates vary how they explain it.
- Research and brief. Establish the actual workflow, example data, and claims each beat must communicate.
- Diverge. Give separate agents distinct concepts and isolated Git worktrees. Each produces a complete film so its pacing and continuity can be judged.
- Build. Establish static layouts and camera framings, animate them, then write narration and generate audio. The order is concept, visuals, narration, audio.
- Review. Inspect frames and footage, run checks, and return concrete defects to the builder.
- Converge. Select a base film and useful contributions from other candidates. Rewrite them into one dataset, layout, and timeline, then review the combined result.
The invoice production explored six candidates. I approved the wave and delegated the final selection; the agent combined the inbox/PDF structure with other candidates' cursor behavior, adjacent-row comparison, and Logs ending.
A selected scene may use different values or assume an earlier action. Synthesis reconciles those dependencies in source code. The repository's synthesis template makes the selection explicit:
Build THE SYNTHESIS video: <slug>. I reviewed N candidates and specced the
exact recipe — this is the convergence cut, not another experiment.
THE RECIPE (my verdicts, follow exactly):
- <element> = <winning-lane>'s (quote the director verdict on why it won)
- <element> = <other-lane>'s …
- <weak beat> = REBUILT: <the fix>
Mine `git show origin/<branch>:<path>` for each borrowed element.
<fixed spec + the locked sequence + verbatim captions if any>How mistakes are caught
The system uses several kinds of checks because valid code can still produce a misleading film.
| Check | What it catches |
|---|---|
| Shared geometry and timing | Containers too small for their contents, detached connection anchors, block reveals preceding their clicks. |
| Types and validation | Unknown objects, invalid references, nonexistent highlight targets, configuration/schema mismatches. |
| Film audits | Overlapping interactions, input before a panel settles, possible dead holds. |
| Rendered stills | Cropping, unreadable fields, panels covering the action. |
| Boundary comparison | Unintended visual changes at joins meant to remain continuous. |
| Playback and screening | Weak explanations, poor pacing, incorrect emphasis, motion that passes static checks but teaches little. |
The early boundary verifier caught a highlight that had not faded, changing 4,140 pixels across a cut. Another build passed static checks but used mostly binary state changes and looked like a slideshow.
Mechanical failures can become library constraints. Adding a block now derives the search, cursor click, palette dismissal, block reveal, and editor focus from one operation. The block's reveal time follows its click time.
The review prompt sends one batch of timestamped repairs back to the existing builder:
FIX ROUND <n> on <slug> (you built it — same worktree/branch <branch>).
Frames read, findings (each = timestamp → what's wrong → the fix):
- <t>s: <defect> → <derived fix, name the construct/constant if there is one>
- <t>s: <defect> → <fix>
…
Re-still each fixed beat and READ it before re-render. bun run lint +
bun scripts/anim-lint.ts clean. Re-render → same mp4 path. Reply ≤6 lines:
what changed per finding, any finding you disagree with + why, mp4+branch.That is a historical template. The preserved linter's default discovery can miss later DSL films, so its clean result does not establish full audit coverage. Checks must target the intended film, and the repaired motion still needs playback review.
How VO sync works
Narration describes the authored visuals. scenes.ts defines scene names and minimum visual durations; narration.ts associates speech with those names.
The vo-sync script generates an ElevenLabs clip per scene and measures its duration. A hash of text, voice, model, and settings lets unchanged scenes reuse accepted takes.
Each scene gets the longer of its visual minimum or its audio plus padding, rounded up to a tenth of a second:
const durationSec = Math.max(
s.minSec,
Math.ceil((audioSec + pad) * 10) / 10,
);With the default 0.7-second padding, a 14.237-second clip extends a nine-second visual minimum to fifteen seconds. Generated timing and the audio manifest move the next scene and clip to the same new start.
This aligns scene boundaries, not individual words. An action at two seconds stays there unless its timing is explicitly revised. Duration-relative camera moves can absorb a longer read; smaller beats and playback review keep the key actions with the relevant speech.
Components
The DSL and how it was designed
The DSL is an ordinary TypeScript library. Its declarations separate what exists, what happens, and where the viewer looks:
| API | What it owns |
|---|---|
defineWorld | Named blocks, configuration rows, references, example outputs, and layout derived from product geometry. |
scheduleRun | Block start/finish times, dependency waits, wire travel, and downstream resolution. |
simulate | State at a requested time, derived from scheduled events. |
buildScript / workspaceScript | Construction actions and their cursor, editor, and panel behavior. |
planFilm | Beat claims, durations, camera direction, and cumulative starts. |
highlight / marksAt | Emphasis on named objects or fields, using their actual geometry. |
A run's finish time supplies the completion ring, outgoing pulse, downstream value, and log entry. Maintaining one schedule removes timing values an agent would otherwise have to keep consistent.
The simulation computes state from events up to the requested time. Remotion can render frames out of order, so a frame cannot depend on having rendered the previous frame first.
World processes use film time. Camera and highlight direction can use time local to a beat. In this source excerpt, W and plan are declared earlier:
const coldRun = scheduleRun(W, {
t0: plan.at("the-workflow", 0.8),
});
const References: React.FC = () => {
const {t, tFilm, dur} = useBeatClock(plan, "references");
const marks = marksAt(W, [
highlight(
[row("answer", "Messages"), block("retrieve")],
{in: [2.2, 0.5], out: [dur - 1.4, 0.9]},
),
], dur);
return <Stage
world={W}
cam={plan.cam("references")(t)}
tFilm={tFilm}
run={coldRun.at(tFilm)}
marks={marks(t)}
/>;
};coldRun.at(tFilm) preserves the run across cuts. marks(t) starts a highlight window within the current beat. The author names the Messages row and its source block; the renderer supplies their positions and shared emphasis.
These are authored demonstrations. Invoice contents and responses come from film data, and some final log values are authored too. The renderer does not execute business APIs to discover the outputs. Ordinary React remains available for scenes outside the DSL, with fewer built-in guarantees.
I designed the language from successful films and repeated failures. Fable, a model I temporarily had access to through Claude Code, supplied many of the strongest early examples. When access ended in June, I tried to preserve its decisions through annotations and components that Opus could reuse.
Components helped, but Opus could copy a film's overall structure and still show a table changing without a write operation causing it. Successful Fable examples also contained hand-positioned overlays and independently timed elements that other agents copied incorrectly.
When Fable access returned in July, new films and their traces supplied more examples. I asked for the shared model directly:
i think I want a "simulation engine" is the thing -- it should basically run the model, logs renderer of that state model happens in real time, chat of that happens in real time as well if u know what i mean. is that a lot to ask for? basically an animatable sim-world. I think it's mainly state management / reducer with actions wdytDistillation meant moving those relationships into examples, instructions, and software. It involved no model-weight training. The DSL turned “highlight this row” into a reference to existing geometry and made workflow views consume common state.
On July 7, nine agents built the same Slack IT-triage brief, three each using Sonnet, Opus, and Fable, without VO. I judged Sonnet's films comparable. All three tiers could still miscoordinate a click and block reveal; that defect became the shared add-block sequence. The comparison established useful fixed-brief execution, while cheaper-model original ideation remained untested.
Components of a good explainer video
Each beat needs a visible claim. A second run needs another teaching purpose, and the camera needs a reason to move.
Visual pointing makes a relationship visible through synchronized emphasis. A reference field and its producing block light together; the reference turns into its resolved value where it already sits.
Controlled comparison holds enough constant to identify the change. A memory explainer repeats the same question with memory off and on. In the invoice film, the manual and automatic rows have different invoice values but share the same fields and clerical task.
Provenance connects a result to the operation that produced it. Continuity keeps the same objects and layout recognizable while attention moves between them.
Behavior before anatomy gives the mechanism something to explain. The invoice film establishes manual entry before building the workflow, then returns to the inbox to show invoices being processed while the cursor remains still.
Narration supplies the names and reasons that the picture cannot convey. A claim string or a motion audit cannot judge whether this explanation works; the rendered sequence is the evidence for review.
Supporting files
A new agent reads the repository's instructions and relevant accepted and rejected examples. Film-specific files preserve the data and timing it must keep consistent.
| Files | Responsibility |
|---|---|
CLAUDE.md / AGENTS.md | Production order, repository conventions, product constraints, review requirements. |
| Product source and schema references | Real interface components, field names, options, and legal workflow behavior. |
| Notes and visual scripts | Viewer confusion, beat claims, visible actions, and source grounding. |
World data, layout, Video.tsx | Shared objects, schedules, geometry, and rendering. |
scenes.ts, narration.ts | Ordered beats, visual minima, and corresponding speech. |
_generated/vo-timing.ts, audio manifest and MP3s | Measured durations, cumulative offsets, and cached takes. |
Accepted critiques, REJECTED.md, case studies | Decisions and failure examples future agents need to read. |
Examples must change when the API changes. Otherwise an agent can copy the fragile technique from a recommended film even after a safer operation exists.
The public creator repository preserves the earlier component-based harness and annotated examples. It predates the mature Academy DSL.
The prompts and subagents
The production skills divide the work:
| Skill | Responsibility |
|---|---|
sim-video-ideate | Explore distinct concepts. |
sim-video-brief | Specify the story, workflow, claims, and constraints. |
sim-video-fleet | Dispatch candidates and organize refinement and synthesis. |
sim-video-review | Inspect rendered work and return specific repairs. |
The video-builder subagent owns one complete candidate. Its system prompt supplies the shared contract:
You build ONE complete Sim explainer video end-to-end, in your OWN worktree,
from the CONCEPT brief you were handed. You have zero session context beyond
this prompt — everything you need is in the repo. This system prompt is the
INVARIANT: the conventions, the build order, the verify gates, and the report
format that every lane shares so that lanes are comparable and recomposable.The fleet adds the variable concept. The following is the opening of the repository template; setup and verification instructions follow it:
Build ONE complete Sim video end-to-end: variant <slug> — "<CONCEPT NAME>".
CONCEPT: <2–4 sentences describing THIS staging's distinct visual metaphor,
camera language, and structural idea. Make it genuinely different from the
siblings — a different lens on the same idea, not a re-skin.>A separate engine-folder agent turns repeated implementation into shared operations. Its task template requires proving the change on an accepted film:
ENGINE FOLD: <what becomes grammar>. Precision over speed; kill DOFs, derive.
READ FIRST: <the spec doc + the reference implementations by branch>.
THE FOLD: <enumerated API + the laws it should enforce + the lint check that
makes the old bug unrepresentable>. Back-compat: raw props still work, zero
breaking changes.
ACID TEST: re-point ONE golden onto the new construct — its code must get
SHORTER with identical-or-better frames (still-gate before/after, READ the
pairs). Re-render. Refresh the golden.
REPLY: API shipped (signatures), what was mis-ordered before, lines saved in
the acid test, before/after verdict, branch.The builder fixes the current film. The engine-folder updates the operation, its checks, and the example future builders will copy.