> ## Documentation Index
> Fetch the complete documentation index at: https://docs.repost.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Output and errors

> The JSON envelope, the stdout/stderr split, exit codes, and the stable error codes the Repost CLI guarantees to programs.

Agents consume JSON, not terminal tables. This page is the **contract** every other page in this section relies on: how to turn JSON on, the envelope shape, where output goes, and the stable codes the CLI guarantees. Read it once and branch on it. (Per-command flags live in the [command reference](/cli/reference#agentic-commands).)

## Get machine-readable output

Pass `--json` explicitly in scripts. The CLI also switches to JSON automatically when stdout is not a terminal, but an explicit flag keeps logs auditable.

| Trigger                              | Result                              |
| ------------------------------------ | ----------------------------------- |
| `--json`                             | JSON on stdout.                     |
| `--output json` / `--output table`   | Forces JSON on, or human tables on. |
| Non-TTY stdout (piped or redirected) | JSON automatically.                 |

Precedence: `--output` wins, then `--json`, then the TTY check (non-terminal → JSON, terminal → human tables).

<Note>
  **Data goes to stdout. Diagnostics, progress, and errors go to stderr.** Parse stdout; never scrape stderr for results.
</Note>

## Read the success envelope

Almost every command returns the same two-field envelope. (The few that don't are listed under [envelope exceptions](#know-the-envelope-exceptions).)

<ResponseField name="schema" type="string" required>
  Stable identifier for the output shape, for example `repost.events.search/v1`. Branch on this — it is versioned and never changes meaning within a major version.
</ResponseField>

<ResponseField name="data" type="object" required>
  The command result. Its shape is determined by `schema`.
</ResponseField>

<CodeGroup>
  ```bash Command theme={null}
  repost events search 'method:POST' --bucket stripe-prod --since 15m --limit 2 --json
  ```

  ```json Result theme={null}
  {
    "schema": "repost.events.search/v1",
    "data": {
      "items": [
        {
          "id": "evt_01JZ8V1",
          "bucket_id": "bkt_01HV9S",
          "method": "POST",
          "path": "/stripe/webhook",
          "received_at": "2026-06-19T08:31:22Z",
          "response_status": 500
        }
      ],
      "total": 12,
      "next_cursor": "eyJvZmZzZXQiOjJ9",
      "has_more": true
    }
  }
  ```
</CodeGroup>

Guard on `schema`, then read `data`. The same result, parsed two ways — this is **your** consuming code, not Repost internals:

<CodeGroup>
  ```bash jq theme={null}
  result=$(repost events search 'method:POST' --bucket stripe-prod --since 15m --json)

  # Fail loudly if the shape isn't the one you built against
  echo "$result" | jq -e '.schema == "repost.events.search/v1"' >/dev/null

  # Read fields; take the next page only when there is one
  echo "$result" | jq -r '.data.items[] | select(.response_status >= 500) | .id'
  next=$(echo "$result" | jq -r 'if .data.has_more then .data.next_cursor else empty end')
  ```

  ```typescript TypeScript theme={null}
  import { execFileSync } from "node:child_process";

  const stdout = execFileSync("repost", [
    "events", "search", "method:POST",
    "--bucket", "stripe-prod", "--since", "15m", "--json",
  ]);
  const result = JSON.parse(stdout.toString());

  if (result.schema !== "repost.events.search/v1") {
    throw new Error(`unexpected schema: ${result.schema}`);
  }
  const failed = result.data.items.filter((e) => e.response_status >= 500);
  const nextCursor = result.data.has_more ? result.data.next_cursor : null;
  ```
</CodeGroup>

<Note>
  Event payloads in `data` are **redacted by default** — credential and signature headers and secret-like JSON keys are masked, but PII is not. See [Transcript safety](/agents/transcript-safety) for exactly what is and isn't removed.
</Note>

## Know the envelope exceptions

A few commands do **not** use the `{schema, data}` envelope. An agent that assumes a `schema` field everywhere will break on these.

| Command                      | Output                                                                                        |
| ---------------------------- | --------------------------------------------------------------------------------------------- |
| `expect`                     | A single **bare** JSON object (no envelope, no `schema`). Always JSON, even without `--json`. |
| `tail`                       | **NDJSON** — one bare JSON object per matching event, one per line. Always JSON.              |
| `capabilities`               | Top-level `{ schema, version, commands }`. Always JSON.                                       |
| `events get --body-only`     | The raw request body, no envelope.                                                            |
| `docs schema`                | `{ schema, command, output_schema, output_mode, envelope, data_shape? }`.                     |
| `docs agent` / `docs search` | Markdown text, not JSON.                                                                      |

<Tip>
  Wait commands (`expect`, `tail`) are covered in [Wait for events](/agents/wait-for-events). Discovery commands (`capabilities`, `docs schema`) are covered in [Discovery](/agents/discovery).
</Tip>

## Handle errors

When JSON is enabled, failures are structured: the `error` object goes to **stderr** and the process exits non-zero.

<ResponseField name="error.code" type="string" required>
  Stable machine code. **Branch on this**, never on the message.
</ResponseField>

<ResponseField name="error.message" type="string" required>
  Human-readable explanation. For people, not programs.
</ResponseField>

<ResponseField name="error.exit_code" type="integer" required>
  Mirrors the process exit status (see the table below).
</ResponseField>

<ResponseField name="error.hint" type="string">
  Suggested next step, when one applies.
</ResponseField>

<ResponseField name="error.docs" type="string">
  Always present; defaults to `repost docs agent`.
</ResponseField>

<ResponseField name="error.missing_scope" type="string">
  The exact scope to request. Present only on `forbidden_scope`.
</ResponseField>

<ResponseField name="error.token_creation_url" type="string">
  Where to mint a token with the missing scope. Present only on `forbidden_scope`.
</ResponseField>

<CodeGroup>
  ```bash Command theme={null}
  repost replay evt_01JZ8V1 --bucket stripe-prod --forwarder prod-api --yes --json
  ```

  ```json Error (stderr, exit 3) theme={null}
  {
    "error": {
      "code": "forbidden_scope",
      "message": "missing required scope: write",
      "exit_code": 3,
      "hint": "create or select a token that includes the write scope",
      "docs": "repost docs agent",
      "missing_scope": "write",
      "token_creation_url": "https://app.repost.sh/settings/tokens"
    }
  }
  ```
</CodeGroup>

Decide from `error.code` and `exit_code`, never from the human message:

<CodeGroup>
  ```bash jq theme={null}
  # Capture stdout (data) and stderr (the error envelope) separately
  if out=$(repost replay evt_01JZ8V1 --bucket stripe-prod --forwarder prod-api --yes --json 2>err.json); then
    echo "$out" | jq '.data'
  else
    case "$(jq -r '.error.code' err.json)" in
      rate_limited|upstream_unavailable) action=retry ;;   # the only codes worth retrying
      forbidden_scope)                   action=request_scope ;;
      partial_failure)                   action=inspect ;;
      *)                                 action=fail ;;
    esac
    exit "$(jq -r '.error.exit_code' err.json)"
  fi
  ```

  ```typescript TypeScript theme={null}
  // A pure decision from the code alone — no message parsing.
  type RepostError = { code: string; exit_code: number; missing_scope?: string };

  function decide(error: RepostError | undefined) {
    if (!error) return { action: "succeed" } as const;      // no error key = success
    switch (error.code) {
      case "rate_limited":
      case "upstream_unavailable": return { action: "retry" } as const;
      case "forbidden_scope":      return { action: "request_scope", scope: error.missing_scope } as const;
      case "partial_failure":      return { action: "inspect" } as const;
      default:                     return { action: "fail", exit: error.exit_code } as const;
    }
  }
  ```
</CodeGroup>

| `error.code`           | exit | When                                                                                               | Agent response                                                                    |
| ---------------------- | :--: | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `validation_failed`    |   2  | Bad input — a client-side flag/argument check or HTTP 400/422                                      | Fix the command. Do not retry unchanged.                                          |
| `active_org_required`  |   2  | A user token can see zero or multiple licensed organizations, so a mutation has no unambiguous org | Use an organization token or select a user context with exactly one licensed org. |
| `unauthorized`         |   3  | Invalid or missing token (401)                                                                     | Refresh or replace `REPOST_TOKEN`.                                                |
| `forbidden_scope`      |   3  | Token lacks a scope (403)                                                                          | Request `missing_scope` or take a read-only path.                                 |
| `not_found`            |   4  | Unknown id/bucket/forwarder (404)                                                                  | Verify the id and active organization.                                            |
| `quota_exceeded`       |   5  | Plan limit reached (402)                                                                           | Not transient. Stop or reduce scope.                                              |
| `rate_limited`         |   6  | Server limit (429)                                                                                 | Back off, then retry.                                                             |
| `timeout`              |   7  | `expect` / `tail` / `replay wait` deadline, or `promote setup` browser pairing deadline            | Report the unmet wait. Inspect state before retrying.                             |
| `partial_failure`      |   8  | `health --fail-on` breach; `replay wait` with failures                                             | Inspect details before declaring success.                                         |
| `conflict`             |   1  | State changed under you (409/412)                                                                  | Refresh state, then retry.                                                        |
| `upstream_unavailable` |   1  | Service error / unknown (5xx)                                                                      | Retry with backoff, then report instability.                                      |

<Check>
  `validation_failed`, `active_org_required`, `forbidden_scope`, and `quota_exceeded` are **terminal** — retrying the same command will fail the same way. Only `rate_limited` and `upstream_unavailable` are worth retrying.
</Check>

## Pin to a contract version

<Update label="v1" description="Current contract">
  Every command schema is `repost.<area>/v1`. The success envelope is `{schema, data}`; the failure envelope is `{error}`. Exit codes 0–8 are stable. When a payload changes incompatibly, the schema suffix bumps (`/v2`) and both are served during migration — so an agent can pin to the version it was built against.
</Update>

## Output contract (quick reference)

* Enable JSON: `--json`, or `--output json`, or pipe stdout (non-TTY auto-enables JSON). `--output` wins over `--json`.
* Success: `{"schema":"repost.<area>/v1","data":{...}}` on stdout.
* Failure: `{"error":{"code","message","exit_code","hint?","docs","missing_scope?","token_creation_url?"}}` on stderr, non-zero exit.
* Exceptions (no `{schema,data}`): `expect` (bare object), `tail` (NDJSON), `capabilities` (`{schema,version,commands}`), `events get --body-only` (raw body), `docs schema` (`{schema,command,output_schema,output_mode,envelope}`), `docs agent`/`docs search` (markdown).
* Exit codes: 0 ok · 2 validation\_failed/active\_org\_required · 3 unauthorized/forbidden\_scope · 4 not\_found · 5 quota\_exceeded · 6 rate\_limited · 7 timeout · 8 partial\_failure · 1 conflict/upstream\_unavailable.
* Branch on `error.code` and `exit_code`, never on `message`. Retry only `rate_limited` and `upstream_unavailable`. Output redacts credentials, not PII.

## Continue

<Columns cols={3} className="gap-y-4">
  <Card title="Authentication & scopes" icon="key-round" href="/agents/authentication" cta="Handle auth errors" arrow="true">
    Recover from `unauthorized` and `forbidden_scope`, and the read / write / secrets model.
  </Card>

  <Card title="Transcript safety" icon="file-lock-2" href="/agents/transcript-safety" cta="See what's redacted" arrow="true">
    What `events get` / `events diff` mask in `data` — and what they leave in the clear.
  </Card>

  <Card title="Discovery" icon="compass" href="/agents/discovery" cta="Read the schemas" arrow="true">
    Pull each command's exact `data` shape from the binary with `docs schema`.
  </Card>
</Columns>
