Chained Workflow
Build multi-step FFmpeg pipelines with chained commands.
Chained Workflow
Some video processing tasks require multiple FFmpeg operations in sequence. RenderIO's chained commands endpoint lets you submit up to 10 FFmpeg commands that execute one after another in the same sandbox. Each step can use the output files from previous steps as inputs, enabling multi-pass encoding, sequential transformations, and complex pipelines.
How chained commands work
When you submit chained commands:
- All commands share the same sandbox filesystem
- Commands execute sequentially in the order you provide them
- Output files from earlier steps are available as inputs to later steps
- Intermediate files (not mapped to an
out_alias) persist between steps - You get a single
command_idto track the entire chain
Use POST /api/v1/run-chained-ffmpeg-commands with an ffmpeg_commands array instead of a single ffmpeg_command string.
Example: Resize then add watermark
This pipeline first resizes a video to 1280x720, then overlays a logo watermark on the resized result.
curl -X POST https://renderio.dev/api/v1/run-chained-ffmpeg-commands \
-H "Content-Type: application/json" \
-H "X-API-KEY: ffsk_your_api_key_here" \
-d '{
"input_files": {
"in_video": "https://example.com/source.mp4",
"in_logo": "https://example.com/logo.png"
},
"output_files": {
"out_video": "final.mp4"
},
"ffmpeg_commands": [
"ffmpeg -i {{in_video}} -vf \"scale=1280:720\" -c:v libx264 -c:a aac intermediate.mp4",
"ffmpeg -i intermediate.mp4 -i {{in_logo}} -filter_complex \"overlay=W-w-10:H-h-10\" -c:a copy {{out_video}}"
]
}'import requests
response = requests.post(
"https://renderio.dev/api/v1/run-chained-ffmpeg-commands",
headers={
"Content-Type": "application/json",
"X-API-KEY": "ffsk_your_api_key_here",
},
json={
"input_files": {
"in_video": "https://example.com/source.mp4",
"in_logo": "https://example.com/logo.png",
},
"output_files": {
"out_video": "final.mp4",
},
"ffmpeg_commands": [
'ffmpeg -i {{in_video}} -vf "scale=1280:720" -c:v libx264 -c:a aac intermediate.mp4',
'ffmpeg -i intermediate.mp4 -i {{in_logo}} -filter_complex "overlay=W-w-10:H-h-10" -c:a copy {{out_video}}',
],
},
)
data = response.json()
print("Command ID:", data["command_id"])interface ChainedCommandResponse {
command_id: string;
}
const response = await fetch(
"https://renderio.dev/api/v1/run-chained-ffmpeg-commands",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": "ffsk_your_api_key_here",
},
body: JSON.stringify({
input_files: {
in_video: "https://example.com/source.mp4",
in_logo: "https://example.com/logo.png",
},
output_files: {
out_video: "final.mp4",
},
ffmpeg_commands: [
'ffmpeg -i {{in_video}} -vf "scale=1280:720" -c:v libx264 -c:a aac intermediate.mp4',
'ffmpeg -i intermediate.mp4 -i {{in_logo}} -filter_complex "overlay=W-w-10:H-h-10" -c:a copy {{out_video}}',
],
}),
},
);
const { command_id } = (await response.json()) as ChainedCommandResponse;
console.log("Command ID:", command_id);const response = await fetch(
"https://renderio.dev/api/v1/run-chained-ffmpeg-commands",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": "ffsk_your_api_key_here",
},
body: JSON.stringify({
input_files: {
in_video: "https://example.com/source.mp4",
in_logo: "https://example.com/logo.png",
},
output_files: {
out_video: "final.mp4",
},
ffmpeg_commands: [
'ffmpeg -i {{in_video}} -vf "scale=1280:720" -c:v libx264 -c:a aac intermediate.mp4',
'ffmpeg -i intermediate.mp4 -i {{in_logo}} -filter_complex "overlay=W-w-10:H-h-10" -c:a copy {{out_video}}',
],
}),
},
);
const { command_id } = await response.json();
console.log("Command ID:", command_id);<?php
$ch = curl_init("https://renderio.dev/api/v1/run-chained-ffmpeg-commands");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"X-API-KEY: ffsk_your_api_key_here",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"input_files" => [
"in_video" => "https://example.com/source.mp4",
"in_logo" => "https://example.com/logo.png",
],
"output_files" => [
"out_video" => "final.mp4",
],
"ffmpeg_commands" => [
'ffmpeg -i {{in_video}} -vf "scale=1280:720" -c:v libx264 -c:a aac intermediate.mp4',
'ffmpeg -i intermediate.mp4 -i {{in_logo}} -filter_complex "overlay=W-w-10:H-h-10" -c:a copy {{out_video}}',
],
]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo "Command ID: " . $data["command_id"] . "\n";In this example:
- Step 1 resizes the input video and saves it as
intermediate.mp4(a plain filename, not an alias) - Step 2 reads
intermediate.mp4from the shared filesystem and adds the watermark, saving the result as the aliased output{{out_video}}
The intermediate file intermediate.mp4 is not mapped to an out_ alias, so it is not uploaded to storage. Only files referenced by output_files aliases appear in the final result.
Example: Extract audio and compress video
Process a video into two separate outputs: a compressed MP4 and an extracted MP3 audio track.
curl -X POST https://renderio.dev/api/v1/run-chained-ffmpeg-commands \
-H "Content-Type: application/json" \
-H "X-API-KEY: ffsk_your_api_key_here" \
-d '{
"input_files": {
"in_video": "https://example.com/recording.mp4"
},
"output_files": {
"out_video": "compressed.mp4",
"out_audio": "audio.mp3"
},
"ffmpeg_commands": [
"ffmpeg -i {{in_video}} -c:v libx264 -crf 28 -preset medium -c:a aac -b:a 128k {{out_video}}",
"ffmpeg -i {{in_video}} -vn -c:a libmp3lame -q:a 2 {{out_audio}}"
]
}'import requests
response = requests.post(
"https://renderio.dev/api/v1/run-chained-ffmpeg-commands",
headers={
"Content-Type": "application/json",
"X-API-KEY": "ffsk_your_api_key_here",
},
json={
"input_files": {
"in_video": "https://example.com/recording.mp4",
},
"output_files": {
"out_video": "compressed.mp4",
"out_audio": "audio.mp3",
},
"ffmpeg_commands": [
"ffmpeg -i {{in_video}} -c:v libx264 -crf 28 -preset medium -c:a aac -b:a 128k {{out_video}}",
"ffmpeg -i {{in_video}} -vn -c:a libmp3lame -q:a 2 {{out_audio}}",
],
},
)
data = response.json()
print("Command ID:", data["command_id"])interface ChainedCommandResponse {
command_id: string;
}
const response = await fetch(
"https://renderio.dev/api/v1/run-chained-ffmpeg-commands",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": "ffsk_your_api_key_here",
},
body: JSON.stringify({
input_files: {
in_video: "https://example.com/recording.mp4",
},
output_files: {
out_video: "compressed.mp4",
out_audio: "audio.mp3",
},
ffmpeg_commands: [
"ffmpeg -i {{in_video}} -c:v libx264 -crf 28 -preset medium -c:a aac -b:a 128k {{out_video}}",
"ffmpeg -i {{in_video}} -vn -c:a libmp3lame -q:a 2 {{out_audio}}",
],
}),
},
);
const { command_id } = (await response.json()) as ChainedCommandResponse;
console.log("Command ID:", command_id);const response = await fetch(
"https://renderio.dev/api/v1/run-chained-ffmpeg-commands",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": "ffsk_your_api_key_here",
},
body: JSON.stringify({
input_files: {
in_video: "https://example.com/recording.mp4",
},
output_files: {
out_video: "compressed.mp4",
out_audio: "audio.mp3",
},
ffmpeg_commands: [
"ffmpeg -i {{in_video}} -c:v libx264 -crf 28 -preset medium -c:a aac -b:a 128k {{out_video}}",
"ffmpeg -i {{in_video}} -vn -c:a libmp3lame -q:a 2 {{out_audio}}",
],
}),
},
);
const { command_id } = await response.json();
console.log("Command ID:", command_id);<?php
$ch = curl_init("https://renderio.dev/api/v1/run-chained-ffmpeg-commands");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"X-API-KEY: ffsk_your_api_key_here",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"input_files" => [
"in_video" => "https://example.com/recording.mp4",
],
"output_files" => [
"out_video" => "compressed.mp4",
"out_audio" => "audio.mp3",
],
"ffmpeg_commands" => [
"ffmpeg -i {{in_video}} -c:v libx264 -crf 28 -preset medium -c:a aac -b:a 128k {{out_video}}",
"ffmpeg -i {{in_video}} -vn -c:a libmp3lame -q:a 2 {{out_audio}}",
],
]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo "Command ID: " . $data["command_id"] . "\n";Both commands reference the original {{in_video}} input. The first step compresses the video, and the second step extracts just the audio. Both output files are uploaded to storage and available in the poll response.
Example: Two-pass encoding
Two-pass encoding produces the best quality-to-size ratio by analyzing the video in the first pass and encoding with optimized bitrate allocation in the second.
curl -X POST https://renderio.dev/api/v1/run-chained-ffmpeg-commands \
-H "Content-Type: application/json" \
-H "X-API-KEY: ffsk_your_api_key_here" \
-d '{
"input_files": {
"in_video": "https://example.com/source.mp4"
},
"output_files": {
"out_video": "encoded.mp4"
},
"ffmpeg_commands": [
"ffmpeg -i {{in_video}} -c:v libx264 -b:v 2M -pass 1 -an -f null /dev/null",
"ffmpeg -i {{in_video}} -c:v libx264 -b:v 2M -pass 2 -c:a aac -b:a 128k {{out_video}}"
]
}'import requests
response = requests.post(
"https://renderio.dev/api/v1/run-chained-ffmpeg-commands",
headers={
"Content-Type": "application/json",
"X-API-KEY": "ffsk_your_api_key_here",
},
json={
"input_files": {
"in_video": "https://example.com/source.mp4",
},
"output_files": {
"out_video": "encoded.mp4",
},
"ffmpeg_commands": [
"ffmpeg -i {{in_video}} -c:v libx264 -b:v 2M -pass 1 -an -f null /dev/null",
"ffmpeg -i {{in_video}} -c:v libx264 -b:v 2M -pass 2 -c:a aac -b:a 128k {{out_video}}",
],
},
)
data = response.json()
print("Command ID:", data["command_id"])interface ChainedCommandResponse {
command_id: string;
}
const response = await fetch(
"https://renderio.dev/api/v1/run-chained-ffmpeg-commands",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": "ffsk_your_api_key_here",
},
body: JSON.stringify({
input_files: {
in_video: "https://example.com/source.mp4",
},
output_files: {
out_video: "encoded.mp4",
},
ffmpeg_commands: [
"ffmpeg -i {{in_video}} -c:v libx264 -b:v 2M -pass 1 -an -f null /dev/null",
"ffmpeg -i {{in_video}} -c:v libx264 -b:v 2M -pass 2 -c:a aac -b:a 128k {{out_video}}",
],
}),
},
);
const { command_id } = (await response.json()) as ChainedCommandResponse;
console.log("Command ID:", command_id);const response = await fetch(
"https://renderio.dev/api/v1/run-chained-ffmpeg-commands",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": "ffsk_your_api_key_here",
},
body: JSON.stringify({
input_files: {
in_video: "https://example.com/source.mp4",
},
output_files: {
out_video: "encoded.mp4",
},
ffmpeg_commands: [
"ffmpeg -i {{in_video}} -c:v libx264 -b:v 2M -pass 1 -an -f null /dev/null",
"ffmpeg -i {{in_video}} -c:v libx264 -b:v 2M -pass 2 -c:a aac -b:a 128k {{out_video}}",
],
}),
},
);
const { command_id } = await response.json();
console.log("Command ID:", command_id);<?php
$ch = curl_init("https://renderio.dev/api/v1/run-chained-ffmpeg-commands");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"X-API-KEY: ffsk_your_api_key_here",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"input_files" => [
"in_video" => "https://example.com/source.mp4",
],
"output_files" => [
"out_video" => "encoded.mp4",
],
"ffmpeg_commands" => [
"ffmpeg -i {{in_video}} -c:v libx264 -b:v 2M -pass 1 -an -f null /dev/null",
"ffmpeg -i {{in_video}} -c:v libx264 -b:v 2M -pass 2 -c:a aac -b:a 128k {{out_video}}",
],
]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo "Command ID: " . $data["command_id"] . "\n";Pass 1 writes a statistics log file to the shared filesystem. Pass 2 reads that log file and produces the final encoded output.
Tips and variations
- Maximum 10 commands: A single chained request can contain up to 10 FFmpeg commands.
- Shared timeout: The command timeout applies to the total time for all commands combined, not per command. Command timeout is determined by your subscription plan.
- Intermediate files: Use plain filenames like
intermediate.mp4ortemp_audio.wavfor files that only need to exist between steps. Onlyout_aliased files are uploaded to storage. - Using outputs as inputs: You can reference
{{out_video}}in later commands to use a previous step's output as input. You can also use plain filenames written by earlier steps. - Error handling: If any command in the chain fails, the entire chain stops and the command status is set to
FAILED. Check theerror_messagefield in the poll response for details about which step failed. - Single command_id: The entire chain is tracked by one
command_id. PollGET /api/v1/commands/:commandIdto check the status of the whole pipeline.
Related guides
- Batch Processing -- process multiple independent files in parallel instead
- Compress Video -- two-pass encoding uses chained commands
Further reading
- FFmpeg as a Service -- chained commands, parallel processing, and the API model
- FFmpeg REST API Tutorial -- complete tutorial with chained command examples