The renderer worker renders a project from its git repo: it fetches a commit, pulls Git LFS assets, installs dependencies, renders an exported composition on a warm GPU, checks the file with ffprobe and uploads it. It runs as a Docker container on any NVIDIA host, a RunPod pod or a RunPod serverless endpoint. Deploying it: renderer-service README.
The worker has no engine of its own. It renders with the framefields version in the project’s lockfile.
Project checklist
Section titled “Project checklist”A project that follows Getting started already meets most of this. Check every line before submitting a job.
| Rule | Why | Fails with |
|---|---|---|
framefields is a dependency, version 2.2 or newer |
2.2 adds progress, cancel, warm workers and safe video encoders | ENGINE_NOT_FOUND; 2.1 renders but without those |
A lockfile is committed and matches package.json: pnpm-lock.yaml, package-lock.json or yarn.lock |
the worker installs with --frozen-lockfile / npm ci. Bun lockfiles aren’t supported |
LOCKFILE_MISSING, INSTALL_FAILED |
packageManager is pinned, e.g. "pnpm@10.13.1" |
the worker installs with that exact version | install differences |
Install-script approvals are in package.json (only skia-canvas) |
the worker runs the same install as you | Cannot find module '.../skia.node' |
The entry module only builds the film: film.ts exports buildFilm, and render.ts does the rendering |
the worker imports the entry. Rendering or process.exit() at import time is rejected |
EXPORT_SIDE_EFFECT, EXPORT_NOT_FOUND |
Every input file is under assets/, stored with Git LFS, and pushed (git lfs ls-files) |
the worker pulls LFS objects for the commit it renders | LFS_POINTER_UNRESOLVED, LFS_OBJECT_MISSING, ASSET_NOT_FOUND |
Asset paths are built from import.meta.url, never process.cwd() |
the worker’s working directory isn’t your project | ASSET_NOT_FOUND |
Nothing writes into node_modules while rendering |
the worker shares node_modules files read-only between projects |
a permission error (EACCES/EPERM) with a hint |
| Everything is committed and pushed | the worker renders the pushed commit, not your working tree | stale output |
What the export can return
Section titled “What the export can return”// One videoexport async function buildFilm(args?: { locale?: string }): Promise<Composition> { … }
// Several named outputsexport const cuts = () => ({ main: buildFilm(), teaser: buildTeaser() });
// A plan, with settings per outputexport const deliverables = () => ({ outputs: [ { name: "film", composition: buildFilm() }, { name: "sheet", composition: buildFilm(), render: { kind: "frameGrid", grid: { step: 30 } } }, ],});This is the same { entry, export } convention as startPreview. target.args is passed to a function export. target.select picks names from a record or plan.
Submit a job
Section titled “Submit a job”curl -s "$WORKER/v2/jobs" -H "authorization: Bearer $RENDERER_API_KEY" -H "content-type: application/json" -d '{ "version": 2, "source": { "repo": "https://github.com/acme/films.git", "ref": "main", "workdir": "videos/launch" }, "target": { "entry": "src/film.ts", "export": "buildFilm" }, "render": { "kind": "video", "codec": "h264", "quality": "high" }, "output": { "type": "s3", "key": "launch/{shortSha}/{name}.{ext}" }}'# → {"id":"…","status":"IN_QUEUE"}| Call | Returns |
|---|---|
GET /v2/jobs/<id> |
status and result: output URLs or keys, the rendered commit SHA, timings, GPU |
GET /v2/jobs/<id>/events |
live progress (Server-Sent Events) |
POST /v2/jobs/<id>/cancel |
stops the render between frames |
On RunPod serverless, send the same JSON as {"input": …} to https://api.runpod.ai/v2/<endpoint-id>/run. Then poll /status/<id>, which includes progress.
Useful job fields:
| Field | Use |
|---|---|
source.ref |
branch, tag or commit SHA. The result reports the SHA it rendered |
source.commit |
fail if the ref has moved past this commit |
source.workdir |
the package folder, for a monorepo |
source.lfs.include |
pull only the assets this render needs |
render.kind |
video (h264, vp9, vp8, gif), audio (aac, mp3, opus), frames, frameGrid, lut |
render.qa.failOn |
QA codes that fail the job (QA_FAILED). The outputs still upload |
output.type |
s3 (key template), presigned (one URL per output), local, inline (base64) |
idempotencyKey |
a retry of the same commit returns the earlier upload |
Key templates can use {jobId}, {sha}, {shortSha}, {ref}, {date}, {name}, {index} and {ext}.
Errors
Section titled “Errors”A failed job returns { code, message, phase, retryable, hint }. When retryable is true, resubmit the same job. Otherwise, fix the cause first:
| Code | Fix |
|---|---|
LOCKFILE_MISSING, INSTALL_FAILED |
commit a lockfile that matches package.json |
EXPORT_NOT_FOUND, EXPORT_INVALID, EXPORT_SIDE_EFFECT |
export a builder from the entry; render only in render.ts |
LFS_POINTER_UNRESOLVED, ASSET_NOT_FOUND |
push the LFS object, or widen source.lfs.include |
REPO_NOT_ALLOWED, GIT_AUTH_FAILED, REPO_NOT_FOUND |
the worker’s allowlist or git token doesn’t cover the repo |
REF_NOT_FOUND, REF_AMBIGUOUS |
push the branch, or pass a SHA |
ENGINE_SWITCH (retryable) |
the worker loaded another framefields version and restarts. Keep projects on one version per worker to avoid it |
GPU_UNAVAILABLE (retryable) |
the host’s GPU isn’t usable for WebGPU. Another worker takes the retry |
RENDER_TIMEOUT |
raise timeoutSec, or the endpoint’s execution timeout on RunPod |
Hardware
Section titled “Hardware”- Drawing frames. Frames are drawn with WebGPU on Vulkan. The
FF_GPUsetting:autouses the GPU, or the CPU (lavapipe, much slower) when there is no GPU;requirerefuses to run without a GPU (used by the RunPod templates);cpualways draws on the CPU. - Vision. Vision models run on the same GPU through WebGPU. The log line
[vision] Inference on the GPU (WebGPU)confirms it. - Video encoding. framefields encodes with NVENC when it works and falls back to libx264/libx265/libvpx/SVT-AV1 otherwise. NVENC needs NVIDIA driver 610 or newer. A100 and H100 have no NVENC.
- Picking a GPU. RTX/L4/L40S-class cards suit rendering. AI-training GPUs cost more and encode on the CPU.