# Experimental API

> Runtime opt-in via `capabilities.experimentalApi`, stable vs experimental schema generation, rejection messages, and maintainer gating patterns for fields and notifications.

- Repository: openai/codex
- GitHub: https://github.com/openai/codex
- Human docs: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1
- Complete Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/llms-full.txt

## Source Files

- `README.md`
- `src/transport.rs`
- `src/filters.rs`
- `src/request_serialization.rs`
- `tests/suite/v2/thread_rollback.rs`
- `tests/suite/v2/compaction.rs`

---

---
title: "Experimental API"
description: "Runtime opt-in via `capabilities.experimentalApi`, stable vs experimental schema generation, rejection messages, and maintainer gating patterns for fields and notifications."
---

App-server v2 exposes a subset of RPC methods, request fields, server notifications, and server-initiated request payloads behind a per-connection capability negotiated at `initialize`. Clients that omit opt-in receive JSON-RPC `Invalid Request` (`-32600`) when they call gated methods or set gated fields; opted-out connections never receive experimental notifications and may receive stripped fields on certain server requests. Schema generators mirror the same split: stable artifacts by default, full surface with `--experimental`.

## Capability negotiation

`initialize.params.capabilities.experimentalApi` is a boolean on `InitializeCapabilities` (wire name `experimentalApi`, default `false` when `capabilities` is omitted).

<ParamField body="experimentalApi" type="boolean">
Opt into experimental RPC methods, experimental request/response fields, experimental server notifications, and experimental fields on server-initiated requests for this connection.
</ParamField>

<Steps>
<Step title="Send initialize with opt-in">
```json
{
  "method": "initialize",
  "id": 1,
  "params": {
    "clientInfo": {
      "name": "my_client",
      "title": "My Client",
      "version": "0.1.0"
    },
    "capabilities": {
      "experimentalApi": true
    }
  }
}
```
</Step>
<Step title="Send initialized">
Emit the standard `initialized` notification before any other RPC on the connection.
</Step>
<Step title="Use experimental surface">
Call experimental methods and set experimental fields only after the handshake completes successfully.
</Step>
</Steps>

<Note>
`experimentalApi` is stored per connection in `ConnectionSessionState` and propagated to outbound writers when initialization completes. A second `initialize` on the same connection returns `"Already initialized"`. There is an open design note to revisit instance-global first-write-wins scoping when multiple clients share threads.
</Note>

Integration tests under `tests/suite/v2/` typically opt in via `TestAppServer::initialize_with_client_info`, which passes `experimental_api: true` by default—do not assume production clients do the same.

## Three runtime enforcement layers

| Layer | When it runs | Behavior without `experimentalApi` |
| --- | --- | --- |
| Incoming client RPC | Before handler dispatch in `MessageProcessor` | Reject with `-32600` and message `{reason} requires experimentalApi capability` |
| Outgoing notifications | In `route_outgoing_envelope` / `should_skip_notification_for_connection` | Drop notifications whose type reports an experimental reason (silent skip) |
| Outgoing server requests | In `filter_outgoing_message_for_connection` | Strip known experimental fields (today: `additionalPermissions` on command-execution approvals) |

```mermaid
sequenceDiagram
    participant Client
    participant Transport as transport.rs
    participant MP as message_processor.rs
    participant Handler as request processor

    Client->>Transport: initialize (experimentalApi: true/false)
    Transport->>MP: store capability on session
    Client->>Transport: thread/realtime/start (example)
    Transport->>MP: dispatch_initialized_client_request
    alt experimental_reason and not opted in
        MP-->>Client: JSON-RPC error -32600
    else allowed
        MP->>Handler: handle request
        Handler-->>Transport: result / notifications
        alt experimental notification and not opted in
            Transport-->>Client: (notification dropped)
        else opted in or stable notification
            Transport-->>Client: notification delivered
        end
    end
```

### Incoming RPC rejection

After `initialize`, every client request passes through `ClientRequest::experimental_reason()`. When a reason is present and `session.experimental_api_enabled()` is false, the server returns `invalid_request` with code `-32600` (JSON-RPC Invalid Request).

