TypeBox Schemas: Single Source of Truth for Gateway Protocol

This page explains how TypeBox schemas define the Gateway WebSocket protocol for handshake, request/response patterns, and server events. It is essential for developers working with runtime validation, JSON Schema export, or Swift code generation.

Read this when

  • Updating protocol schemas or codegen

TypeBox is a schema library built with TypeScript from the ground up. OpenClaw relies on it to describe the Gateway WebSocket protocol, including handshake, request/response patterns, and server events. These schemas power runtime validation through AJV, JSON Schema export, and Swift code generation for the macOS application. A single source of truth drives all generated artifacts.

For more on the higher level protocol, see Gateway architecture.

Mental model (30 seconds)

Any Gateway WS message falls into one of three frame types:

  • Request: { type: "req", id, method, params }
  • Response: { type: "res", id, ok, payload | error }
  • Event: { type: "event", event, payload, seq?, stateVersion? }

The initial frame must be a connect request. After that, clients can invoke methods such as health, send, and chat.send, and subscribe to events like presence, tick, and agent.

Minimal connection flow:

Client                    Gateway
  |---- req:connect -------->|
  |<---- res:hello-ok --------|
  |<---- event:tick ----------|
  |---- req:health ---------->|
  |<---- res:health ----------|

Common methods and events:

CategoryExamplesNotes
Coreconnect, health, statusconnect is required first
Messagingsend, agent, agent.wait, system-event, logs.tailmethods with side effects require idempotencyKey
Chatchat.history, chat.send, chat.abortused by WebChat
Sessionssessions.list, sessions.patch, sessions.deletesession management
Automationwake, cron.list, cron.run, cron.runswake and cron control
Nodesnode.list, node.invoke, node.pair.*Gateway WS plus node operations
Eventstick, presence, agent, chat, health, shutdownserver push

The authoritative discovery registry of advertised features is in src/gateway/server-methods-list.ts (listGatewayMethods, GATEWAY_EVENTS).

