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

# Machine contract

> Exit codes, output modes, introspection, result handles, and resumable gates - the CLI's contract for scripts and agents.

The `engini` CLI is built to be driven programmatically. Everything on this page is a stable contract of the npm distribution (`@engini/cli`).

## Exit codes

| Code | Meaning |
| - | - |
| 0 | OK |
| 1 | Generic failure (failed liveness check, failed refresh, OAuth denied...) |
| 2 | Usage error (missing/invalid arguments) |
| 3 | Authentication required or failed |
| 4 | Not found |
| 5 | Validation - **including `need:*` gates that tell you the next step** |
| 124 | Timeout (a polled async job didn't finish) |

<Note>
  **No exit code changed in `@engini/cli` 0.16.0.** The table above is unchanged from previous releases; this one adds commands and a flag, not new failure modes. The new `connections update`, `replace`, `defaults`, `set-default`, and `clear-default` commands use the existing codes: `2` for a missing or malformed argument, `4` for an unknown connection, `5` for a server-side validation failure (including a `connections replace` whose activity cannot be remapped onto the target).
</Note>

### MCP server commands

**No exit code changed in `@engini/cli` 0.18.0.** The `engini mcp` group reuses the table above; it adds commands, not failure modes.

| Situation | Code |
| - | - |
| Missing `<mcp_server_token>` argument | `2` |
| Missing a required `--connection-id` | `2` |
| `--connection` / `--connection-id` that is not a plain decimal integer | `2` |
| `engini mcp connections add` / `workflows add` / `connections rm` / `workflows rm` with nothing to apply | `2` |
| `engini mcp tools set` with both `--tool` and `--all`, or with neither | `2` |
| `--tool-name` / `--tool-description` alongside two or more `--workflow` values | `2` |
| Unknown or foreign `mcp_server_token` | `4` |
| `connections rm` / `workflows rm` for a binding not on the server | `4` |
| `tools set --connection-id` for a connection not on the server | `4` |
| Server-side rejection: duplicate `connectionId`, duplicate workflow, an `applicationSlug` that contradicts its `connectionId`, an empty tool slug, an unknown workflow token | `5` |

A not-found error reads `Unknown MCP server token.` and does **not** echo the token back - it is [secret-equivalent](/developers/cli/command-reference#mcp-servers).

### On-prem agent commands

**No exit code was added in `@engini/cli` 0.19.0.** The `engini opa` group reuses the table above.

| Situation | Code |
| - | - |
| Missing `<agent_id>` argument, or one that is not a positive integer | `2` |
| `engini opa create` without `--name` | `2` |
| `engini opa update` with nothing to change | `2` |
| `engini opa logs` without `-o` | `2` |
| `--parallel-tasks` / `--pull-period` that is not an integer | `2` |
| `delete` or `token rotate` in a non-TTY without `--force` | `2` |
| Unknown id, another account's agent, or an id that is a regular connection - the API answers `404` to all three | `4` |
| A setting outside the range the agent connector declares (`400`) | `5` |
| More than one on-prem-agent application and no `--application-slug` (`409 OPA_APPLICATION_AMBIGUOUS`) - the message lists the candidates | `5` |
| `download` sha256 mismatch - the file is **not** written | `5` |
| `logs` when the agent is not `Online` (`409 OPA_AGENT_OFFLINE`) | `1` |
| `logs` when the agent answered with a failure (`502 OPA_AGENT_DISPATCH_FAILED`) | `1` |
| `logs` when the agent did not answer in time (`504 OPA_AGENT_TIMEOUT`) | `124` |

Two deliberate choices in that table:

* **`OPA_AGENT_OFFLINE` is `1`, not a new code.** The agent being offline is a retriable operational state, not something the caller did wrong. The exit-code set is published and scripted against; it grows only when a new *kind* of failure appears, and "the thing you asked for is not reachable right now" already has a code.
* **`update` against an offline agent exits `0`.** The change is persisted; `applied_to_agent: false` in the output is how you learn the agent has not seen it yet. Failing would hide a committed write and leave the caller unable to tell whether retrying is safe.

### Trigger commands

**No exit code was added in `@engini/cli` 0.20.0.** The `engini triggers` group reuses the table above. One code's *meaning* is newly assigned: a resume cursor that retention has pruned (`410 SINCE_PRUNED`) exits **`4`**, not `1` — "the thing you asked for no longer exists" is a not-found, even though the status is `410`.

| Situation | Code |
| - | - |
| Missing `<trigger_id>`, `<trigger_slug>`, `<event_id>`, or `<name>` argument | `2` |
| `engini triggers create` without `--connection`, on `0.20.0`-`0.22.x` | `2` |
| `engini triggers create --connection ""` (explicit empty value) | `2` |
| An unknown flag | `2` |
| `delete`, `destinations rm`, or `destinations rotate-secret` in a non-TTY without `--force` | `2` |
| Unknown or foreign `ti_…`, `te_…`, or destination name | `4` |
| `listen --since <event_id>` whose cursor retention has pruned (`410 SINCE_PRUNED`) | `4` |
| Server-side rejection: a `config` that fails the type's `config_schema`, a schedule below your plan's floor, a duplicate destination name | `5` |
| `engini triggers create` omitting `--connection`, from `0.23.0`, for a type whose application needs one (`400 EMPTY_QUERY`) | `5` |
| The provider refused the subscription on `create` or `enable` (`502`) | `1` |
| Too many concurrent streams on the account (`429`) | `1` |
| `destinations rm` for a destination triggers still point at, with no `--reassign` (`409`) | `1` |
| `events replay` when the destination has auto-disabled (`DESTINATION_DISABLED`) | `1` |
| `listen` that ended on its own with at least one failed `--forward` POST | `1` |

Three deliberate choices in that table:

* **`410 SINCE_PRUNED` is `4`, and it is the only `410` in the group.** The gap error is recognised by its wire code, not by its HTTP status — there is no general `410` row to script against.
* **A subscribe failure (`502`) and a stream-limit rejection (`429`) are `1`.** Both are operational states rather than caller errors, and the exit-code set is published and scripted against; it grows only when a new *kind* of failure appears.
* **A failed `--forward` is the receiver's failure, not the stream's.** `listen` exits `1` only when it ended on its own *and* at least one forwarded POST failed. A forward failure never kills the stream mid-run; the count is reported in the summary line.

**Interrupting `listen` exits `0`** (from `0.23.0`). `Ctrl-C` closes the stream and any in-flight `--forward` POST, prints the usual summary line, and exits `0` even if some forwards had failed - you asked it to stop and it stopped. Only a stream that ended on its own with failed forwards exits `1`.

<Warning>
  **On `0.20.0`-`0.22.x`, `Ctrl-C` does not stop `listen`.** The interrupt is only noticed between stream frames, so an idle stream never notices at all. Upgrade to `0.23.0` or later; on an older version, kill it from outside (`pkill -f "engini triggers listen"`).
</Warning>

### Enum-valued flags

`--kind` (on `engini connections list`) accepts `Regular`, `Key`, or `MCPClient`, **case-insensitively** - `--kind key` is valid.

The CLI does not validate the value locally. It forwards whatever you pass, and the server rejects an unrecognised value with a `400` that lists the accepted ones, surfacing as exit **`5`**. That keeps one authority for the list: a client-side copy would be a second place to update whenever the enum grows.

On `engini connections list`, `--kind MCPClient` is accepted and exits `0` with an empty array - that endpoint serves only `Regular` and `Key` connections. **`--kind OPA` exits `5`** since `@engini/cli` 0.19.0: the server rejects it with `INVALID_ENUM_VALUE` and a message naming `/v1/on-prem-agents`. On-prem agents were never served by that endpoint, so no caller can have depended on the old empty result; the value was removed rather than left pointing at nothing. Use [`engini opa`](/developers/cli/command-reference#on-prem-agents).

`--status` (on `engini opa list`) follows the same pattern: `WaitingForConnection`, `Online`, `Offline`, `Disabled`, case-insensitive, validated server-side, unknown value exits `5`.

### Interactive prompts

* **Declining** a confirmation (`[y/N]` → `n`) exits **`0`**. You were asked, you said no, nothing went wrong.
* **Aborting** a prompt with Ctrl-C or Ctrl-D exits **`2`**, as does stdin ending before you answered. A cancelled prompt makes no claim about what a partly-completed command already did - `engini connect`, for example, may already have created the connection server-side.
* A command that needs to prompt but **can't** exits **`2`**. Prompting requires a terminal on both stdin *and* stdout; pass the values as flags (`--api-key`, `--field`, `--force`) when either is piped.

## Output modes

* **TTY**: human-readable output; progress lines go to stderr.
* **Piped or `--json`**: pretty JSON on stdout - always machine-parseable.

<Note>
  Output format follows **stdout**; interactivity follows **both** streams. They are separate decisions, so a caller that pipes only stdin still gets human-formatted output - including `need:*` payloads. Pass `--json` whenever you intend to parse the result and stdout might be a terminal.
</Note>

* **`--quiet`**: suppresses non-essential output.
* Errors are structured too: a failure prints a JSON error object with a `hint` where applicable.

<Note>
  **`engini triggers listen` is the one exception to "format follows stdout".** It prints **one compact JSON line per event, always** — on a terminal, piped, with or without `--json`. A stream has no end to pretty-print at, and a line-oriented stream is what `jq` and `while read` want, so the format is fixed rather than context-dependent. It closes with a single summary line, `{"events": n, "forwarded": n, "failed": n}`.
</Note>

## Introspection: `--schema`

Every command supports `--schema`, printing its argument and output schema as JSON - agents discover flags without parsing help text:

```bash theme={null}
engini tools call --schema
```

## Resumable gates: `need:*` + exit 5

`engini connect` **never blocks** for a non-interactive caller. "Interactive" means a terminal on **both** stdin and stdout - every gate that would prompt needs stdin to read the answer, so piping stdin alone is enough to take the machine path. At each point where it needs input it prints a self-describing payload and exits `5`:

```bash theme={null}
# direct-credential (e.g. monday)
engini connect monday --json                  # → {need:"fields", resume:"engini connect monday --auth 0 --field ApiKey=<value>"}
engini connect monday --auth 0 --field ApiKey=<key>   # creates → checks → refreshes

# OAuth2 (e.g. outlook) — never opens a browser for the agent
engini connect outlook --json                 # → {need:"oauth", authentication_id, sign_in_url, state,
                                               #    state_expires_in_seconds:300, resume:"engini connect outlook --auth 0 --oauth-state <state>"}
#   show sign_in_url to the user; after they sign in:
engini connect outlook --auth 0 --oauth-state <state>  # polls token → creates → checks → refreshes

# object selection (apps that support it) — resume never re-creates
engini connect <app> --json                   # → {need:"objects", connection_id, objects, resume:"engini connect <app> --connection <id> --object <id>"}
engini connect <app> --connection <id> --object 5 --object 9
```

Each gate payload contains everything needed to proceed (`methods`, `fields`, `sign_in_url`, `objects`...) plus a `resume` string with the **exact next command**. The loop is: run → read `need` → obtain the value (ask your user) → run `resume`. Any `--field` flags you originally passed are echoed back into `resume` so replaying it never drops them; a secret field's value (`isPassword` or `maskInLogs`) is shown as `<value>` rather than the real secret.

A `resume` replayed after `state_expires_in_seconds` exits **`4`** (not found) - the OAuth sign-in state has been purged, expired, or never existed, and the CLI fails fast on the first poll rather than waiting out the full `--timeout`. Start a new `engini connect <app>`.

### Field options in need fields and need auth

A field's entry in either payload's `fields` array can carry two different things about its legal values, and they mean different things:

* **`list_values`** - the raw, possibly-`null` string straight off the application's static schema (`AppConnectionField.ListValues`). Never parsed by the CLI; a caller that wants a structured list has to parse it itself.
* **`options`** - the OAuth terminal poll's **dynamically-discovered** map for a field with no static list (e.g. Salesforce's `ApiVersionUrl`), shaped the same as the SDK's `fieldOptions` - see [Field options discovered after sign-in](/developers/sdk/connections-oauth#field-options-discovered-after-sign-in) for the orientation warning. Only ever populated on the OAuth path; a `--field`/direct-credential method never signs in, so there's no org to discover options from.

```bash theme={null}
# need:"auth" - before any sign-in
engini connect salesforce --json
# → {need:"auth", methods:[{..., fields:[{field_name:"ApiVersionUrl", list_values:null, options:null, ...}]}]}

# need:"fields" - after the OAuth sign-in completes
engini connect salesforce --oauth-state <state> --json
# → {need:"fields", fields:[{field_name:"ApiVersionUrl", list_values:null,
#      options:{"/services/data/v60.0":"60.0", "/services/data/v59.0":"59.0"}, ...}],
#    resume:"engini connect salesforce --auth 0 --oauth-state <state> --field ApiVersionUrl=<value>"}
```

<Warning>
  **`options: null` means two different things depending on which payload it's in - both fall out as the same JSON, so don't conflate them.** In `need:"auth"`, `null` means *not knowable yet* - no sign-in has happened, so there is no org to ask. In `need:"fields"`, `null` means there's nothing dynamic to offer for this field right now - either an OAuth sign-in ran and the lookup found none or couldn't complete, or the field's method (e.g. `--field`/direct-credential, which never signs in) never does that lookup at all. Either way, fall back to `list_values`, if any, or free text. Neither `null` means "this field takes anything" if the field is otherwise constrained.
</Warning>

When `options` is present and non-empty for a field, the value you pass via `--field <fieldName>=<value>` **must be one of its keys** - never a label. An unrecognised value fails fast, client-side, before any request reaches the server:

```bash theme={null}
$ engini connect salesforce --auth 0 --oauth-state <state> --field ApiVersionUrl=v60.0 --json
{"error":"UsageError","message":"'v60.0' is not a legal ApiVersionUrl for this org. Legal values: /services/data/v60.0, /services/data/v59.0","exit_code":2,"hint":"Resume with: engini connect salesforce --auth 0 --oauth-state <state>"}
```

Exit `2` (usage), not `5` - the legal set is already in hand from the payload above, so a bad value is a caller mistake the CLI can catch locally, not a server-side validation round-trip. In a TTY, the same field prompts as a **numbered picker** instead of free text, so you never type the raw value.

<Note>
  Requires `@engini/cli` **0.24.0 or newer** (0.23.0 and earlier neither surface `options` on a field nor validate `--field` against it).
</Note>

## Result handles

Tool results larger than `--max-bytes` (default 4096) are spilled to a local sandbox and replaced by a compact envelope with a **handle** and a shape preview. Drill in instead of re-fetching:

```bash theme={null}
engini tools result h42 --select items.email      # dot-path (maps over a list)
engini tools result h42 --fields id,name           # project keys per item
engini tools result h42 --select items --offset 20 --limit 10   # page a list
engini tools result h42 --full                     # the whole payload
engini tools result --list                          # known handles
engini tools result --clear                         # wipe the sandbox
```

Stored results live under `$XDG_CACHE_HOME/engini/results/` (`%APPDATA%` on Windows) and are auto-pruned to the most-recent 20, dropping anything older than 24h - handles are short-lived, so drill in during the same session. `--inline` (or `--max-bytes 0`) forces the full result inline; `--raw` / `--llm` bypass the sandbox.

## LLM-shaped output

`engini tools call ... --llm openai|anthropic --tool-call-id <id>` emits the result already formatted as that vendor's tool-result message - pipe it straight back into the model conversation.

## Previews

Mutating commands accept `--dry-run`, printing what would happen (e.g. `{"would_connect": ...}`) without executing.

Every mutating `engini mcp` command previews under `--dry-run`, and for the set-replacing ones the preview *is* the safety mechanism - it shows the whole resulting set, not just your edit:

```json theme={null}
// engini mcp activate|deactivate --dry-run
{"would_update": {"mcp_server_token": "…", "is_active": true}}

// engini mcp connections add|rm --dry-run, and engini mcp tools set --dry-run
{"would_update": {
  "mcp_server_token": "…",
  "connections": {
    "current":   [ /* connection objects as stored */ ],
    "resulting": [ /* the full set that would be sent */ ],
    "added":     [ /* … */ ],
    "removed":   [ /* … */ ],
    "changed":   [ {"connection_id": 2139, "application_slug": "gmail",
                    "from": { /* … */ }, "to": { /* … */ }} ]
  }
}}

// engini mcp workflows add|rm --dry-run
{"would_update": {
  "mcp_server_token": "…",
  "workflows": {"current": ["…"], "resulting": ["…"], "added": ["…"], "removed": ["…"]}
}}
```

`changed` exists because a connection kept on both sides but narrowed or widened in tool scope is neither added nor removed - matching on id alone would hide exactly the binding loss a preview is for. Tool-slug order is ignored, so re-listing the same tools in a different order is not reported as a change.

## `engini mcp` output shapes

The `engini mcp` group prints snake\_case keys, mapping one-to-one onto the camelCase members of the [`/v1/mcpservers`](/developers/api-reference) responses.

<Warning>
  `mcp_server_token` is the `{server-token}` component of the server's SSE URL. Redact it in anything you log, publish, or attach to a ticket.
</Warning>

**`engini mcp list`** - a JSON array of server summaries. Deactivated servers are included, with `is_active: false`. Pagination is handled for you.

**`engini mcp get <token>`** - one object: the summary plus `url`, `updated_date`, `created_by_account_user_id`, `updated_by_account_user_id`. Every mutating command in the group also prints the updated summary on success.

| Key | Type | Notes |
| - | - | - |
| `mcp_server_token` | string | Identifier and SSE-URL component. Secret-equivalent |
| `name` | string | |
| `is_active` | boolean | `false` for a deactivated server, which is still listed |
| `is_anonymous_allowed` | boolean | **Read-only** over the API; set in the engini.io UI and always preserved |
| `connections` | array | See below |
| `workflows` | array | See below |
| `created_date` | string (ISO 8601) | |
| `url` | string \| null | `get` only - the SSE endpoint |
| `updated_date` | string \| null | `get` only - `null` until first updated |
| `created_by_account_user_id` | number \| null | `get` only |
| `updated_by_account_user_id` | number \| null | `get` only - `null` until first updated |

`connections[]`:

| Key | Type | Notes |
| - | - | - |
| `connection_id` | number | |
| `application_slug` | string | |
| `application_name` | string \| null | |
| `tool_slugs` | string\[] | The allow-list. **Empty means every tool of the application** |
| `available_tools_count` | number | Tools the application offers in total |
| `selected_tools_count` | number | Explicitly selected; `0` when all tools are permitted |

`workflows[]`:

| Key | Type | Notes |
| - | - | - |
| `workflow_token` | string | The identifier. The wire field is `workflowToken`, **not** `workflowId` |
| `workflow_name` | string \| null | |
| `tool_name` | string \| null | Override; `null` means the workflow's own slug is used |
| `tool_description` | string \| null | Override; `null` means the workflow's own description is used |

**`engini mcp tools list --connection-id <id>`** - a JSON array of `{"tool_name": string, "description": string | null, "is_mandatory": boolean}`. A mandatory tool is exposed regardless of the allow-list.

### Calling `/v1/mcpservers` directly

Four operations over three paths, all account-scoped by your API key:

| Operation | Path |
| - | - |
| List MCP servers | `GET /v1/mcpservers?offset=&top=` |
| Get MCP server | `GET /v1/mcpservers/{mcpServerToken}` |
| Update MCP server | `PATCH /v1/mcpservers/{mcpServerToken}` |
| List a connection's available tools | `GET /v1/mcpservers/{mcpServerToken}/available-tools?connectionId=` |

* `GET /v1/mcpservers` returns the standard envelope: `{"items": [...], "totalCount": n, "offset": n, "top": n}`. `top` is silently **clamped to 1-100** (a missing, zero, or negative `top` becomes 100), and a **negative `offset` is treated as `0`** - neither is an error, so do not infer a page size from what you asked for. Read `offset`/`top` back off the response.
* `PATCH` leaves any omitted member unchanged. **A supplied `connections` or `workflows` list replaces the whole set** - it does not merge. Send one connection and every other connection on that server is dropped. Read the server, modify the list you got back, and send all of it. The CLI does this read-modify-write for you; a direct caller must do it themselves.
* There is deliberately **no create and no delete**. Authoring and destroying an MCP server stay in the engini.io UI, so `POST /v1/mcpservers` and `DELETE /v1/mcpservers/{mcpServerToken}` do not exist.
* `isAnonymousAllowed` is read-only and is preserved by every update.
* Every connection entry must carry an explicit `connectionId`. MCP servers are account-wide, so there is no per-user default connection to resolve one from. An `applicationSlug` sent alongside it must match, or the request is rejected with `400`.
* An `mcpServerToken` belonging to another account returns `404`, never `403` - the same answer as one that does not exist.

## `engini opa` output shapes

Snake\_case keys mapping one-to-one onto the camelCase members of the [`/v1/on-prem-agents`](/developers/api-reference) responses. Agent **ids are not secrets** and are echoed in error messages; agent **tokens are**, and appear only as the deliberate output of `create`, `token get`, and `token rotate`.

**`engini opa list`** - a JSON array of agent summaries.

**`engini opa get <id>`** - the summary plus `description`, `settings`, `created_date`, `updated_date`. `create`, `update`, `enable`, and `disable` also print an agent object on success.

| Key | Type | Notes |
| - | - | - |
| `agent_id` | number | Not a secret |
| `name` | string | |
| `status` | string | `WaitingForConnection` \| `Online` \| `Offline` \| `Disabled` - the name, never a number |
| `version` | string \| null | What the agent last self-reported; `null` until it first connects |
| `last_seen_at` | string (ISO 8601) \| null | Last heartbeat; `null` until it first connects |
| `description` | string \| null | `get` and mutations only |
| `settings` | object | `get` and mutations only - see below |
| `created_date` / `updated_date` | string \| null | `get` only; `updated_date` is `null` until first updated |
| `applied_to_agent` | boolean | **`update` only.** `false` = stored, agent has not received it yet. Not an error |
| `dispatched_task_ids` | string\[] | **`update` only.** Empty when nothing was dispatched |
| `token` | string | **`create`, `token get`, `token rotate` only.** A credential |

`settings`:

| Key | Type | Notes |
| - | - | - |
| `parallel_tasks` | number | Range declared by the agent connector - currently 1-100 |
| `pull_period_seconds` | number | Minimum declared by the agent connector - currently 10 |
| `log_level` | string | e.g. `Information`, `Debug` |

**`engini opa logs <id> -o <file>`** - `{"wrote": string, "bytes": number, "agent_id": number}`. The archive goes to the file, never to stdout.

**`engini opa download`** - with `--json`, the raw manifest: `{"version", "platform", "url", "sha256", "sizeBytes", "releasedAt", "degraded"}`. Without it, a receipt: `{"wrote": string, "bytes": number, "version": string, "verified": boolean | null, "degraded": boolean}`. `verified` is `true` when the file matched the manifest's `sha256`, `false` when the manifest was `degraded` and carried none.

**Previews (`--dry-run`)** - `{"would_create": {...}}`, `{"would_update": id, "changes": {...}}` (only the members you passed - this is the merge, made visible), `{"would_delete": id}`, `{"would_enable": id}`, `{"would_disable": id}`, `{"would_rotate_token_for": id}`, `{"would_write": path, ...}` for `logs` and `download`. `token get` has no `--dry-run`: it is a read, and no read in this CLI has one.

### Calling `/v1/on-prem-agents` directly

Eleven operations over eight paths. All are account-scoped by your API key except `GET /v1/on-prem-agents/releases/latest`, which is anonymous.

| Operation | Path | Notes |
| - | - | - |
| List | `GET /v1/on-prem-agents?status=&name=&offset=&top=` | Paginated envelope; `top` clamped to 1-100 |
| Create | `POST /v1/on-prem-agents` | Body `{name, description?, applicationSlug?, settings?}`. Response includes `token` |
| Get / Update / Delete | `GET` / `PATCH` / `DELETE /v1/on-prem-agents/{id}` | `PATCH` **merges**: omit a member to leave it unchanged |
| Enable / Disable | `POST …/{id}/enable` · `…/{id}/disable` | Both return the agent |
| Token | `GET …/{id}/token` | `{agentId, token}` |
| Rotate | `POST …/{id}/token/rotate?confirm=true` | **`400 CONFIRMATION_REQUIRED` without `confirm=true`** |
| Logs | `GET …/{id}/logs` | `application/zip`; `409 OPA_AGENT_OFFLINE` unless `Online`; `504 OPA_AGENT_TIMEOUT` |
| Manifest | `GET /v1/on-prem-agents/releases/latest` | Anonymous. `degraded: true` = no checksum available |

Every `{id}` route answers `404` for an unknown id, another account's agent, **or an id that is a regular connection**. The three are indistinguishable by design.

## `engini connections auth-providers` output shapes

Snake\_case keys, derived from the camelCase members of the `/v1/auth-providers` responses.

**A provider** (`get`, `create`, `update`, and each element of the `list` array):

| Key | Type | Notes |
| - | - | - |
| `id` | number | |
| `provider_name` | string | |
| `application_id` / `application_name` | number / string | `application_name` may be `null` |
| `source_authentication_id` / `source_authentication_name` | number / string | The OAuth2 method the provider supplies fields for |
| `is_default` | boolean | The application's default provider |
| `is_sandbox` | boolean | |
| `status` | string | `"active"` or `"disabled"` (`update --status` takes the same words, or `1` / `0`) |
| `scopes_override` / `extra_options` | string \| null | |
| `connection_count` | number | Connections signed in through this provider |
| `connector_removed` | boolean | The application was removed - the provider can only be deleted |
| `no_longer_supported` | boolean | The connector stopped allowing providers on this method - existing connections keep working, new sign-ins are refused |
| `fields_need_review` | boolean | A required value is missing, or the connector changed which fields are set on the provider since the values were entered |
| `fields` | array | One entry per "Set on provider" field, below |

Each `fields` entry is `{"slot", "label", "is_password", "is_required", "value", "has_value"}`. A secret (`is_password: true`) always has `"value": null` - `has_value` tells you whether one is stored.

| Command | Output |
| - | - |
| `list` | A JSON array of providers |
| `get <id>`, `create`, `update <id>` | One provider |
| `delete <id>` | `{"deleted": <id>}` |
| `redirect-uri` | `{"redirect_uri": string}` |
| `usable` | An array of `{"id", "provider_name", "application_id", "application_name", "source_authentication_id", "source_authentication_name", "is_default"}` - no `fields` |
| `sources` | An array of `{"application_id", "application_name", "methods": [{"authentication_id", "display_name", "authentication_type", "fields": [{"slot", "label", "is_password", "is_required"}]}]}` |

**Previews (`--dry-run`)** never print a field value - only the slots you named:

```json theme={null}
{"would_create": {"application": "salesforce", "authentication_id": null, "provider_name": "Acme prod", "is_default": false, "is_sandbox": false, "scopes_override": null, "extra_options": null, "field_slots": ["Key1", "Key2"]}}
{"would_update": {"id": 7, "status": 0}}
{"would_delete": 7}
```

`would_create.authentication_id` is `null` when you left out `--auth`; `would_update` lists the options you passed under the request's camelCase names (`providerName`, `isDefault`, `isSandbox`, `status` as `1` / `0`, `scopesOverride`, `extraOptions`) plus `field_slots`.

**Exit codes.** No exit code was added in `@engini/cli` 0.26.0. Four `409` codes from `/v1/auth-providers` exit **`5`** (validation) because each has a fix the caller can make: `AUTH_PROVIDER_IN_USE`, `AUTH_PROVIDER_NOT_SUPPORTED`, `AUTH_PROVIDER_NAME_TAKEN` and `CONNECTOR_REMOVED`. Any other `409` - including `AUTH_PROVIDER_CONFLICT`, a retriable race - stays `1`. A bad `--field`, a missing required option, a provider id that is not a positive integer, a prompt with no terminal, and `delete` without `--force` off a terminal all exit `2`; an application or provider that cannot be found exits `4`.

### Calling `/v1/auth-providers` directly

Eight operations over five paths. All are account-scoped by your API key.

| Operation | Path | Notes |
| - | - | - |
| List | `GET /v1/auth-providers` | A plain array, not paginated. Needs permission to manage providers |
| Get | `GET /v1/auth-providers/{id}` | One provider. Needs permission to manage providers |
| Create | `POST /v1/auth-providers` | Body `{providerName, applicationId, sourceAuthenticationId, isDefault, isSandbox, scopesOverride?, extraOptions?, fieldValues?}`; the method must be OAuth2 and allow providers |
| Update | `PATCH /v1/auth-providers/{id}` | Only the members present change |
| Delete | `DELETE /v1/auth-providers/{id}` | No response body |
| Usable | `GET /v1/auth-providers/usable` | The active providers you can sign in through - ids, names and methods only |
| Sources | `GET /v1/auth-providers/sources` | Applications and methods a provider can be based on, with their field definitions. Needs permission to create providers |
| Redirect URI | `GET /v1/auth-providers/redirect-uri/{applicationId}?authenticationId=&sandbox=` | `{"redirectUri": string}` |

* `fieldValues` is an object keyed by slot (`Key1`..`Key10`, `ApiUser`, `ApiPassword`, `BaseUrl`). A secret value is write-only: a response carries `hasValue: true` and a `null` `value`, never the secret.
* On `PATCH`, a slot left out of `fieldValues` keeps its stored value and `""` clears it, so unlike `connections update` it does not replace the whole map. `scopesOverride` and `extraOptions` also take `""` to clear.
* A request body carrying `clientId` or `clientSecret` is rejected with `400 INVALID_AUTH_PROVIDER`.
* Deleting a provider that connections are signed in through is `409 AUTH_PROVIDER_IN_USE`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.