---
title: "Scripting and CI"
description: "The anlyon CLI's machine contract: JSON output, stdout and stderr, exit codes, environment variables, a CI example, and what may change between releases."
icon: "code"
---

Scripts, CI jobs and agents should rely on `--json` output and exit codes, not on human-readable output, which may change in any release. The flags and exit codes on this page are the machine contract. While the CLI is 0.x, a minor release changes one only with a changelog entry marked **Breaking** (see [Compatibility](#compatibility)).

## Terminal contract

| Flag | Meaning |
| --- | --- |
| `--json` | stdout carries exactly one JSON document. Lists: `{"data": [...], "nextCursor": string\|null, "total"?: number}`. Single objects: `{"data": {...}}`. |
| `--profile <name>` | Use this profile. |
| `--environment <id\|slug>` | Assert the credential's environment. It never switches it. |
| `--input <file\|->` | Read options as a JSON object (keys are the command's option names, plus `limit`/`cursor`). Flags win. Unknown keys are rejected. |
| `--limit <1-100>` / `--cursor <c>` | Pagination for list commands. Pass back `nextCursor`. Cursors are opaque and bound to the list that issued them. |
| `--timeout <duration>` | Give up after e.g. `30s`, `2m`, `500ms` (default 60s, 10m for `auth login`, and the wait plus one minute when `--wait` is set). |
| `--no-input` | Never prompt or open a browser. Fail instead. |

For `actions invoke`, `--input` supplies the action input itself, as an alternative to `--data`.

### Output

- Results go to **stdout**. Progress, hints, warnings and errors go to **stderr**.
- With `--json`, an error is one JSON object on stderr:

  ```json
  {"error": {"code": "...", "message": "...", "exitCode": 4, "status": 403, "requestId": "...", "hint": "..."}}
  ```

  `status`, `requestId` and `hint` are present when they apply.
- Output is redacted. API keys, tokens and `Bearer` values are removed before anything is printed.
- Ctrl-C cancels the in-flight request and exits 130. The exception is a write (`actions invoke`, `actions declare`, `approvals approve`, `approvals deny`) that was sent and has no reply yet. Ctrl-C or `--timeout` then exits 13, because the request may have reached Anlyon. The error names the idempotency key to re-run with.

### Paging through a list

```bash
anlyon invocations list --status unknown --limit 100 --json > page1.json
anlyon invocations list --status unknown --limit 100 --json \
  --cursor "$(jq -r .nextCursor page1.json)" > page2.json
```

`nextCursor` is `null` on the last page.

## Exit codes

| Code | Meaning |
| --- | --- |
| 0 | Success |
| 1 | Error (unexpected, or no more specific code) |
| 2 | Usage: bad flags, arguments or `--input`. Nothing was sent |
| 3 | Not logged in, or the credential expired, was revoked or was rejected (401) |
| 4 | Permission denied: missing scope or role (403) |
| 5 | Not found in this workspace/environment (404) |
| 6 | Rejected by the server as invalid or conflicting (400/409/422) |
| 7 | Rate limited or over quota (429) |
| 8 | Server error or unreachable (5xx, network) |
| 9 | `--timeout` elapsed. A write cut off before its reply exits 13 instead |
| 10 | Target mismatch: `--environment`, a changed binding, or an API origin override |
| 11 | Needs a server endpoint that is not available yet. Nothing was sent |
| 12 | Action accepted, not finished: waiting for approval or still running |
| 13 | Outcome unknown: the action may have taken effect, or a write was cut off before its reply. Do not retry with a new key |
| 14 | Action did not succeed: failed, denied, or its approval expired |
| 130 | Cancelled (Ctrl-C). A write cut off before its reply exits 13 instead |

`anlyon doctor` exits 1 when a check fails and 0 otherwise. A warning does not fail it. Exit 11 is reserved for a command whose endpoint a server does not have. No current command uses it.

The codes that need a decision from a script are 12, 13 and 14:

- **12**: Nothing has failed. The action is waiting for a person, or still running. Check later with `anlyon invocations get <id>`, or invoke with `--wait <duration>`.
- **13**: The destination may or may not have acted. Do not retry with a new idempotency key. Reconcile with the destination first. Re-running with the **same** `--idempotency-key` is safe: it returns the recorded result.
- **14**: The action did not run successfully: it failed, a reviewer or policy denied it, or its approval expired.

## Environment variables

| Variable | Purpose |
| --- | --- |
| `ANLYON_PROFILE` | Profile for this shell. |
| `ANLYON_API_KEY` | API key for automation. It bypasses profiles. |
| `ANLYON_API_URL` | API origin for `ANLYON_API_KEY` and new logins (`ANLYON_BASE_URL` is accepted too). |
| `ANLYON_CONFIG_DIR` | Config directory. The default is `$XDG_CONFIG_HOME/anlyon` when `XDG_CONFIG_HOME` is set, `~/.config/anlyon` otherwise, and `%APPDATA%\anlyon` on Windows. |
| `ANLYON_CREDENTIAL_STORE` | `auto` (default), `keychain`, or `file` (unencrypted opt-in). |
| `ANLYON_NO_BROWSER=1` | Print the login URL instead of opening a browser. |

## CI example

A GitHub Actions step that invokes an action with an API key stored as a repository secret, waits up to 15 minutes for an approval, and branches on the exit code:

```yaml
- name: Refund via Anlyon
  env:
    ANLYON_API_KEY: ${{ secrets.ANLYON_API_KEY }}
  run: |
    npm install -g @anlyonhq/cli
    set +e
    anlyon actions invoke refund_payment \
      --data '{"charge":"ch_123","amount":1200}' \
      --idempotency-key "refund-ch_123-${{ github.run_id }}" \
      --environment production \
      --wait 15m --yes --no-input --json > result.json
    code=$?
    set -e
    case "$code" in
      0)  echo "Refund succeeded." ;;
      12) echo "::warning::Still waiting for approval $(jq -r .data.approvalId result.json)." ;;
      13) echo "::error::Outcome unknown. Reconcile with the destination before re-running; do not change the idempotency key."; exit 1 ;;
      14) echo "::error::Refund did not succeed: $(jq -r .data.status result.json)."; exit 1 ;;
      *)  exit "$code" ;;
    esac
```

What each part does:

- **`ANLYON_API_KEY`** comes from a secret, never the repository. Create the key in the target environment with only the scopes the job needs (here `actions:invoke` and `actions:read`, because `--wait` reads the invocation while it waits). With an API key set, profiles are ignored.
- **`--idempotency-key`** includes the workflow run id, so re-running a failed job reuses the key and returns the recorded result instead of invoking the action twice.
- **`--environment production`** asserts the key is bound to production. If it is not, the command stops with exit code 10 before anything is sent.
- **`--wait 15m`** follows an approval to its outcome. If nobody decides within 15 minutes, the command exits 12.
- **`--yes`** confirms the write. Without it a non-interactive write is refused and nothing is sent. **`--no-input`** makes sure the CLI never waits on a prompt.
- **`set +e`** stops the shell failing the step before the exit code can be read.

For a successful, pending or finished invoke, `--json` writes the invocation under `data` (with `status` and `approvalId`), and the idempotency key used under `idempotencyKey`.

## Compatibility

- **Versioning.** The CLI is versioned independently of the SDK and the API. While 0.x, a minor release may add commands, flags, JSON fields and exit codes. A minor release may also remove or rename one, change the meaning of an exit code, or change the shape of an existing `--json` field. Each such change carries a changelog entry marked **Breaking**. From 1.0, those are major-version changes only.
- **Machine contract.** Scripts should rely on `--json` output and exit codes, not on human-readable output, which may change in any release.
- **SDK.** Each build bundles a specific `@anlyonhq/sdk`, shown by `anlyon --version`. The CLI's tests run against that SDK's source and check every request path against the API's OpenAPI contract.
- **Server.** The CLI requires `GET /api/v2/auth/identity` and `GET /api/v2/environment`, and, for browser login, OAuth discovery at `/.well-known/oauth-protected-resource/mcp`.
- **Config.** `config.json` carries a `version`. A CLI refuses a config version it does not know rather than rewriting it.
- **Platforms.** Linux (x64, arm64), macOS (x64, arm64) and Windows (x64). The npm package supports Node 20.3 and later.
