docs · pre-release
Menu
Reference / JSON output

JSON output

Sourcecrates/shep-cli/src/output/mod.rsSpec§9 CLI surfaceAPIshep_core::protocol::request::ProcessInfo

Add --format json to any command and it prints one JSON object instead of a table, on the same stream it would have used anyway: success on stdout, errors on stderr. Nothing about the command's behavior changes; only the rendering does.

The envelope

Every non-streaming command wraps its payload in the same three fields: a schema version, the verb name, and the data itself. Here's shep flock --format json against a two-sheep flock:

shep flock --format json
{
  "schema_version": 1,
  "command": "flock",
  "data": [
    {
      "id": 0,
      "name": "web",
      "status": "online",
      "pid": 40968,
      "restarts": 0,
      "uptime_ms": 1019,
      "fold": "backend",
      "out_file": "/tmp/shepdemo/logs/web-0-out.log",
      "err_file": "/tmp/shepdemo/logs/web-0-err.log",
      "cpu_percent": null,
      "memory_bytes": 1212416,
      "dog": null,
      "lambs": null,
      "last_exit": null,
      "smit": null,
      "instance": 0,
      "handshook": null
    },
    {
      "id": 1,
      "name": "worker",
      "status": "online",
      "pid": 40969,
      "restarts": 0,
      "uptime_ms": 1017,
      "fold": "backend",
      "out_file": "/tmp/shepdemo/logs/worker-0-out.log",
      "err_file": "/tmp/shepdemo/logs/worker-0-err.log",
      "cpu_percent": null,
      "memory_bytes": 1212416,
      "dog": null,
      "lambs": null,
      "last_exit": null,
      "smit": null,
      "instance": 0,
      "handshook": null
    }
  ]
}

command is the verb you typed, spelled out in full: run it as shep ls --format json and commandstill reads "flock", because that's what the daemon answered to, not the alias that got you there. data is always the same shape for a given command: a single object for something like shep ping, an array for anything that lists multiple sheep.

Errors get their own envelope

A failed command prints a different, smaller shape to stderr, nocommand key, because the command never produced a result:

shep describe doesnotexist --format json
{
  "schema_version": 1,
  "error": {
    "code": "not_found",
    "message": "the daemon reported NotFound: selector matched no registered sheep"
  }
}

code is the same word that appears inerror[<code>]: … under the default table format, and it's the same taxonomy as the process exit code: 3for not_found, 4 forinvalid_config, and so on. A script can switch onerror.code without parsing English, and the exit status confirms it independently.

What schema_version promises

It's at 1 today, and it moves only for a breaking change to some command's data shape: a field renamed, removed, or given a different type. Adding a new field to an existing payload does not bump it: a consumer that reads the keys it cares about and ignores the rest survives an additive release untouched. The contract that's actually worth writing code against:

  • Check schema_version once, up front, rather than per field: it's the one thing that tells you whether the shape you coded against still applies.
  • Don't assume every key is present on every row. dogis null for a plain sheep. lambs isnull from shep flock (the plain listing never walks the process tree) but a populated array (empty if the sheep has spawned no children of its own) from bothshep describe and shep fold, since fold renders the same per-sheep detail the table format's Lambs section shows. Check the key itself, not which command you ran, if you're writing something generic.
  • last_exit is null until a sheep's process has stopped at least once under this daemon, and afterwards holds one of two shapes: {"code": 1, "signal": null} for a normal exit, or {"code": null, "signal": 15} for one ended by a signal. Both are never set at once, and both beingnull is not a state the daemon produces. It is the field that distinguishes a process that started and crashed from one that never started at all, which the status alone cannot: both read errored.
  • cpu_percent reads null, not0, when a sample wasn't available yet. A fresh sheep a few hundred milliseconds old, most often. Table format prints the same case as -, for the same reason: a confident zero would be a claim shep can't back up.
  • handshook is about dogs and is null for every sheep. false is the one to act on: the dog's process is up and the shepherd has never heard from it, so it is not doing its job. status still readsonline there, truthfully — it describes the process. Table format shows this row as silent.
  • smit is null until a dog attaches one over the client protocol's SetSmit request. shep stores and reports the string verbatim, and never parses it — seeterminology.
note

shep bleats --format json is the one exception. A log follow has no end, so there's nothing to close an envelope around: it prints one JSON object per line, newline-delimited, with nocommand or data wrapper.

shep bleats web --format json
{"schema_version":1,"id":0,"name":"web","stream":"out","instance":null,"line":"…"}

instance is the sheep's slot within its app, when that app has more than one instance registered; nullotherwise, and also null for a backlog line read from a log file several instances share, since nothing in that file says which one wrote it.

Table format is not a lesser version of this

The default --format table and --format jsonrender from the same underlying data (there's no separate code path that could drift between them) but table adds human formatting JSON doesn't: uptime_ms: 1019 becomes 1s,memory_bytes: 1212416 becomes 1.2M. If you're piping shep's output anywhere, reach for--format json and do the formatting yourself rather than parsing the table's columns.

Where to go next