Agent commands. Your AI agent uses this node in workflows. You don’t configure it directly.
Parameters
Output
Passing values to the command
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:
$, 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 booleanTrue/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", not1.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 reporthide a value whose name contains the wordTOKEN,SECRET,PASSWORD,AUTH,CREDENTIAL,COOKIEorAPI_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 withpflow 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
stdininstead.
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.${...} 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):
${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
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).
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 unlessPFLOW_SHELL_STRICT=true:
sudo,su -shutdown,reboot,haltsystemctl 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 pasttimeout, 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:
; (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.
Recommended tools
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
grep, awk, cut, sort, head, tail, curl.
For JSON processing, install jq:

