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:
| Category | Examples | Notes |
|---|---|---|
| Core | connect, health, status | connect is required first |
| Messaging | send, agent, agent.wait, system-event, logs.tail | methods with side effects require idempotencyKey |
| Chat | chat.history, chat.send, chat.abort | used by WebChat |
| Sessions | sessions.list, sessions.patch, sessions.delete | session management |
| Automation | wake, cron.list, cron.run, cron.runs | wake and cron control |
| Nodes | node.list, node.invoke, node.pair.* | Gateway WS plus node operations |
| Events | tick, presence, agent, chat, health, shutdown | server 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.tsre-exports domain modules underpackages/gateway-protocol/src/schema/*.ts(frames.tsfor top level envelopes and handshake,agent.ts,sessions.ts,cron.ts, and others per feature area).protocol-schemas.tsserves as the centralProtocolSchemasregistry 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:genoutputs a JSON Schema (draft-07) todist/protocol.schema.json.- Swift gateway models are produced by
pnpm protocol:gen:swift. pnpm protocol:checkexecutes 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
connectrequest whose parameters align withConnectParamsis accepted. - Client side: the JS client validates event and response frames prior to using them.
- Feature discovery: the Gateway transmits a cautious
features.methodsandfeatures.eventslist insidehello-ok, sourced fromlistGatewayMethods()andGATEWAY_EVENTS. - That discovery list is not an exhaustive export of every callable helper within
coreGatewayHandlers; certain helper RPCs are present insrc/gateway/server-methods/*.tswithout 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 }.
- 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>;
- Validation
Within packages/gateway-protocol/src/index.ts, export an AJV validator:
export const validateSystemEchoParams = ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);
- 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.
- Regenerate
pnpm protocol:check
- 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
GatewayFrameenum featuringreq,res,event, andunknowncases - strongly typed payload structs/enums
ErrorCodevalues,GATEWAY_PROTOCOL_VERSION, andGATEWAY_MIN_PROTOCOL_VERSION
Unrecognized frame types are retained as raw payloads to maintain forward compatibility.
Versioning and compatibility
PROTOCOL_VERSIONresides inpackages/gateway-protocol/src/version.ts(currently set to4).- Clients transmit
minProtocolandmaxProtocol; 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: falsein most objects. - For IDs, method names, and event names,
NonEmptyString(Type.String({ minLength: 1 })) serves as the standard default. - A discriminator on
typeis applied to the top-levelGatewayFrame. - Side-effect methods generally expect an
idempotencyKeyin their parameters, as seen withsend,poll,agent, andchat.send. - For runtime-generated orchestration context, such as subagent or cron task completion handoff,
agentaccepts an optionalinternalEvents; 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
- Within the owning
packages/gateway-protocol/src/schema/*.tsmodule, update the TypeBox schemas and register them inprotocol-schemas.ts. - In
src/gateway/server-methods-list.ts, register the method or event. - If the new RPC needs operator or node scope classification, update
src/gateway/method-scopes.ts. - Execute
pnpm protocol:check. - Commit the Swift models after regeneration.