Where the schemas live

  • Source barrel: packages/gateway-protocol/src/schema.ts re-exports domain modules under packages/gateway-protocol/src/schema/*.ts (frames.ts for top level envelopes and handshake, agent.ts, sessions.ts, cron.ts, and others per feature area). protocol-schemas.ts serves as the central ProtocolSchemas registry linking schema names to their TypeBox definitions.
  • Runtime validators (AJV): packages/gateway-protocol/src/index.ts
  • Advertised feature/discovery registry: src/gateway/server-methods-list.ts
  • Server handshake and method dispatch: src/gateway/server.impl.ts
  • Node client: src/gateway/client.ts
  • Generated JSON Schema: dist/protocol.schema.json (build output, not committed)
  • Generated Swift models: apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift

Current pipeline

  • pnpm protocol:gen outputs a JSON Schema (draft-07) to dist/protocol.schema.json.
  • Swift gateway models are produced by pnpm protocol:gen:swift.
  • pnpm protocol:check executes both generators and confirms the Swift output is committed (the JSON Schema output is treated as a build artifact that git ignores).

How the schemas are used at runtime

  • Server side: every inbound frame undergoes AJV validation. During the handshake, only a connect request whose parameters align with ConnectParams is accepted.
  • Client side: the JS client validates event and response frames prior to using them.
  • Feature discovery: the Gateway transmits a cautious features.methods and features.events list inside hello-ok, sourced from listGatewayMethods() and GATEWAY_EVENTS.
  • That discovery list is not an exhaustive export of every callable helper within coreGatewayHandlers; certain helper RPCs are present in src/gateway/server-methods/*.ts without being listed in the advertised feature set.

Example frames

Connect (first message):

{
  "type": "req",
  "id": "c1",
  "method": "connect",
  "params": {
    "minProtocol": 3,
    "maxProtocol": 4,
    "client": {
      "id": "openclaw-macos",
      "displayName": "macos",
      "version": "1.0.0",
      "platform": "macos 15.1",
      "mode": "ui",
      "instanceId": "A1B2"
    }
  }
}

Hello-ok response:

{
  "type": "res",
  "id": "c1",
  "ok": true,
  "payload": {
    "type": "hello-ok",
    "protocol": 4,
    "server": { "version": "dev", "connId": "ws-1" },
    "features": { "methods": ["health"], "events": ["tick"] },
    "snapshot": {
      "presence": [],
      "health": {},
      "stateVersion": { "presence": 0, "health": 0 },
      "uptimeMs": 0
    },
    "auth": { "role": "operator", "scopes": ["operator.read"] },
    "policy": { "maxPayload": 1048576, "maxBufferedBytes": 1048576, "tickIntervalMs": 30000 }
  }
}

Request and response:

{ "type": "req", "id": "r1", "method": "health" }
{ "type": "res", "id": "r1", "ok": true, "payload": { "ok": true } }

Event:

{ "type": "event", "event": "tick", "payload": { "ts": 1730000000 }, "seq": 12 }

Minimal client (Node.js)

Minimal working flow: connect followed by health.

import { WebSocket } from "ws";

const ws = new WebSocket("ws://127.0.0.1:18789");

ws.on("open", () => {
  ws.send(
    JSON.stringify({
      type: "req",
      id: "c1",
      method: "connect",
      params: {
        minProtocol: 4,
        maxProtocol: 4,
        client: {
          id: "cli",
          displayName: "example",
          version: "dev",
          platform: "node",
          mode: "cli",
        },
      },
    }),
  );
});

ws.on("message", (data) => {
  const msg = JSON.parse(String(data));
  if (msg.type === "res" && msg.id === "c1" && msg.ok) {
    ws.send(JSON.stringify({ type: "req", id: "h1", method: "health" }));
  }
  if (msg.type === "res" && msg.id === "h1") {
    console.log("health:", msg.payload);
    ws.close();
  }
});

Worked example: add a method end-to-end

Example: introduce a new system.echo request that delivers { ok: true, text }.

  1. Schema (source of truth)

Append to packages/gateway-protocol/src/schema/system.ts (or the nearest matching feature module):

export const SystemEchoParamsSchema = Type.Object(
  { text: NonEmptyString },
  { additionalProperties: false },
);

export const SystemEchoResultSchema = Type.Object(
  { ok: Type.Boolean(), text: NonEmptyString },
  { additionalProperties: false },
);

Bring both into packages/gateway-protocol/src/schema/protocol-schemas.ts, register them in the ProtocolSchemas registry, and export the derived types:

  SystemEchoParams: SystemEchoParamsSchema,
  SystemEchoResult: SystemEchoResultSchema,
export type SystemEchoParams = Static<typeof SystemEchoParamsSchema>;
export type SystemEchoResult = Static<typeof SystemEchoResultSchema>;
  1. Validation

Within packages/gateway-protocol/src/index.ts, export an AJV validator:

export const validateSystemEchoParams = ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);
  1. Server behavior

Place a handler inside src/gateway/server-methods/system.ts:

export const systemHandlers: GatewayRequestHandlers = {
  "system.echo": ({ params, respond }) => {
    const text = String(params.text ?? "");
    respond(true, { ok: true, text });
  },
};

Register it in src/gateway/server-methods.ts (which already merges systemHandlers), then include "system.echo" in the listGatewayMethods input located in src/gateway/server-methods-list.ts.

If operator or node clients can invoke the method, also categorize it in src/gateway/method-scopes.ts so scope enforcement and hello-ok feature advertising stay consistent.

  1. Regenerate
pnpm protocol:check
  1. Tests and docs

Insert a server test in src/gateway/server.*.test.ts and document the method.

Swift codegen behavior

The Swift generator produces:

  • a GatewayFrame enum featuring req, res, event, and unknown cases
  • strongly typed payload structs/enums
  • ErrorCode values, GATEWAY_PROTOCOL_VERSION, and GATEWAY_MIN_PROTOCOL_VERSION

Unrecognized frame types are retained as raw payloads to maintain forward compatibility.

Versioning and compatibility

  • PROTOCOL_VERSION resides in packages/gateway-protocol/src/version.ts (currently set to 4).
  • Clients transmit minProtocol and maxProtocol; the server rejects ranges that do not encompass its current protocol.
  • The Swift models preserve unknown frame types to prevent breaking older clients.

Schema patterns and conventions

  • Strict payloads are typically handled through additionalProperties: false in most objects.
  • For IDs, method names, and event names, NonEmptyString (Type.String({ minLength: 1 })) serves as the standard default.
  • A discriminator on type is applied to the top-level GatewayFrame.
  • Side-effect methods generally expect an idempotencyKey in their parameters, as seen with send, poll, agent, and chat.send.
  • For runtime-generated orchestration context, such as subagent or cron task completion handoff, agent accepts an optional internalEvents; treat this as an internal API surface.

Live schema JSON

The generated JSON Schema is a build artifact and should not be committed to the repository. The published raw file is generally accessible at:

When you change schemas

  1. Within the owning packages/gateway-protocol/src/schema/*.ts module, update the TypeBox schemas and register them in protocol-schemas.ts.
  2. In src/gateway/server-methods-list.ts, register the method or event.
  3. If the new RPC needs operator or node scope classification, update src/gateway/method-scopes.ts.
  4. Execute pnpm protocol:check.
  5. Commit the Swift models after regeneration.