BLOG/DEVELOPER DAILY
THEME
DEVELOPER DAILY

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.

Jun 28, 2026 OrchStep Team 6 minROLE: Backend DeveloperSCALE: Any

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 deploy

A workflow that reads stdin

stdin is its own namespace, separate from vars, env, and steps. Reference parsed fields with {{ stdin.field }}:

orchstep.yml
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: success

Send 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 failed

Auto-detection, three formats

OrchStep sniffs the format so you don't declare it:

PriorityFormatDetectionAccess
1JSONvalid JSON object or array{{ stdin.field }}
2YAMLvalid YAML map or list{{ stdin.field }}
3Textfallback{{ 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 response

Now {{ 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 repo

The 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 wayThe stdin way
jq -r '.field' per value{{ stdin.field }}, typed
shell quoting per interpolationnone — fields render in templates
format declared / assumedJSON / 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

Take a command you already pipe into jq and point it at orchstep run instead. The fields are waiting in {{ stdin }}.

#STDIN#JSON#PIPELINES#UNIX
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — DEVELOPER DAILY