<RequestExample>
```json
{
  "jsonrpc": "2.0",
  "id": 42,
  "error": {
    "code": -32600,
    "message": "thread/realtime/start requires experimentalApi capability"
  }
}
```
</RequestExample>

Descriptor strings in `error.message` identify what triggered the gate:

| Pattern | Example |
| --- | --- |
| Whole method | `mock/experimentalMethod`, `thread/memoryMode/set`, `process/spawn` |
| Field on stable method | `thread/start.mockExperimentalField`, `thread/start.runtimeWorkspaceRoots` |
| Enum variant | `askForApproval.granular` (for `approvalPolicy: { "granular": ... }`) |

`tests/suite/v2/experimental_api.rs` locks these behaviors for representative methods and fields.

### Outgoing notification suppression

Notifications annotated with `#[experimental("...")]` in `app-server-protocol/src/protocol/common.rs` implement `ExperimentalApi`. The transport layer drops them for connections where `experimental_api_enabled` is false—clients see no error; the event simply never arrives. Examples include `thread/realtime/started`, `thread/settings/updated`, `process/outputDelta`, and `process/exited`.

`capabilities.optOutNotificationMethods` is separate: it suppresses **exact** stable or experimental method names the client lists, regardless of experimental opt-in.

### Outgoing server-request field stripping

For `item/commandExecution/requestApproval`, when the client did not opt in, app-server clears `additionalPermissions` before writing the server request to the connection (`CommandExecutionRequestApprovalParams::strip_experimental_fields`). Opted-in clients receive the full payload. This lets stable clients handle basic approvals while experimental clients consume granular permission requests.

## Stable vs experimental schema generation

`codex app-server generate-ts` and `codex app-server generate-json-schema` default to **stable-only** output: experimental methods and fields are filtered from generated TypeScript and JSON Schema bundles. Pass `--experimental` to include the full gated surface. Generated artifacts match the binary version used to run the command.

<CodeGroup>
```bash title="Stable (default)"
codex app-server generate-ts --out DIR
codex app-server generate-json-schema --out DIR
```

```bash title="Include experimental API"
codex app-server generate-ts --out DIR --experimental
codex app-server generate-json-schema --out DIR --experimental
```
</CodeGroup>

Maintainers regenerate checked-in fixtures with:

```bash
just write-app-server-schema
just write-app-server-schema --experimental
```

The protocol crate applies `filter_experimental_schema`, prunes experimental entries from `ClientRequest` unions, and removes experimental type files when `experimental_api` is false during export (`app-server-protocol/src/export.rs`).

<Warning>
Stable schemas intentionally omit experimental methods and fields. Runtime servers still accept gated calls when clients opt in at `initialize`—codegen and live behavior can diverge unless you generate with `--experimental` or read the protocol sources.
</Warning>

## Gating patterns for maintainers

Experimental surface is declared in `codex-app-server-protocol` (primarily `protocol/common.rs` and `protocol/v2/*`), enforced at runtime in `app-server`, and reflected in export filters.

### Method-level gate

Mark the `ClientRequest` variant in `client_request_definitions!`:

```rust
#[experimental("thread/memoryMode/set")]
ThreadMemoryModeSet => "thread/memoryMode/set" {
    params: v2::ThreadMemoryModeSetParams,
    // ...
},
```

The wire method string becomes the rejection descriptor when the whole RPC is experimental.

### Field-level gate on a mostly stable method

Annotate fields on params/response types and derive `ExperimentalApi`:

```rust
#[derive(ExperimentalApi)]
pub struct ThreadStartParams {
    #[experimental("thread/start.mockExperimentalField")]
    pub mock_experimental_field: Option<String>,
    // ...
}
```

In `common.rs`, keep the method stable and set `inspect_params: true` so runtime checks delegate to the params type:

```rust
ThreadStart => "thread/start" {
    params: v2::ThreadStartParams,
    inspect_params: true,
    // ...
},
```

Rejection descriptors use `method.field` (for example `thread/start.mockExperimentalField`).

### Enum variant gate

```rust
#[derive(ExperimentalApi)]
pub enum AskForApproval {
    #[experimental("askForApproval.granular")]
    Granular { /* ... */ },
    // stable variants omit the attribute
}
```

