Your app gets one WebSocket gateway. It holds no business logic: it connects a browser, puts it on a channel, and hands each message to an action that does the work. The action decides who hears back — just the sender, or everyone on the channel.

Connect and send

Open wss://<your app's WebSocket endpoint>?token=<the user's sign-in token>. A missing or invalid token is refused. Then send JSON messages:

json
{ "action": "customapi_run", "channel": "order-42", "message": { "apiName": "summarize_order", "input": { "orderId": "42" } } }
FieldMeaning
actionWhich handler runs: customapi_run (any Logic Block), crew_run (a crew), agent_activity_subscribe (the live activity feed).
channelAny name your app chooses — a conversation id, a room, a run id.
messageThe action's input.

Replies arrive as { type, data } frames — for example ai_token chunks as an answer is written, then customapi_result. Failures look like { type: "error", data: { code } } with MISSING_FIELDS, NOT_CONNECTED, UNKNOWN_ACTION or HANDLER_ERROR.

Channels

You name the channel; the platform decides whose it is. Behind the scenes it is stored under your org and tenant, taken from the sign-in token and never from the message. So two tenants can both use general without ever meeting, and no client can join another org's channel by guessing its name. Everyone in the same tenant who uses the same name is on the same channel — that is how a group forms. A connection is on one channel at a time; a message that names a different channel moves it there.

One or everyone

An action never pushes to browsers itself. It either returns a result — unicast (just the sender), broadcast (everyone on the sender's channel), stream (it already pushed) or silent — or it calls the notifier once per event. A notifier call that names one connection reaches only that browser; one that names only a channel reaches every active connection on it. The same gateway therefore does a private reply, a progress stream, a presence list and a group chat.

What you get today

A Logic Block or crew replies to the browser that asked. A Logic Block can also share its result with everyone on the channel (see below), and a crew run shares its events with everyone watching the tenant's activity feed.

Notify everyone from a Logic Block

To send a Logic Block's result to everyone on the channel, make the block set a boolean variable named `notifyall` to `true` (a create_variable or set_variable step). The caller still gets customapi_result; everyone on the channel — the caller too — also gets { type: "customapi_message", data: { output, from } }, where from is the caller's user id so a client can recognise its own message. Only a real true counts, a failed run never broadcasts, and the channel is always the caller's own org and tenant. The HTTPS endpoint ignores the variable.

Channels are not private inside a tenant

Anyone in the same tenant who names a channel joins it, so use unguessable channel names for anything private and check who is allowed inside the Logic Block.

Building a chat

An AI chat needs nothing new: a Logic Block with an ai_call step, run over customapi_run, plus an entity that keeps the history. For a room with several people, the console's own team chat is the pattern: join returns the recent messages and who is online; send saves the message and pushes it to the other connections on the channel (the room name is the channel); a private room pushes straight to each participant's connections.