SDK API Reference
Applies to: ChengOS v0.2.0+ | Last verified: 2026-09-25 | Source:
chengflow-sdk/src/sdk/index.ts,chengflow-sdk/src/sdk/channel-client.ts,chengflow-sdk/src/sdk/ws-client.ts,chengflow-sdk/src/sdk/management-client.ts,chengflow-sdk/src/sdk/session-manager.ts
The SDK is plain TypeScript with no framework dependency. React hooks and components are built on top of it, but the core classes work anywhere.
import {
ChannelClient, ChannelClientError,
ManagementClient, AuthClient, BrowserAuthSession,
SessionManager, ExecutionMappingStore, WsClient,
} from "@chengflow/chat/sdk";
@chengflow/chat/sdk is the framework-free entry point of the @chengflow/chat package (ESM
only). The package root, @chengflow/chat, re-exports the same classes together with the React
layer. There is no separate @chengflow/sdk package.
ChannelConfig
Every client takes the same config shape:
{
apiBaseUrl: "https://api.example.com/api/v1",
wsBaseUrl: "wss://api.example.com/ws/executions",
workspaceId: "…",
channelId: "weather-app",
boundWorkflowId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
}
workspaceId is required — omitting it throws a VALIDATION_ERROR rather than falling back to
a default workspace.
ChannelClient
The REST client. Authorization is handled for you: a token provider supplies the JWT, refresh is automatic, and an unrecoverable auth failure notifies through the auth-expired channel rather than throwing in a random call site.
const client = new ChannelClient(config);
execute() — the method you will use most
const { conversation_id, workflow_id, execution_id } =
await client.execute(channelId, workflowId, message, sessionId?, externalUserId?, attachments?);
It is a two-step JWT flow, not one endpoint:
POST /workspaces/:workspace_id/conversations/resolve → conversation_id
POST /conversations/:id/messages → execution_id
external_user_id defaults to web-user; external_chat_id comes from sessionId, defaulting
to a timestamped one. mode is workflow_chat and extra_context carries the channel_id.
execution_id may be null when the message did not trigger a workflow. Handle that
instead of assuming a run started.
The rest
| Method | Does |
|---|---|
resolveConversation(workspaceId, req) | Resolve or create a conversation for (app_id, external_user_id, external_chat_id) |
createMessage(conversationId, req) | Post a message; role, content, optional references, model, external_message_id, attachments |
getConversationMessages(conversationId) | History |
workflowSupportsAttachments(workflowId) | True if the workflow contains an io/file_upload node |
submitApproval(...) | Submit an approval decision and scope |
resumeExecution(executionId) | Resume a paused execution |
bulkUpdateMemoryControls(...) | Update memory controls in bulk |
getExecution(executionId) | Execution detail |
getExecutionResult(executionId) | The result payload |
workflowSupportsAttachments is how the UI decides whether to show the file button — ask the
workflow rather than guessing from configuration.
Failures throw ChannelClientError carrying a code, message, and HTTP status, so you can branch
on the code instead of parsing text.
WsClient
const ws = new WsClient({
url: "wss://api.example.com/ws/executions",
tokenProvider: () => localStorage.getItem("access_token"),
});
| Option | Default |
|---|---|
reconnect | true |
reconnectInterval | 1000 ms |
reconnectMaxInterval | 30000 ms |
reconnectBackoffRate | 2 |
reconnectMaxAttempts | Infinity |
reconnectJitter | true |
heartbeatInterval | 30000 ms |
heartbeatTimeout | 10000 ms |
debug | false |
tokenProvider is called before every connection, and the token is appended as ?token=.
That matters for long-lived pages: a client that captured the token once would reconnect with an
expired one.
Subscriptions are per scope:
ws.send({ type: "SUBSCRIBE", scope: { type: "conversation", conversationId } });
ws.send({ type: "SUBSCRIBE", scope: { type: "execution", executionId } });
ws.send({ type: "UNSUBSCRIBE", scope: { type: "execution", executionId } });
Scopes are workspace, conversation, and execution. Subscriptions are restored
automatically after a reconnect — you do not re-subscribe by hand.
Heartbeats are PING / PONG with an optional data field and timestamp.
Field casing — the detail that saves an afternoon
The backend wraps every message in a WsEnvelope with a SCREAMING_SNAKE_CASE type. Event
fields on the wire are a mix of snake_case and camelCase across message types. WsClient
normalises everything to camelCase before dispatch, so application code consumes camelCase
only.
Do not write code against the raw wire shape.
Events
Conversation scope: MESSAGE_CREATED, MESSAGE_COMPLETED, WORKFLOW_TRIGGERED,
CONVERSATION_CREATED.
Execution scope: EXECUTION_START, EXECUTION_PROGRESS, EXECUTION_COMPLETE,
EXECUTION_FAILED, EXECUTION_CANCELLED, EXECUTION_STATE_CHANGED, the NODE_STREAM_*
family, plus approval and agent-review events.
Protocol handshake
Every connection starts with a handshake frame, sent before any subscription:
{ type: "HANDSHAKE", protocol_version: WS_PROTOCOL_VERSION, client_version: SDK_VERSION }
| Export | Value | Meaning |
|---|---|---|
WS_PROTOCOL_VERSION | "1.0" | Wire protocol the client speaks; the server compares the major version |
SDK_VERSION | the package version | Informational; the server logs it |
PROTOCOL_MISMATCH_CODE | "PROTOCOL_MISMATCH" | Error code a server sends before closing when it cannot speak that major version |
On PROTOCOL_MISMATCH the client stops reconnecting, reports status protocol_mismatch, and
getProtocolRejectionMessage() returns the server's explanation for your UI. A server that
predates the handshake ignores the frame, so the client stays compatible with it.
SessionManager
Owns the browser half of the session mapping, keyed per channelId and persisted in
localStorage.
| Method | Does |
|---|---|
getOrCreateSessionId() | Stable UUID v4 per channel, surviving reloads |
resetSession() | New session id; clears the conversation id — the browser's ~new |
setConversationId(id) / getConversationId() / clearConversationId() | Conversation binding |
listSessions() / createSession(label?) / renameSession(id, label) | Multiple sessions per channel |
togglePinSession(id) / deleteSession(id) | Session list management |
getActiveSessionId() / setActiveSessionId(id) | Which one is current |
clear() | Wipe everything for this channel |
The session id maps to the backend's external_chat_id; combined with (workspace_id, app_id)
it determines a conversation. See Channel Routing.
A custom Storage can be injected — the SDK falls back to an in-memory store outside the
browser, so it is testable and SSR-safe.
ManagementClient
For building a console rather than a chat window.
| Area | Methods |
|---|---|
| Channels | listChannels, createChannel, updateChannel, deleteChannel |
| Workspaces | listWorkspaces, createWorkspace |
| Workflows | listPublishedWorkflows({ workspaceId, limit }), getWorkflowName |
| Connection | getChannelStatus, getChannelCapabilities, getChannelAuthPattern, connectChannel, completeConnect, disconnectChannel |
| Keys | listChannelKeys, deleteChannelKey |
getChannelAuthPattern returns the fields a given platform needs, so a pairing form can be
rendered generically instead of hard-coding one form per platform. connectChannel /
completeConnect mirror the two-step connection lifecycle described in
Messaging Channels.
AuthClient and BrowserAuthSession
AuthClient handles login and password reset. BrowserAuthSession implements
AuthTokenProvider over localStorage, using the exported ACCESS_TOKEN_KEY and
REFRESH_TOKEN_KEY, and is the default provider when you do not pass one.
Implement AuthTokenProvider yourself if your host application already owns the tokens.
ExecutionMappingStore
Tracks the mapping from a message to its execution and the execution's client-side status —
which is what lets a UI show a spinner on the right bubble when several turns are in flight.
isTerminalExecutionStatus() and the execution-status exports classify backend statuses into
client ones.
React layer
<ChatProvider config={config}>
<ChatWindow />
</ChatProvider>
useChannel(config) is the core hook and returns:
{ messages, sendMessage, isLoading, connectionStatus, streamingContent,
resetConversation, submitApproval, continueAgentReview,
submitContextMemoryReview, error, supportsAttachments }
ChatProvider simply injects useChannel's return value into context. Other hooks:
use-websocket, use-conversation, use-channels, use-channel-connection, use-auth.
Next steps
- SDK Quickstart — the shortest path to a working chat.
- Integration Examples — putting these together.
- WebSocket API — the protocol underneath
WsClient.