### Nested experimental values

Use `#[experimental(nested)]` on a container field so reasons bubble from nested types (for example `approval_policy: Option<AskForApproval>` inside config shapes).

### Server notifications

Add `#[experimental("thread/realtime/started")]` on the `server_notification_definitions!` variant. No separate client opt-in beyond `experimentalApi`; non-opted connections never see the notification.

### Server-initiated request fields

Annotate experimental fields on server request param types for schema export. For runtime compatibility without opt-in, ensure app-server strips or omits those fields in `filter_outgoing_message_for_connection` (command-execution approval is the reference implementation).

### Registration inventory

The `#[derive(ExperimentalApi)]` macro registers `ExperimentalField` entries via `inventory` for schema filtering and documents type/field/reason triples used by export.

<Check>
After protocol changes: run `just write-app-server-schema` (both stable and `--experimental` when experimental fixtures change), then `just test -p codex-app-server-protocol`.
</Check>

## Representative experimental surface

The README API overview marks many v2 RPCs as experimental; the authoritative list is the `#[experimental(...)]` attributes on `ClientRequest` and `ServerNotification` in `app-server-protocol/src/protocol/common.rs`. Groupings include:

| Category | Examples (require `experimentalApi`) |
| --- | --- |
| Thread lifecycle / settings | `thread/settings/update`, `thread/memoryMode/set`, `thread/turns/list`, `thread/backgroundTerminals/clean`, `thread/search` |
| Realtime | `thread/realtime/start`, `appendAudio`, `appendText`, `stop`, `listVoices` + `thread/realtime/*` notifications |
| Process control | `process/spawn`, `writeStdin`, `kill`, `resizePty` + `process/outputDelta`, `process/exited` |
| Remote control | `remoteControl/enable`, `disable`, `status/read`, `pairing/start` |
| Other | `environment/add`, `collaborationMode/list`, `mock/experimentalMethod`, fuzzy-file-search session RPCs |

Stable methods with common experimental **fields** include `thread/start`, `thread/resume`, `thread/fork`, and `turn/start` (permissions, environments, `dynamicTools`, `runtimeWorkspaceRoots`, granular `approvalPolicy`, etc.).

## Not the same as `experimentalFeature/*`

`experimentalFeature/list` and `experimentalFeature/enablement/set` are **stable** RPCs for product feature-flag metadata and in-memory enablement (`apps`, `plugins`, `remote_control`, etc.). They do not require `capabilities.experimentalApi`. Precedence for enablement is documented in the README: cloud requirements → CLI `--enable` → `config.toml` → `experimentalFeature/enablement/set` → code default.

Use `experimentalApi` for protocol surface that may change without notice; use `experimentalFeature/*` for toggling Codex product features at runtime.

## Verification checklist

| Check | Expected signal |
| --- | --- |
| Initialize without `experimentalApi`, call `mock/experimentalMethod` | `-32600`, message ends with `requires experimentalApi capability` |
| Same client, `thread/start` with only stable fields | Success |
| Opted-in client receives `thread/realtime/started` | Notification on wire |
| Non-opted client during realtime session | No `thread/realtime/started` (dropped) |
| `generate-json-schema` without `--experimental` | No experimental method entries in `codex_app_server_protocol.schemas.json` |
| Regenerate fixtures after protocol edit | `just write-app-server-schema` succeeds; protocol tests pass |

## Related pages

<CardGroup>
<Card title="Connection lifecycle" href="/connection-lifecycle">
`initialize` handshake, `clientInfo`, notification opt-out, and per-connection state.
</Card>
<Card title="Schema generation" href="/schema-generation">
`generate-ts`, `generate-json-schema`, stable vs `--experimental`, and fixture regeneration.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Catalog of v2 methods with stable vs experimental markers.
</Card>
<Card title="Notifications and events" href="/notifications-and-events">
Turn/item notification streams and opt-out behavior.
</Card>
<Card title="Development and testing" href="/development-and-testing">
Integration suites, `TestAppServer`, and `just test -p codex-app-server`.
</Card>
</CardGroup>
