BLOG/TEAM CI/CD
THEME
TEAM CI/CD

Machine-readable runs: --output json and the exit-code contract

OrchStep auto-detects pretty vs plain output, emits a single clean JSON document on demand, writes a result file alongside human logs, and follows a documented exit-code contract so CI can branch on the kind of failure.

Jun 20, 2026 OrchStep Team 6 minROLE: DevOps EngineerSCALE: Any

A task runner that only prints pretty boxes is a task runner you cannot script. The moment OrchStep runs in CI, you need two things: output a machine can parse, and an exit code a machine can branch on. OrchStep gives you both, with sane defaults that need no flags at all.

Output that knows where it is running

orchstep run renders in one of three modes, and it auto-detects which to use: a real terminal gets pretty; a pipe or a CI runner gets plain. You rarely set it by hand.

ModeWhenWhat you get
prettydefault on a TTYinteractive box-drawing output
plaindefault when piped / in CIclean line-oriented text, no boxes
jsonopt-ina single structured document, streaming suppressed
orchstep run deploy                 # pretty on a terminal, plain when piped
orchstep run deploy --output plain  # force plain
orchstep run deploy --output json   # structured result

JSON you can pipe straight into jq

--output json prints exactly one JSON document on stdout — nothing else — so it pipes cleanly into jq. The shape is small and stable: workflow, task, status, and a steps array, each step carrying task, name, status, and output.

orchstep run show --output json | jq '.'
{
  "workflow": "envtest",
  "task": "show",
  "status": "success",
  "steps": [
    { "task": "show", "name": "a", "status": "ok", "output": "..." },
    { "task": "show", "name": "b", "status": "ok", "output": "..." }
  ]
}

So a gate is a one-liner:

orchstep run deploy --output json | jq -r '.status'           # -> success
orchstep run deploy --output json | jq '.steps[] | select(.status=="failed")'

When a run fails, the document still parses — status becomes failed and the offending step's status is failed too, so you can pinpoint it programmatically instead of grepping logs.

Logs and a result file: --json-file

--output json is exclusive — it suppresses the logs so stdout is pure JSON. But in CI you usually want both: human-readable logs in the run view and a machine-readable artifact for a later step. That is what --json-file is for — keep a normal output mode and write the structured result to a file:

orchstep run deploy --output plain --json-file result.json
# stdout: the full plain logs (visible in the CI run)
# result.json: the same RunResult document, for a later step / artifact

Critically, --json-file is written even when the run fails, so a downstream step can always inspect what happened. Verified: a failing run still produces a result.json with "status": "failed" and the failed step named.

The exit-code contract

Here is the part that makes CI gating precise. orchstep run follows a documented exit-code contract, so a pipeline can branch on the kind of failure — not just pass/fail:

CodeMeaning
0success
1a task/step failed at execution time
2usage error (e.g. no subcommand in a non-TTY pipeline)
3the workflow could not be loaded/parsed (unknown key, version mismatch, missing file)
4an assert step failed

Each of these is verified against the binary. The payoff is a gate that reacts differently to a broken pipeline vs a genuine failure vs a failed assertion:

orchstep run check
case $? in
  0) echo "ok" ;;
  4) echo "an assertion failed — investigate the check" ;;
  3) echo "the workflow itself is broken — fix the YAML" ;;
  *) echo "a step failed" ;;
esac

That distinction matters. Exit 3 means a misconfigured pipeline (a typo, a missing file, an engine-version mismatch) — it should fail fast and loud, and it is a different problem from exit 1, a step that ran and returned an error. Exit 4 isolates the case where your own assert caught a bad state, so an alert can say "the deploy's health check failed" rather than "something broke."

What you gained

NeedWithout itWith OrchStep
Parse the resultscrape stdout--output json | jq
Logs and a result artifactpick one--json-file writes both
Branch on failure kindonly pass/failexit codes 1 / 3 / 4
Right output for the contextconfigure per envauto-detected TTY vs pipe

The honest boundary

The JSON document is a run summary — workflow, task, per-step status and captured output — not a firehose of every log line or timing trace. For deep post-mortem (live variables, full context per step) that is what orchstep context inspect and the dashboard are for. For CI gating and artifacts, the JSON summary plus the exit code is exactly the right amount of surface, and it is stable enough to script against.

Where to go next

Already running OrchStep in a pipeline? Add | jq -r '.status' to your run step and gate on the exit code — two changes, and your CI speaks OrchStep fluently.

#CI-CD#JSON#EXIT-CODES#AUTOMATION
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — TEAM CI/CD