docs · pre-release
Menu
Machine surfaces / JSON output

JSON output

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.

schema_version is 1 today, and it only moves for a rename, a removal, or a retype in some command's data shape. A new field never bumps it.

The envelope

Two verbs add a fourth field: shep flock adds host, and shep reload adds refused, only sometimes, see a reload that was refused part of its fold. 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": 14955,
      "restarts": 0,
      "uptime_ms": 3047,
      "fold": "backend",
      "depends_on": [],
      "out_file": "/tmp/shepdemo/logs/web-0-out.log",
      "err_file": "/tmp/shepdemo/logs/web-0-err.log",
      "cpu_percent": null,
      "memory_bytes": 3129344,
      "cpu_ms": 4,
      "dog": null,
      "lambs": null,
      "last_exit": null,
      "smit": null,
      "instance": 0,
      "handshook": null,
      "dog_stale": null,
      "max_memory": null
    },
    {
      "id": 1,
      "name": "worker",
      "status": "online",
      "pid": 14979,
      "restarts": 0,
      "uptime_ms": 3020,
      "fold": "backend",
      "depends_on": [],
      "out_file": "/tmp/shepdemo/logs/worker-0-out.log",
      "err_file": "/tmp/shepdemo/logs/worker-0-err.log",
      "cpu_percent": null,
      "memory_bytes": 3129344,
      "cpu_ms": 3,
      "dog": null,
      "lambs": null,
      "last_exit": null,
      "smit": null,
      "instance": 0,
      "handshook": null,
      "dog_stale": null,
      "max_memory": null
    }
  ],
  "host": {
    "cpu_percent": 18.083654,
    "memory_used_bytes": 38749880320,
    "memory_total_bytes": 51539607552,
    "disk_bytes_per_second": [16384, 851968],
    "network_bytes_per_second": [17480, 4174]
  }
}

host sits beside data rather than inside it, so a script reading data[0].name sees nothing new. Its pairs are read then written, and received then transmitted. Three shapes, three answers: an object, as here; null where sysinfo cannot read the platform; and no key at all from a shepherd too old to be asked. Each rate is null on its own until the shepherd has a window to divide by. Same figures as the host line.

command is the verb you typed, spelled out in full: run it as shep ls --format json and command still 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, no command 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 in error[<code>]: … under the default table format, and it's the same taxonomy as the process exit code: 3 for not_found, 4 for invalid_config, and so on. A script can switch on error.code without parsing English, and the exit status confirms it independently.

A reload that was refused part of its fold

shep reload is the one verb that can half-answer. A fold reloads in stages, and an app already reloading is skipped instead of failing the rest, so the run has two things to report. It still prints one object: refused sits beside data, and the exit status is 1.

shep reload all --format json
{
  "schema_version": 1,
  "command": "reload",
  "data": [
    {
      "id": 1,
      "name": "api",
      "status": "online",
      "pid": 63850,
      "restarts": 0,
      "uptime_ms": 7059,
      "fold": null,
      "depends_on": ["db"],
      "out_file": "/tmp/shepdemo/logs/api-0-out.log",
      "err_file": "/tmp/shepdemo/logs/api-0-err.log",
      "cpu_percent": null,
      "memory_bytes": null,
      "cpu_ms": null,
      "dog": null,
      "lambs": null,
      "last_exit": null,
      "smit": null,
      "instance": 0,
      "handshook": null,
      "dog_stale": null
    }
  ],
  "refused": [
    { "name": "db", "reason": "db is already being reloaded" }
  ]
}

The key appears only on a run that refused something, so an ordinary reload carries the same three fields as everything else. Nothing goes to stderr either: the names are already in the envelope, and a second object would break the one-per-invocation rule the rest of this page rests on.

What schema_version promises

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. dog is null for a plain sheep. lambs is null 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 both shep 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. A lamb is a pid and a name, plus a label only when the sheep named it.
  • 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 being null 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, not 0, 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.
  • cpu_ms is cumulative CPU-milliseconds the sheep's process tree has spent since it started, the raw counter cpu_percent is one view of. null when the shepherd isn't sampling the sheep (not running, or the peer daemon predates the field), and on any verb that answers without taking a live reading: only shep list and shep describe pay for one, so the shep reload reply above shows a null counter on an online sheep; it's the one casecpu_percent can't cover the same way, since a sheep spawned since the last periodic tick has a counter already but no baseline yet to turn it into a percent. shep lookout reads this field directly and differences two polls of its own rather than trusting cpu_percent's window, which is why the two can show different numbers for the same sheep at the same instant.
  • 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 reads online there, truthfully — it describes the process. Table format shows this row as silent.
  • dog_stale is the other half of that and is null for every sheep. true means the shepherd has given up: it restarted the dog once for never answering, that did not help, and it will not restart it again. You cannot work this out from handshook — a dog spawned a second ago and a dog the shepherd will never touch again are both false. Why it gave up is in that dog's own log rather than on the row, because the shepherd could only see the evidence at the moment it happened: shep bleats <dog>.
  • 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 — see terminology.
  • max_memory is the sheep's memory ceiling in bytes, or null when it has none configured. It's the same number --format table and lookout's MEM/CEIL gauge divide against; a null here is why that gauge draws an all-tail bar instead of a fraction.
  • reload_deadline_ms is the one key that appears on shep reload's rows and nowhere else. It is how long the shepherd gives one swap of that instance before it gives up: the instance's own listen_timeout plus its graceful_timeout plus five seconds of slack. Per instance, not per app, so a client waiting a reload out reads the number the shepherd is actually keeping to rather than one worked out from a Flockfile copy. The key is absent, not null, on a row no swap is queued for, and on every other command.

Streaming output carries no envelope

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 no command 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; null otherwise, 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 json render 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.