JSON output and scripting

For API-backed command executions, --json switches human-readable output to a stable, single-line JSON envelope. Built-in help (solo help and --help) and the root solo --version command remain human-readable even when --json is supplied. Combined with meaningful exit codes, JSON output makes the CLI easy to use from shell scripts, CI jobs, and other tools.

The JSON envelope

For a command that supports JSON output, a successful invocation with --json prints one JSON object to stdout:

{"ok": true, "command": "projects list", "data": { "...": "..." }}
  • ok is always true on success.
  • command names the command that ran, such as "projects list" or "todos create".
  • data holds the command-specific payload.

Failures print an error envelope to stderr instead, and exit non-zero:

{"ok": false, "error": {"code": "not_found", "message": "process 99 not found", "command": "processes get"}}
  • error.code is a stable machine-readable code (see below).
  • error.message is a human-readable explanation.
  • error.command names the failing command when it's known.

Because success goes to stdout and errors go to stderr, you can pipe stdout straight into jq without error output getting mixed in.

Error codes

Connection, discovery, and CLI errors:

Code Meaning
app_not_running The discovery file is absent from an explicitly selected app data directory; the app may not be running or local CLI access may be off.
stale_discovery The saved app process is no longer running, or the saved connection cannot be reached, times out, or otherwise fails during the request.
invalid_discovery The discovery file is malformed.
discovery_unavailable No usable discovery file was found in any default location.
version_mismatch The CLI and app speak different API versions; this can be detected from discovery or from the live /api/version response. Update one of them.
permission_denied The discovery file exists but could not be read due to filesystem permissions.
input_error A local input problem, such as a missing --body-file path.
protocol_error, internal_error Unexpected transport/response protocol or internal failure; a protocol error can occur after the app responds.

Usage errors (bad arguments or flags) use granular codes describing the specific problem, such as unknown_command, duplicate_flag, or invalid_project_id, and always exit with code 64.

Errors returned by the app map to codes like bad_request, unauthorized, forbidden, not_found, conflict, precondition_failed (for example a scratchpad --expected-revision mismatch), service_unavailable, and internal.

Exit codes

The CLI uses sysexits-style exit codes so scripts can branch on the failure class without parsing anything:

Exit code Meaning
0 Success.
64 Usage error: bad arguments or flags.
65 The app rejected the request (not found, conflict, revision mismatch, bad request).
66 An input file (such as --body-file or --content-file) could not be read.
69 The app is unavailable: not running, stale discovery, or starting up.
70 Protocol or internal error.
74 I/O error reading input from stdin.
77 Permission denied: a bad or rejected token, or an unreadable discovery file.
78 Configuration error, including CLI/app version mismatches.

List responses and pagination

list commands return their items in an array (projects, processes, todos, or scratchpads) alongside pagination fields:

{
"ok": true,
"command": "processes list",
"data": {
"scope": { "type": "project", "projectId": 2, "projectName": "my-app", "projectDisplayName": null },
"processes": [ { "id": 9, "name": "dev", "status": "running", "...": "..." } ],
"totalCount": 12,
"offset": 0,
"limit": 50,
"limitClamped": false,
"hasMore": false,
"nextOffset": null
}
}

Project, process, todo, and scratchpad list responses each include limitClamped. It is true when an oversized requested limit was reduced to the server maximum. When hasMore is true, pass --offset with the returned nextOffset to fetch the next page.

Scripting examples

Get a project's ID by name:

project_id=$(solo projects list --json | jq '.data.projects[] | select(.name == "my-app") | .id')

List the names of everything currently running:

solo processes list --status running --json | jq -r '.data.processes[].name'

Restart every failed process in a project:

solo processes list --project-id 2 --status failed --json \
| jq -r '.data.processes[].id' \
| xargs -n1 solo processes restart

Count open high-priority todos, and fail a CI step if any exist:

count=$(solo todos list --project-id 2 --status open --priority high --json \
| jq '.data.totalCount')
if [ "$count" -gt 0 ]; then
echo "There are $count open high-priority todos" >&2
exit 1
fi

Read a scratchpad's current revision before writing to it:

rev=$(solo scratchpads read 5 --project-id 2 --json | jq '.data.scratchpad.revision')
solo scratchpads append 5 --project-id 2 --expected-revision "$rev" \
--newline --content "deployed at $(date -u +%FT%TZ)"

Branch on failure class using exit codes:

solo status --json > /dev/null 2>&1
case $? in
0) echo "Solo is up" ;;
69) echo "Solo is not running" ;;
77) echo "CLI access denied; re-check the HTTP API settings" ;;
*) echo "Unexpected failure" ;;
esac
  • CLI overview — installation, discovery, and diagnostics
  • HTTP API — call the same API directly over HTTP

Are you an agent? Read this doc via API: /api/v1/docs/cli/json-output-and-scripting