How it works
- Step 1Fork the blueprint — Open this workflow and click Fork. JAD copies the two-node chain (video-transcoder → cloudflare-r2) into a new private draft owned by you, snapshots it as version 1, and wires the transcoder's
outport to the R2 node'sinport automatically. - Step 2Pair a runner — The R2 node is a connector — it only executes on a paired
@jadapps/runner. Start your runner and confirm it shows green in the orchestrator status pill. Without a runner the upload step throws a clear 'requires a paired runner' error. - Step 3Add R2 credentials on the runner — Store a credential under the ref
r2-prodholding accessKeyId, secretAccessKey, and accountId. The runner keeps these locally; JAD's server never sees the keys and derives the R2 endpoint from the accountId. - Step 4Configure the transcode output — Open the video-transcoder node and pick your output container/codec (default H.264 + AAC MP4). Transcode runs in your browser, so a Pro + Media plan is required for hardware-accelerated WebCodecs and large files.
- Step 5Set the destination bucket and key — In the R2 node set
actionto putObject, fill inbucket, setkey(defaultvideos/output.mp4), andcontentTypetovideo/mp4. Use a per-file key template if you batch, or the default key will overwrite each run. - Step 6Run on one file, then schedule or batch — Drop a single video to test the full chain end to end. To process many, wrap the chain in a For-Each over a folder; to automate, add a cron schedule (the workflow ships unscheduled —
scheduleCronis null) or trigger via webhook.
The real node chain
Exactly what this blueprint wires, in order, from lib/orchestrator/seo-workflow-blueprints.ts. The transcoder's video out port connects to the R2 node's wildcard in port.
| # | Tool node | Category / runs where | Key config | Port out → in |
|---|---|---|---|---|
| 1 | video-transcoder | video · browser (FFmpeg WASM / WebCodecs) | {} (defaults to H.264 + AAC MP4) | video → * |
| 2 | cloudflare-r2 | connector · paired runner only | action: putObject · key: videos/output.mp4 · contentType: video/mp4 · credentialRef: r2-prod | file → (sink) |
Trigger, credential, and tier matrix
Mechanics verified against the orchestrator runtime (workflow-runner, tool-executor, tier-precheck, cron tick, from-blueprint route).
| Aspect | This workflow | How it works |
|---|---|---|
| Default trigger | Manual / webhook | Blueprint ships scheduleCron: null — no cron until you add one |
| Scheduling | Optional cron | Set a 5-field POSIX cron; the cron tick (Cloudflare Cron Triggers) enqueues a run when due |
| R2 credentials | credentialRef r2-prod | accessKeyId + secretAccessKey + accountId stored on the runner, never in the graph or on JAD's server |
| Required tier | Pro + Media | Strictest minTier across nodes: video-transcoder is pro_media; cloudflare-r2 is pro |
| Forking | Any signed-in user | from-blueprint route copies the graph, snapshots v1, writes a forked audit event |
| Audit | Server-side | forked / run_triggered / run_cancelled written to workflow_audit_events |
Transcode tier limits (browser pipeline)
Source-file size handling for the video-transcoder node, from the video processor's pipeline rules. The transcode is what gates this workflow to Pro + Media.
| Tier | Max source size | Pipeline |
|---|---|---|
| Free | Not supported here | ≤ 10 MB RAM-loaded, but the chain needs pro_media to run |
| Pro | Up to 500 MB | RAM-loaded, hardware acceleration when available |
| Pro + Media | Unlimited | Chunks streamed via JAD-Stream; required for this workflow |
Cookbook
Five concrete recipes for the transcode → R2 chain. Each shows the relevant node config (config keys map 1:1 to the orchestrator tool registry); unknown keys are silently ignored, so a recipe shipped before a config option exists still wires the correct chain.
Web-ready MP4 to a public bucket
ExampleThe default: transcode any source to H.264 + AAC and put it at videos/output.mp4. Set the bucket to one with a public domain so the returned object is embeddable.
video-transcoder:
config: {} # defaults: H.264 + AAC, .mp4 container
cloudflare-r2:
action: putObject
bucket: my-public-media
key: videos/output.mp4
contentType: video/mp4
credentialRef: r2-prodUnique key per run (no overwrite)
ExampleThe blueprint's default key is constant, so every run overwrites videos/output.mp4. Give each upload a distinct key prefix/name so historical assets survive.
cloudflare-r2: action: putObject bucket: my-public-media key: videos/2026/demo-launch-v3.mp4 contentType: video/mp4 credentialRef: r2-prod
Batch a folder with For-Each
ExampleWrap the two-node chain in a logic.for-each over a dropped folder. The for-each pulls items, runs the body per item (concurrency 1-32), and emits a JSON array of results on its done port.
logic.for-each:
config: { concurrency: 4 }
body:
- video-transcoder: {}
- cloudflare-r2:
action: putObject
bucket: my-public-media
key: videos/${item.name}.mp4 # template per item
contentType: video/mp4
credentialRef: r2-prodResilient upload with retry
ExampleNetwork blips to R2 shouldn't fail a long transcode. Set the R2 node's errorPolicy to retry — the runner re-attempts the putObject up to 3 times with 200ms then 400ms backoff before the run fails.
cloudflare-r2: errorPolicy: retry # 3 attempts, 200ms / 400ms backoff action: putObject bucket: my-public-media key: videos/output.mp4 contentType: video/mp4 credentialRef: r2-prod
Nightly scheduled drop
ExampleTurn the manual chain into an automated one. Add a cron expression; the cron tick scans due workflows and enqueues a run. Pair it with a webhook or a watched source so there's a new file to process.
# Workflow metadata (not node config) scheduleCron: "0 2 * * *" # daily at 02:00 UTC # 5-field POSIX: minute hour dom month dow # Supported: * N A-B A,B,C */N # Not supported: @hourly, seconds, named days/months, L/W/#
Edge cases and verbatim errors
No runner paired for the R2 upload
Error: requires a paired runnerThe transcode runs in the browser, but cloudflare-r2 is a connector. If no @jadapps/runner is paired (or the browser falls back), the connector handler throws cloudflare-r2 is a connector and requires a paired @jadapps/runner. Configure one in the orchestrator status pill. Start a runner before running.
Missing or wrong credentialRef
failIf the ref r2-prod doesn't exist on the runner, or its data lacks accessKeyId / secretAccessKey / accountId, SigV4 signing fails and the putObject errors. The endpoint is derived from accountId, so an absent accountId means the request has nowhere to go. Re-store the credential on the runner.
Free or Pro tier user runs the workflow
403 tier gateTier precheck returns the strictest minTier across the graph. video-transcoder is pro_media, so the precheck reports 'Pro + Media' required and blocks the run with a single upgrade CTA rather than failing mid-chain. Upgrade or the run won't start.
Empty or non-video source dropped
Error: no inputThe video handler calls ensureFile — with no file and no text it throws No input for video tool. A non-video file (e.g. a PDF) fails the metadata probe and the FFmpeg decode, surfacing a transcode error before anything reaches R2.
Source larger than the tier limit
rejectPro caps transcode source at 500 MB (RAM-loaded). Files above that need the Pro + Media streaming pipeline (JAD-Stream, unlimited). On Pro, an oversized source is rejected by the video file-limit check before encoding starts.
Default key overwrites previous uploads
Silent overwrite (200)The blueprint key is the constant videos/output.mp4. Every run putObjects to the same key, so R2 overwrites the prior object — no error, but yesterday's video is gone. Use a templated/unique key (see cookbook) to keep history.
Bucket has no public domain configured
Upload OK, link not publicputObject succeeds (200) but the object isn't reachable via a public URL unless the bucket has a public custom domain or R2.dev access enabled. The connector stores the object; it does not configure bucket-level public access for you.
Cron added but workflow never fires
Skipped: not dueThe cron tick only fires expressions it understands (5-field POSIX). Nicknames like @hourly, seconds, named days/months, and L/W/# are unsupported and either skip as 'not due' or record a parse error in last_fire_error. Use a plain numeric expression like 0 2 * * *.
Transcode succeeds but downstream sees no input
skippedIf the transcoder node errors with errorPolicy skip, it emits no output; the R2 node then has no input on its port and the runner marks it skipped rather than uploading a stale or empty object. Check the transcode step's error before assuming the upload failed.
Forking with a tool the registry doesn't know
422 blueprint_empty / missingThe from-blueprint route resolves each chain slug against the tool-ports registry. Both video-transcoder and cloudflare-r2 resolve, so this blueprint forks cleanly. If a slug were unknown it'd be reported in missing and, if none resolved, the fork returns 422 blueprint_empty.
Frequently asked questions
Does the transcode really run on a server runner?
No — and this corrects a common misconception. The transcode step (video-transcoder) runs in your browser via FFmpeg WASM / WebCodecs through the orchestrator's video handler. Your source never leaves your machine to be re-encoded. Only the second node, the Cloudflare R2 upload, runs on a paired runner because it's a connector that needs your stored credentials and network egress.
Why does the workflow need a runner at all, then?
Because cloudflare-r2 is a connector. Connectors execute exclusively on a paired @jadapps/runner so your R2 keys stay on your own machine and the SigV4-signed request goes out from there. If no runner is paired the upload node throws a clear 'requires a paired runner' error.
Where are my R2 access keys stored?
On the runner, under the credentialRef r2-prod, holding accessKeyId, secretAccessKey, and accountId. They are never written into the workflow graph and JAD's server never sees them — the R2 endpoint is derived from the accountId on the runner side.
Is this workflow cron-scheduled out of the box?
No. The blueprint ships with scheduleCron: null, so it's a manual or webhook-triggered chain. Cron is a real, separate capability: add a 5-field POSIX expression and the cron tick (wired via Cloudflare Cron Triggers) enqueues a run whenever it's due.
What tier do I need?
Pro + Media. Tier precheck takes the strictest minTier across the graph; video-transcoder is pro_media (hardware encode + streaming) and cloudflare-r2 is pro, so pro_media wins. Free and Pro users are blocked with a single upgrade prompt before the run starts.
How big can the source video be?
It depends on the transcode tier. Pro RAM-loads up to 500 MB. Pro + Media streams chunks via JAD-Stream with no size cap, which is why this workflow is gated to Pro + Media. Free's 10 MB ceiling is moot here because the workflow can't run on Free.
What output format does it produce?
By default the transcoder produces H.264 + AAC in an MP4 container — the most broadly compatible web video. You can change container/codec in the video-transcoder node (it supports H.264, H.265, ProRes, VP9, AV1), but keep the R2 node's contentType in sync.
How do I avoid overwriting previous uploads?
The blueprint's default key is the constant videos/output.mp4, so each run overwrites the last. Set a unique key per run — a date/name template like videos/2026/launch.mp4, or ${item.name} inside a For-Each batch.
Can I batch a whole folder?
Yes. Wrap the two-node chain inside a logic.for-each over the dropped folder. The for-each runs the body once per item with bounded concurrency (1-32) and emits a JSON array of results on its done port. Use a per-item key so uploads don't collide.
What happens if the R2 upload fails intermittently?
Set the R2 node's errorPolicy to retry. The runner re-attempts the step up to 3 times with linear backoff (200ms, then 400ms) before the error propagates. Without retry, the default policy aborts the run; skip would drop just that branch.
Is there a record of forks and runs?
Yes. Forking writes a server-side forked audit event (tagged with the blueprint slug) to the workflow_audit_events table, and runs write run_triggered / run_cancelled events. It's fire-and-forget so it never blocks your action, and it's an org/compliance trail rather than a local trace log.
What related tools and workflows pair well with this?
Swap the transcoder for video-merger to stitch clips before upload, or lossless-trimmer to cut without re-encoding. For other automation patterns see the RSS to Notion digest and PDF batch extract to Sheets workflows.
Local-first by design
This workflow executes entirely on your jadapps-runner. API keys, database credentials, and OAuth tokens are stored in an AES-GCM-encrypted vault on your device — they are never uploaded to JAD Apps' servers. The server only stores the workflow graph (the recipe), not the secrets.