# 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:

```json
{"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:

```json
{"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](./todos-and-scratchpads.md) `--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:

```json
{
  "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:

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

List the names of everything currently running:

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

Restart every failed process in a project:

```bash
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:

```bash
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:

```bash
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:

```bash
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
```

## Related pages

- [CLI overview](./overview.md) — installation, discovery, and diagnostics
- [HTTP API](../integrations/http-api.md) — call the same API directly over HTTP

---

Are you a human? Read this doc on the web: https://soloterm.com/docs/cli/json-output-and-scripting
