Generate 1,000 personalized videos from a CSV
One template, one CSV, one thousand MP4s. The pattern, the cost model, and the GitHub Actions matrix.
The most common request from marketing teams who learn about deterministic video rendering: "Can we send a personalized MP4 to every name on our list?" The answer, with a CSV and a template, is yes — and the cost is roughly $4 per thousand at our infrastructure prices, not the $4 per video you would pay a freelancer.
Here is the pattern that makes it work in CI, without spinning up a custom service.
The shape of the data
A flat CSV. One row per recipient. Columns become template variables.
name,company,metric,delta
Jordan,Acme,MRR,24.6
Priya,Globex,signups,38.1
Marcus,Initech,activations,17.2Three rules:
- Keep it flat. If you need nested data, denormalize.
- UTF-8 with no BOM. Most CSV tools do the right thing; some do not.
- Header row is canonical. The template references
{{$NAME}}, not column index 0.
The template
A single HTML file with {{$VAR}} placeholders. We covered the personalized card pattern for the on-demand case; the batch case uses the exact same template.
The template is the variable surface. If the brand changes the layout, you change it here. If the brand changes the recipient list, you change the CSV. Neither change affects the other.
The renderer loop
The skeleton, in any language:
import { renderHtmlToMp4 } from '@hyperframes/sdk';
import { parse } from 'csv-parse/sync';
import fs from 'node:fs';
const csv = fs.readFileSync('recipients.csv');
const rows = parse(csv, { columns: true });
await Promise.all(rows.map(async (row) => {
const html = template
.replace(/\{\{\$NAME\}\}/g, row.name)
.replace(/\{\{\$COMPANY\}\}/g, row.company)
.replace(/\{\{\$METRIC\}\}/g, row.metric)
.replace(/\{\{\$DELTA\}\}/g, row.delta);
const mp4 = await renderHtmlToMp4(html, { width: 1920, height: 1080, duration: 6 });
await fs.promises.writeFile(`out/${row.name}.mp4`, mp4);
}));Two notes:
- Replace, not template-engine. Avoid Handlebars / Mustache here; their parsers do too much. A regex replace is faster, simpler, and predictable.
Promise.allis too aggressive. For 1,000 rows, you want a concurrency limit (8-32 depending on your worker size). Usep-limitor the equivalent in your runtime.
The concurrency bound
A single Chromium instance can drive one render at a time. The bottleneck is process count, not CPU — each render uses ~400MB of RAM. The math on a typical CI worker:
For larger batches, parallelize across GitHub Actions matrix jobs. Each runner gets a slice of the CSV; outputs accumulate in S3.
The variable preview
What changes per row. Drag the knobs to see the per-recipient variation:
Cost per render
Real numbers from our infrastructure for a 6-second, 1080p, 30fps render:
- Compute: ~$0.003 per render (GitHub Actions Large runner, amortized)
- Storage: ~$0.0005 per render (S3 standard, 1 month retention)
- Egress: ~$0.0001 per delivered view
A 1,000-recipient campaign costs about $4 to produce and store. The freelancer rate for personalized video is $1-5 per output, so the pattern is roughly three orders of magnitude cheaper. See our 10K variants overnight piece for the full breakdown.
Distribution
Once you have 1,000 MP4s in S3, the delivery options:
- Email. A signed
<video>URL or an animated GIF preview that links to the MP4. Most clients block autoplay; use a poster image. - Personalized landing pages. A URL per recipient (
/v/{token}) that loads their MP4. Easy to instrument click-through. - Direct DMs. Slack/LinkedIn message with the video attached.
Pick one. Most teams overcomplicate this; a single delivery surface is usually right.
What this enables
The line between "marketing video" and "marketing email" used to be that emails were personalized and videos were not. Once videos are also a function call over a CSV, the line goes away. Every email can have a 6-second MP4 in it that says the recipient's name. The novelty wears off in a few quarters; the engagement lift does not.
If you want to wire this into your CI today, the GitHub Actions integration has a copy-paste matrix workflow. Pair with the marketing use case for examples in the wild.
The CSV is the spec; the template is the contract; the MP4s are the build artifacts. Same pattern that made deterministic builds eat the software industry. It is eating video next.
Cite this postBibTeX · APA · Markdown
@misc{tanaka2026generate,
author = {Kira Tanaka},
title = {Generate 1,000 personalized videos from a CSV},
year = {2026},
url = {https://hyperframes.video/blog/batch-personalized-videos-from-csv},
note = {HyperFrames blog}
}Kira Tanaka. (2026, May 14). Generate 1,000 personalized videos from a CSV. HyperFrames. https://hyperframes.video/blog/batch-personalized-videos-from-csv
[Generate 1,000 personalized videos from a CSV](https://hyperframes.video/blog/batch-personalized-videos-from-csv) — Kira Tanaka, 2026
Kira works on the render core: headless Chromium scheduling, frame capture, and the encoder pipeline. She cares about reproducible builds and small numbers next to the word "variance."
A birthday card video generator (per-recipient, from CSV)
Per-recipient birthday cards from a CSV. One HTML template, hundreds of personalized MP4s, sent at scale.
Animated meme generator (deterministic, scriptable)
Build a scriptable meme video generator in HTML — top-text bottom-text reveal, punchline punch-scale, shaky-cam emphasis — and render reproducible MP4s from a CSV.
An animated invoice summary video (the per-customer billing recap)
Send customers a 20-second recap of their monthly invoice — top metrics, charges, savings — rendered from their data. Quietly more memorable than an email.
Building with HyperFrames? Come hang out.
We're on GitHub, in Discord, and the playground is one click away. Bring weird ideas — we collect them.