Remotion Video Factory
Themed, schema-validated React compositions rendered to MP4s and stills from JSON props, driven end to end by an agent.
Straight off the render queue
What this is
A video factory. React compositions, each with a Zod schema and a theme, rendered to MP4s and PNG stills from JSON props files. h264, 30fps. I don't open a timeline. I hand Claude Code a request, it picks a composition, writes the props, renders, reviews the output, and ships the file.
Ten compositions live in the public repo as of July 2026. Motion: QuoteCard, DataViz, QuadrantVerdict, B4PipelineFlow, B4CadenceWheel, FrontierCrossing, BridgeExplainer, and ScrollShowcase (a 9:16 "scroll through the article" asset). Stills: B4Still and B4Icon. The B4-named ones are real production assets for the B4 Index, left in as worked examples instead of stripped down to hello-world.
Format is a prop, not a separate composition. One registration resolves to square, wide, portrait, post, or banner through calculateMetadata. Same for theme. An agent that wants a 1584x396 banner and a 1080x1920 vertical video is writing two JSON files, not two renderers.
Why it's built this way
I tried the obvious thing first and rejected it. Prompt-to-video generative models produce something new every run, and they garble type. My standing rule on image work is that if a visual needs text, the text gets composited in code with real fonts... a model gets the art layer and nothing else. Video only makes that worse, because now the garble moves.
The other alternative was hand work in After Effects or Canva. Fine for one asset. Useless when the demand is "twelve variants of the same quadrant card, one per category, by Friday," and an agent can't drive either tool anyway.
So: props-driven compositions. Zod on every one, so bad props fail at render time instead of in the output. Each composition file opens with a comment block documenting its props and example prompts, so an agent can use it without reading the implementation. Headlines carry authored line breaks because browser wrap puts the break in the wrong place and the punch line loses its punch.
The build log, failures left in
2026-03-26
Scaffold in one sitting. Remotion plus Tailwind v4, the theme system (buildvsbuy, benroberts, neutral), QuoteCard and DataViz, and a CLAUDE.md of conventions. Then it sat untouched for three months.
2026-07-01
The B4 launch needed its whole visual system in three days, so I sat down and planned it: which compositions, which output formats, and a b4 theme to carry the brand tokens.
2026-07-02
Built most of it in one day... QuadrantVerdict for the verdict videos, the B4Still post template, the b4 theme itself. The same day surfaced three problems I fixed on the spot: empty strings were slipping through the props contract, renders needed a schema guard so bad input failed loudly instead of producing a wrong video, and the still templates had drifted apart enough to consolidate.
The bigger lesson from that day was a rip-out. QuadrantVerdict was registered in Root.tsx with defaultProps including a mover's from block, handy for Studio preview. Two CLI props files omitted from because they wanted the plain variant. Remotion merges defaults with input props key by key, and JSON cannot express "absent," so both renders silently inherited it. The FP&A video told a re-score story that never happened. The Help Desk video animated a dot gliding to itself. Both rendered at 444 frames instead of 360.
The rule that came out of it: never register an optional prop whose presence changes a composition's variant, duration, or narrative. If a rich variant needs Studio preview, give it its own composition id. And run npx remotion compositions src/index.ts --props=<file> first, because a from-less QuadrantVerdict must resolve to 360 frames, and that check costs two seconds.
2026-07-04
The creative reviewer gets a blog-article surface row and mobile-width legibility floors.
2026-07-10 through 07-15
JourneyFlow with stages measured off real analytics, B4PipelineFlow v2 as a flywheel, a source-chip width fix so a publication name stopped truncating.
2026-07-20
Extracted and published, MIT.
⚠ What would break
Remotion's own license. Free for individuals and small teams; companies need a paid license. This factory currently runs on personal-scale terms. Any move into a commercial setting is a license decision before it's an engineering one, and it's the kind of thing that gets discovered late.
Composition sprawl. My working rule is to extract a shared template once the third request with the same shape shows up. But that's a habit, not a guardrail... no build fails when I skip it, and every campaign makes the case for just one more bespoke composition. At ten compositions I can hold the whole library in my head. If I let it drift to forty near-duplicates, choosing the right one becomes its own chore and the factory stops paying for itself.
The creative reviewer is a model, not a test suite. It judges rendered pixels against surface rows and legibility floors. It can pass something ugly, and it can pass something legible that's wrong.
What I learned
The defaultProps bug is the best example I have of AI being confidently wrong in my own work. That same session had already root-caused the identical merge mechanism for B4Still's optional string props and fixed it with the empty-string contract. Fixed, verified, moved on. But from is an object. An empty string can't stand in for it, and the schema is .optional() rather than .nullable(), so null fails validation outright. For a non-string optional prop, absence is inexpressible from a props file once a default is registered. The fix was correct, partial, and reported as complete.
What caught it was adversarial code review, not watching the videos. The visual spot-check passed. Of course it did... the frames were beautiful. Legible, on-brand, animated, and telling a story about a score change that never happened. A rendered asset can look completely finished and still be lying, and no amount of squinting at it will tell you.
The transferable rule: when you fix a bug class for one type, immediately ask which other types the fix doesn't cover. Then go check.
What this demonstrates
Deterministic generation over generative rendering, in a domain where everybody reaches for the generative option first. Interfaces designed so a model can drive them: schema-validated inputs, self-documenting composition headers, one render command. A review gate sitting on the output rather than trust sitting on the process.
The shape generalizes past video. Most creative production an agent could plausibly own looks like this: a constrained surface, validated input, a mechanical render, a judgment pass before anything ships. The hard part was never the animation code.