Skip to main content
Agent commands. Your AI agent uses this node in workflows. You don’t configure it directly.
The shell node runs commands with pipes, redirects, and standard shell features. It blocks dangerous patterns (fork bombs, recursive deletes) before execution, but otherwise commands run with full system access.

Parameters

Output

Passing values to the command

Commands are no longer templated. A command written with a template variable inside it (echo "${name}") now fails validation; the error names the env: line that replaces it.
The command is plain POSIX sh. pflow never fills in ${...} inside it, so $HOME, ${HOME}, ${NAME:-default} and ${#NAME} mean what they mean to the shell. Values reach the command as environment variables bound in env: — template variables resolve there like in any parameter:
A bound value is data: quotes, $, backticks and newlines in it are never interpreted by the shell. Name each variable in UPPER_SNAKE for its content and always double-quote the reference ("$ENDPOINT").
  • Every value binds as text. A number becomes 3, a boolean True/False, null an empty string, an object or array JSON text ({"a": 1}). A string bound directly arrives unchanged. Quote a literal YAML value whose exact text matters (VERSION: "1.10", not 1.10).
  • Names use letters, digits and underscores and don’t start with a digit. Setting a name the shell relies on (PATH, HOME, IFS, ENV, BASH_ENV, PS4, LD_PRELOAD) draws a validation warning.
  • Secrets are masked by name. Approval previews, the web UI, error output and pflow report hide a value whose name contains the word TOKEN, SECRET, PASSWORD, AUTH, CREDENTIAL, COOKIE or API_KEY. Keep those words out of names for ordinary data, or the person approving the node can’t see it. A value passed to a workflow input with such a name, or stored with pflow settings set-env, is also replaced wherever else it appears — the command text, stderr, the result (secret values).
  • The environment has a size limit. On macOS it is about 1 MB for the arguments and the whole environment together; on Linux one value can be at most 128 KiB. A value over the limit, or one containing a NUL byte, fails before the command starts. Pass large data through stdin instead.

Using stdin for large data

stdin pipes a value of any size or shape to the command. Use it for API responses and other data a tool reads from a stream:

stdin type handling

The resulting text is piped as UTF-8. Text holding a surrogate code point (a \uXXXX escape pair cut in half upstream, or bytes that were not valid UTF-8 when pflow read them) cannot be encoded, so the step fails before the command starts, naming stdin:.

Validation and error handling

Your agent handles this. pflow validates commands when the workflow is created. If a command still contains a template variable, the error tells your agent the env: line to add.
A ${...} in the command that names a value pflow knows about — a workflow input, another node’s output, the batch item — is an error, because the shell would expand it to something else (usually nothing):
A name pflow doesn’t know (${HOME}, a variable the command sets itself) is the shell’s and isn’t checked. $${ is an error too: there is nothing to escape in a shell command. When a shell node fails, the error output shows the command and the values it bound in env:, with secrets masked by name and a run’s secret values replaced in the command and stderr.

Security

Commands run directly on your system without sandboxing. While dangerous patterns are blocked, the shell node has full access to your filesystem and network. Sandboxed execution is planned for a future release.
The dangerous-command check reads the command text only. It cannot see what a variable will hold — guard destructive commands yourself ([ -n "$DIR" ] || exit 1).
You’re in control. Your agent asks before running workflows, and you can inspect the workflow to see exactly what commands will execute. If you need stronger restrictions, you can disable shell entirely with pflow settings deny shell - but this significantly limits pflow’s capabilities since shell is often used for data processing and filtering.

Blocked patterns

These commands are rejected immediately with an error:
  • rm -rf / and variants (recursive system deletion)
  • dd if=/dev/zero of=/dev/sda (device operations)
  • :(){:|:&};: (fork bombs)
  • chmod -R 777 / (dangerous permissions)
  • sudo rm -rf / (privileged dangerous commands)

Warning patterns

These trigger warnings but execute unless PFLOW_SHELL_STRICT=true:
  • sudo, su -
  • shutdown, reboot, halt
  • systemctl poweroff

Smart error handling

Some commands return non-zero exit codes for valid “not found” results. The shell node treats these as success:

Examples

Basic command

Process JSON with jq

With environment variables

Working directory

Ignoring errors

External script file

For multi-line scripts, reference an external file. The file content is read and used as the command. Path is relative to the workflow file.

Error handling

The node returns error action on non-zero exit or timeout (unless ignore_errors is true or it’s an auto-handled pattern like grep).

Timeouts

When a step runs past timeout, pflow sends SIGTERM to the command and every process it started, waits up to 5 seconds, then sends SIGKILL to whatever is still running. A trap '…' TERM in the command runs, and whatever it prints is kept in stdout and stderr — use it to report a partial result before the step stops:
Keep the long-running command off the last line, as above, or join it to the trap with ; (trap '…' TERM; ./process-items.sh). macOS’s /bin/sh replaces itself with a final line that is a single command, and the trap goes with it. On Windows there is no SIGTERM for the Git Bash process tree: a timed-out step is killed at once and its trap does not run (ADR-0013: the shell dialect is the same everywhere, the stop signal is platform-native). Everything else is the same on both platforms. A timed-out step has exit_code -1 and timeout true, and keeps the output it wrote before stopping. It returns error action — or, with ignore_errors: true, continues on the normal path so a later step can read that partial output. pflow’s template variables handle most data access - you can use ${api.response.items[0].name} to access nested fields and array elements directly without shell commands. Shell is needed when you need to:
  • Iterate over arrays - jq '.items[].name' (templates can’t do wildcards)
  • Filter or transform - jq 'select(.active)', sort, uniq
  • Compute values - wc -l, arithmetic
Common Unix tools are built into macOS: grep, awk, cut, sort, head, tail, curl. For JSON processing, install jq: