Compose beta
Compose turns a local JSX composition into a product video (MP4 + WebM + poster), uploads it for human review, and serves the approved render from a durable CDN URL — the same "only flips after approval" guarantee as screenshots, for video.
Prerequisites
- The Reshot CLI:
npm i -D @reshotdev/screenshot(ornpx @reshotdev/screenshot). - ffmpeg on your
PATH— the renderer transcodes frames to MP4/WebM with it. - A project + publish token (from onboarding, or Settings → Integrations).
The three files of a composition
reshot compose renders from three inputs that live next to each other:
| File | What it is | How you get it |
|---|---|---|
<name>.compose.tsx | The JSX scene — overlays, labels, timing | Authored by you (scaffolded by reshot record) |
<name>.metadata.json | Required sibling — the workflow timeline, capture size, and target rects | Written by reshot record alongside the capture |
capture video (e.g. login-capture.mp4) | The screen recording the scene annotates, referenced by capturePath | Produced by reshot record |
The
<name>.metadata.jsonsibling is mandatory.reshot compose Foo.compose.tsxreadsFoo.metadata.jsonfor the timeline/targets and errors if it is missing.
Authoring a scene
A composition imports from @reshot/compose and wraps a <ProductFilm> in a
<Composition>:
You don't
npm install @reshot/compose— the CLI bundles the render engine and resolves that import for you whenreshot composebuilds your scene. The import is for authoring only. Keep your.compose.tsx,.metadata.json, and capture video in the same folder; don'timport "./x.metadata.json"into the scene — the CLI bundles the.compose.tsxfrom a temp dir, so a relative file import won't resolve. Pass the timeline via the inlineworkflowobject above.
import { Composition, ProductFilm } from "@reshot/compose";
const capturePath = "./login-capture.mp4";
const workflow = {
durationMs: 9203,
capturePath,
captureSize: { width: 1440, height: 900 },
timeline: [
{ type: "queue_visible", tMs: 0 },
{ type: "hero_approve", tMs: 3000 },
{ type: "workflow_end", tMs: 9203 },
],
targets: {
version_history: { x: 1060, y: 381, w: 347, h: 125 },
},
};
export default function LoginHero() {
return (
<Composition workflow={workflow} slug="LoginHero" capturePath={capturePath}>
<ProductFilm
src={capturePath}
steps={[
{
id: "review-history",
at: "queue_visible",
until: "hero_approve",
target: "version_history",
label: "Version history stays in context",
},
]}
/>
</Composition>
);
}Each step anchors a callout to a target rect between two timeline events
(at → until); tone: "success" | "warning" colors it.
Render → push → review → deliver
# 1. Capture the workflow (writes the capture mp4 + <name>.metadata.json + a scaffold)
reshot record "Login hero"
# 2. Render locally to MP4 + WebM + poster
reshot compose LoginHero.compose.tsx
# → LoginHero.composed.mp4 / .webm / .webp
# 3. Upload for review (lands as PENDING — nothing serves until a human approves)
reshot compose push LoginHero.compose.tsx --project <projectId>After push, the render appears in the review queue next to your screenshots. Approve it and the durable URL flips to the new video.
Delivery URLs
| URL | Serves |
|---|---|
reshot.dev/public/c/<projectSlug>/<compositionSlug>/latest.mp4 | The current approved render (self-updating) |
…/latest.webm, …/poster.webp | WebM + poster of the approved render |
…/v/<renderId>.mp4 | A version-pinned render that never changes |
/public/c/<projectSlug>/<compositionSlug> | An embeddable player page |
Before the first approval these return 404 — an unapproved render never serves.
CI auto-approve
In a trusted pipeline you can skip manual review:
reshot compose push LoginHero.compose.tsx --project <projectId> --auto-approve--auto-approve promotes the render to live immediately. It consumes a review
approval, so it honors your plan's approval allowance — on the Free plan, once
you've used your included approvals the render is left PENDING for review
instead of bypassing the limit.

