# JSON output, errors and exit codes

> The contract every simforge command keeps - one JSON document on stdout, a structured error on stderr, and exit codes 0, 1 and 2 - so scripts and agents can rely on it.

Source: https://simforge.ai/docs/output

Every `simforge` command keeps the same contract, so a script or an agent can drive it without parsing prose.

## stdout: one JSON document

A command prints exactly one JSON document on stdout. JSON is the default; `--json` asks for it explicitly, and `--pretty` indents the same document for people. Nothing else is ever written to stdout.

## stderr: a structured error

When a command fails it prints one JSON line on stderr:

```json
{"code": "max_cost_required", "reason": "…", "hint": "Pass --max-cost CENTS to explicitly cap this cloud job."}
```

- `code` is stable: key your handling on it. Never parse `reason`, which is for people.
- `hint` says what to do next, often the exact command.
- `path` names the input at fault, and `detail` carries structured context, when there are any.
- `httpStatus` and `requestId` are there when the SimForge host refused a request: quote the `requestId` to support, which traces the request by it.

`login` also writes event lines (`{"event": …}`, never a `code`) on stderr while it waits: the URL and code a person must approve. `runtime install` and `maps pull` write `{"event": "distribution.download.progress", "bytes": {"done": …, "total": …}, "percent": …}` lines as they download, at each tenth of the archive and at least every 30 seconds. A command that waits on a cloud run (`--cloud --wait`, `runs watch`) writes `{"event": "run.progress", "runId": …, "status": …, "jobs": [{"kind", "status", "progress", "queue"}], "elapsedSeconds": …, "message": …}` when it first reads the run, whenever the run or a job changes, and at least every 30 seconds.

## Exit codes

| Exit | Meaning | What to do |
|---|---|---|
| 0 | OK | Read stdout |
| 1 | The command could not run: bad flags, a missing file, the runtime or a map not installed, not signed in | Fix the invocation, or install what the error names |
| 2 | The command ran and found something wrong with its input: a failed check, a refused operation, a digest mismatch | Report or repair the input; retrying the same input gives the same answer |

## The command surface as data

```sh
simforge --help --json
simforge render --help
```

`simforge --help --json` prints every command with its arguments and flags: types, defaults, allowed values and which ones are required. `simforge <command> --help` prints one command. An unknown flag is an error (`unknown_flag`), except after the scenario of `render`, `simulate` and `preview`, whose trailing arguments are runtime options: check those against the command's help. The same document is published at [/docs/cli.json](/docs/cli.json) and rendered as the [command reference](/docs/cli).

A request the hosted API refuses (an HTTP 4xx answer, such as `budget_exceeded` or `insufficient_scope`) exits 2 with the API's own code; a server failure exits 1.

## Common error codes

| Code | Meaning |
|---|---|
| `unknown_command`, `unknown_flag` | Not a command or flag of this CLI; read `--help` |
| `missing_argument`, `bad_value` | A required argument is missing, or a value is invalid |
| `not_logged_in`, `session_invalid` | Sign in with `simforge login` |
| `runtime_not_installed` | Install the runtime: `simforge runtime install --accept-license` |
| `runtime_platform_unsupported` | This platform has no local runtime (Windows on Arm): add `--cloud` |
| `map_not_installed` | Pull the map: `simforge maps pull <name>@<version> --accept-license` |
| `agreement_acceptance_required` | Read `simforge runtime license`, then pass `--accept-license` |
| `entitlement_required` | This download is not available to your account |
| `entitlement_revoked` | SimForge revoked this account's right to the download; it is not re-granted by accepting again |
| `gpu_unsupported`, `gpu_memory_unsupported`, `gpu_driver_unsupported` | The GPU is outside the [supported range](/docs/install#what-the-machine-needs) (NVIDIA with at least 8 GiB and driver 570 or newer, or Apple M3 or newer); 2D renders, simulation and `env serve --no-sensors` still work |
| `native_gpu_out_of_memory` | The scene did not fit in GPU memory: use `--textures bc7-512`, fewer or smaller cameras, or `--cloud` |
| `signature_missing`, `signature_invalid`, `signature_untrusted`, `signature_wrong_host`, `signature_mismatch` | The download is not one SimForge signed for this host; nothing was installed |
| `runtime_install_invalid` | The runtime found is not an installed SimForge runtime release (no `runtime-manifest.json` beside it): run `simforge runtime install --accept-license` |
| `recording_missing` | A scenario that replays a recording cannot find `recordings/<sha256>.rec.json.gz` beside its `document.json` |
| `plan2d_map_missing` | The map's 2D derivative is not installed: pull the map again |
| `max_cost_required`, `budget_exceeded` | A cloud run needs a cost cap, or its estimate is over it |
| `insufficient_credits` | The workspace's credits do not cover a cloud run's [price](/docs/cloud#prices) |
| `cloud_not_open` | SimForge Cloud is not open on this host yet, or does not run that work yet (`detail.code` `run_kind_unavailable`): run without `--cloud` |
| `email_unverified` | Downloads and cloud runs need a verified email: open the link in your email, or resend it from the page the hint names |
| `file_exists` | The output exists; pass `--force` to overwrite it |

Codes for a specific area are on its page: [sign-in and runtime](/docs/sign-in), [cloud](/docs/cloud).
