RenderIO
Guides

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:

  1. All commands share the same sandbox filesystem
  2. Commands execute sequentially in the order you provide them
  3. Output files from earlier steps are available as inputs to later steps
  4. Intermediate files (not mapped to an out_ alias) persist between steps
  5. You get a single command_id to 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.mp4 from 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.mp4 or temp_audio.wav for files that only need to exist between steps. Only out_ 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 the error_message field in the poll response for details about which step failed.
  • Single command_id: The entire chain is tracked by one command_id. Poll GET /api/v1/commands/:commandId to check the status of the whole pipeline.

Further reading

On this page