Skip to content

How CLI Nodes Work

CLI nodes run shell commands as pipeline steps.

You declare the tools you need, Radhflow enters a Nix-managed shell with those packages, runs your command, and captures the output. Nothing is installed globally. The same pipeline runs identically on any machine with Nix.

  1. Declare packages. List Nix packages in nix.packages.
  2. Define the command. Write the shell command in params.command. Use {{ }} templates for input fields.
  3. Radhflow enters nix-shell. A transient shell is created with only the declared packages available.
  4. Command runs. stdin receives input data. stdout, stderr, and exit code are captured.
  5. Output is collected. The result is returned as a Record or Table.

No system-wide installs. No version conflicts. No “works on my machine.”

FieldRequiredDefaultDescription
commandYesShell command to execute. Supports {{ }} templates.
argsNoAdditional command arguments.
envNoEnvironment variables passed to the command.
stdinNoInput source piped to the command’s stdin.
stdout_formatNotextHow to parse stdout: text, json, ndjson.
nix.packagesYesList of Nix packages available in the shell.
nix.nixpkgsNoLatest stablePinned nixpkgs revision for reproducibility.
artifactsNoFiles produced by the command.

Data flows in through stdin and out through stdout. This is the fundamental contract for CLI nodes: data in, command runs, data out.

When stdout_format is ndjson, Radhflow parses each line of stdout as a JSON object and assembles the result into a Table. When it’s json, the entire stdout is parsed as a single JSON value. When it’s text, stdout is captured as a string.

flow.yaml
fetch-and-filter:
type: deterministic
op: cli.run
params:
command: >
curl -s https://api.example.com/data
| jq '[.items[] | {name: .name, value: .count}]'
nix:
packages: [curl, jq]
stdout_format: json
outputs:
result: { type: Table }
resize-images:
type: deterministic
op: cli.run
params:
command: >
convert {{ source }} -resize 800x600 {{ dest }}
nix:
packages: [imagemagick]
inputs:
files:
type: Table
from: ref(list-images.files)
outputs:
results: { type: Table }

When the input is a Table, the command runs once per row. Template expressions resolve against each row independently.

extract-audio:
type: deterministic
op: cli.run
params:
command: >
ffmpeg -i {{ input_path }} -vn -acodec mp3
artifacts/{{ name }}.mp3
nix:
packages: [ffmpeg]
artifacts:
- path: "artifacts/{{ name }}.mp3"
type: file
inputs:
request:
type: Record
from: ref(prepare.config)
outputs:
result:
type: Record
schema:
exit_code: { type: number }
stdout: { type: string }
stderr: { type: string }

Radhflow resolves ffmpeg from nixpkgs, enters a shell with it available, runs the command, and returns the result. The host system does not need ffmpeg installed.

The nix block declares the shell environment. Packages are pulled from nixpkgs. Pin a revision for full reproducibility across machines and time.

nix:
packages: [pandoc, texlive.combined.scheme-small]
nixpkgs: github:NixOS/nixpkgs/nixos-24.05 # optional pin

Without a pin, Radhflow uses the latest stable nixpkgs. Pinning is recommended for production pipelines — it guarantees the exact same tool versions on every run.

By default, CLI nodes capture stdout, stderr, and the exit code as a Record.

FieldTypeDescription
exit_codenumberProcess exit code. 0 means success.
stdoutstringStandard output content.
stderrstringStandard error content.

For commands that produce files, declare them in artifacts and reference the output path in downstream nodes.

params:
command: >
pandoc {{ input }} -o artifacts/{{ name }}.pdf
artifacts:
- path: "artifacts/{{ name }}.pdf"
type: file