Pipe JSON into a workflow
curl, terraform output, gh api — they all speak JSON on stdout. OrchStep reads stdin, auto-detects JSON, and exposes it as a typed namespace, so it drops straight into a Unix pipe.
Half the tools you already use print JSON to stdout. curl -s does. terraform output -json does. gh api does. The usual next move is a jq incantation you re-derive every time, piped into a bash variable, quoted three ways to survive the shell.
OrchStep reads stdin directly. Pipe anything in, and if it's JSON it gets parsed and handed to your workflow as a typed stdin namespace — fields and all. No jq, no quoting gauntlet.
echo '{"name":"api","replicas":3,"target":"prod","status":"healthy"}' \
| orchstep run deployA workflow that reads stdin
stdin is its own namespace, separate from vars, env, and steps. Reference parsed fields with {{ stdin.field }}:
name: ingest
tasks:
deploy:
steps:
- name: show
func: shell
do: echo "Deploying {{ stdin.name }} with {{ stdin.replicas }} replicas to {{ stdin.target }}"
- name: guard
func: assert
args:
condition: '{{ eq stdin.status "healthy" }}'
message: "service not healthy"Pipe the JSON in and the fields land where you'd expect:
$ echo '{"name":"api","replicas":3,"target":"prod","status":"healthy"}' | orchstep run deploy
Step: show
Deploying api with 3 replicas to prod
[ok] show
Step: guard
[ok] guard
Result: successSend it a payload that fails the check, and the assert does its job:
$ echo '{"name":"api","replicas":1,"target":"dev","status":"down"}' | orchstep run deploy
[FAIL] guard
Error: ... assertion failedAuto-detection, three formats
OrchStep sniffs the format so you don't declare it:
| Priority | Format | Detection | Access |
|---|---|---|---|
| 1 | JSON | valid JSON object or array | {{ stdin.field }} |
| 2 | YAML | valid YAML map or list | {{ stdin.field }} |
| 3 | Text | fallback | {{ stdin }} (string) |
Plain text just lands as the whole {{ stdin }} string:
echo "hello world" | orchstep run check
# {{ stdin }} = "hello world"And when nothing is piped — you ran it from a terminal — {{ stdin }} resolves to an empty string. The same workflow runs fine piped or not; stdin is additive, never required.
Name it with --stdin-var
If you'd rather the payload show up in the vars namespace (so it follows normal variable precedence), use --stdin-var:
curl -s https://api.example.com/health | orchstep run check --stdin-var responseNow {{ vars.response.status }} works alongside {{ stdin.status }}. Because it's a real variable, a --var can override a single field while stdin keeps reflecting the raw input:
echo '{"env":"prod"}' | orchstep run deploy --stdin-var config --var config.env=staging
# {{ vars.config.env }} = "staging" (--var wins)
# {{ stdin.env }} = "prod" (stdin is always the raw pipe)Where it shines: the middle of a pipeline
This is what makes OrchStep a composable Unix citizen rather than a walled garden. It sits in the middle of a pipe and turns a JSON blob into structured, conditional, retryable steps:
# Validate Terraform state before acting on it
terraform output -json | orchstep run validate-infra --stdin-var tf
# Gate on a live health check
curl -s https://api.example.com/health | orchstep run check-health
# Drive a workflow from a GitHub API response
gh api repos/myorg/myrepo | orchstep run analyze --stdin-var repoThe upstream tool produces JSON; OrchStep gives it branches, asserts, retries, and a dry-run — without you parsing a single field by hand.
What you gained
The jq + bash way | The stdin way |
|---|---|
jq -r '.field' per value | {{ stdin.field }}, typed |
| shell quoting per interpolation | none — fields render in templates |
| format declared / assumed | JSON / YAML / text auto-detected |
| no-pipe case crashes | {{ stdin }} is just empty |
| override means re-piping | --stdin-var + --var per field |
A couple of edges worth knowing
A trailing newline is trimmed, the way $(...) substitution behaves. A JSON array is stored as an array — reach into it with {{ index stdin 0 }}. Multi-document YAML keeps the first document only. And one interaction to keep in mind: when data is piped in, stdin is consumed by the pipe reader, so interactive prompt steps automatically fall back to their defaults — the same behaviour as ORCHSTEP_NON_INTERACTIVE=true. That's usually exactly what you want in a pipeline.
One YAML gotcha when you write these steps: a do: value with a bare colon-space (like do: echo "Status: ok") trips the YAML parser. Wrap the whole value in single quotes (do: 'echo "Status: ok"') and it's fine.
Where this is not the answer
If you just need one field once, jq -r '.x' in a one-off command is shorter — reach for OrchStep when that JSON needs to drive something: branches, asserts, retries, multiple steps. And stdin is one stream: you can't pipe two different JSON blobs into one run. For multiple inputs, pass the rest as --var.
Where to go next
- Stdin & Pipes — the full reference, including
--stdin-var - assert — gate a pipeline on a condition
- Variables & Outputs — how
--stdin-varjoins precedence
Take a command you already pipe into jq and point it at orchstep run instead. The fields are waiting in {{ stdin }}.
curl -fsSL https://orchstep.dev/install.sh | sh