Description
n8n-nodes-custom-exec
Run a shell command in an exec sidecar from n8n, over HTTP.
Some jobs need a real shell and real tooling — ffmpeg, imagemagick, pandoc,
yt-dlp, a font stack — that a hardened n8n image deliberately does not ship. The
usual answer is to stand up a small sidecar: an HTTP service that accepts a command,
runs it, and returns the result. This node is the n8n end of that arrangement. Give
it a command (n8n expressions and all) and it hands back the exit code, output and
duration.
The sidecar’s URL and an optional shared secret are read from environment variables
on the n8n container — EXECSIDECARURL and EXECSIDECARTOKEN — not from a
credential, so there’s nothing to configure per-workflow beyond the command itself.
> Built and maintained by Shadow Software — we run
> n8n in production across a family of products and open-source the nodes we rely on.
> See our other node, n8n-nodes-huggingface-space,
> for running AI models from any Hugging Face Space.
Installation · Configuration · The exec sidecar · Usage · Response · Security · Compatibility
Installation
Follow the community nodes installation guide,
then search for n8n-nodes-custom-exec.
Self-hosted, from the CLI:
npm install n8n-nodes-custom-exec
Configuration
Set these environment variables on the n8n container (not per-workflow):
EXECSIDECARURL— where your exec sidecar lives, e.g.EXECSIDECARTOKEN(optional) — a shared secret. When set, it is sent
http://exec-sidecar:8080. Defaults to that value if unset. Commands are
POSTed to {EXECSIDECARURL}/exec.
as the X-EXEC-TOKEN header on every request; leave it unset if the sidecar
is unauthenticated.
The exec sidecar
The sidecar is yours to run — the node is only the n8n client. Any HTTP server
works as long as it honours this tiny contract:
| Route | Purpose |
| — | — |
| POST /exec | body { "command": string, "timeout": number } → { "exitCode": number, "stdout": string, "stderr": string, "durationMs": number } |
A minimal reference implementation is a few dozen lines. For example, in Node:
import express from 'express';
import { exec } from 'node:child_process';const app = express();
app.use(express.json());
const TOKEN = process.env.EXECTOKEN; // set the same value as EXECSIDECAR_TOKEN
app.post('/exec', (req, res) => {
if (TOKEN && req.get('X-EXEC-TOKEN') !== TOKEN) return res.sendStatus(401);
const { command, timeout = 300 } = req.body ?? {};
const started = Date.now();
exec(command, { timeout: Math.min(timeout, 1800) * 1000, maxBuffer: 64 << 20 },
(err, stdout, stderr) => res.json({
exitCode: err?.code ?? 0,
stdout, stderr,
durationMs: Date.now() - started,
}));
});
app.listen(8080);
Run it in a container that has the tooling you need (ffmpeg, fonts, imagemagick,
…) alongside n8n, mount a shared volume into both, and point EXECSIDECARURL at
it. Read the Security section before you expose it anywhere.
Usage
Set Command to whatever you want to run. n8n expressions are interpolated, so
you can build the command from earlier items:
ffmpeg -y -i /shared/{{ $json.inputFile }}
-vf "scale=1280:-1" /shared/{{ $json.outputFile }}
If both the sidecar and n8n mount the same /shared volume at the same path,
files written by one are visible to the other, so a workflow can drop a file, run a
command against it, and pick the result back up.
extra 30 seconds on the HTTP call itself so a job that runs right up to its
deadline still returns its result rather than being cut off in transit.
failing the node.
stdout and stderr as separate fields rather than a single output field.
Response
Each item gains an exec object:
{
"exec": {
"command": "ffmpeg -i /shared/in.png … /shared/out.webp",
"exitCode": 0,
"durationMs": 1834,
"output": "…stdout…", // single-field mode (default)
"stderr": "…" // present only when the command wrote to stderr
}
}
With Return Full Output on, output is replaced by separate stdout and
stderr fields. The item’s existing JSON is preserved alongside exec.
Security
**This node runs arbitrary shell commands on whatever sidecar EXECSIDECARURL
points at.** Anyone who can edit the workflow can run any command that sidecar
allows, with that sidecar’s privileges and filesystem access.
EXECSIDECARURL at a sidecar you control, on a network youtrust — never a shared or public endpoint.
EXECSIDECARTOKEN and have the sidecar reject requests without a matching X-EXEC-TOKEN. Do not expose the sidecar unauthenticated.
volume, no host networking), and enforce your own timeout ceiling on its side —
the client-supplied timeout is a request, not a guarantee.
Treat the exec sidecar as a remote shell, because that is exactly what it is.
Compatibility
Tested against n8n 1.x.
Dependencies
The node has zero runtime dependencies — nothing is shipped but the compiled
node itself, so a plain npm audit --omit=dev reports no vulnerabilities.
A plain npm audit does report advisories. Every one of them comes from
n8n-workflow, which is a peer dependency: n8n supplies it at runtime from its
own tree, so those advisories are resolved by upgrading n8n, not this package.
Links
We also build and open-source a family of WordPress plugins and themes for
WooCommerce stores: Broadside
(theme) and Broadside Blocks,
Crypto for WooCommerce, and
AGT for WooCommerce.
About
Made by Shadow Software — we build and run
automation-heavy SaaS products and open-source the n8n nodes we depend on. If you
need custom n8n nodes, workflow automation, or a platform built around it, get in
touch at shadowsoftware.com.