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

MethodDoes
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"),
});
OptionDefault
reconnecttrue
reconnectInterval1000 ms
reconnectMaxInterval30000 ms
reconnectBackoffRate2
reconnectMaxAttemptsInfinity
reconnectJittertrue
heartbeatInterval30000 ms
heartbeatTimeout10000 ms
debugfalse

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 }
ExportValueMeaning
WS_PROTOCOL_VERSION"1.0"Wire protocol the client speaks; the server compares the major version
SDK_VERSIONthe package versionInformational; 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.

MethodDoes
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.

AreaMethods
ChannelslistChannels, createChannel, updateChannel, deleteChannel
WorkspaceslistWorkspaces, createWorkspace
WorkflowslistPublishedWorkflows({ workspaceId, limit }), getWorkflowName
ConnectiongetChannelStatus, getChannelCapabilities, getChannelAuthPattern, connectChannel, completeConnect, disconnectChannel
KeyslistChannelKeys, 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