Integration Examples

Applies to: ChengOS v0.1.0+ | Last verified: 2026-08-12 | Source: chengflow-sdk/example/App.tsx, chengflow-sdk/src/components/index.ts, chengflow-sdk/src/components/ChatProvider.tsx, chengflow-sdk/src/hooks

Four integration shapes, from "one component" to "no framework at all". Pick the one that matches how much of the UI you want to own.

1. The whole console, one component

The shipped example is nine lines of real code:

import { MultiChannelLayout } from "@chengflow/chat";

const config = {
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL || "/api/v1",
  wsBaseUrl:  import.meta.env.VITE_WS_BASE_URL  || "/ws/executions",
  channelId:  import.meta.env.VITE_CHANNEL_ID   || "weather-app",
  boundWorkflowId: import.meta.env.VITE_BOUND_WORKFLOW_ID || "",
};

export default function App() {
  return <MultiChannelLayout config={config} loginTitle="Chengflow"
                             chatWindowProps={{ height: "100vh" }} />;
}

MultiChannelLayout is the complete browser gateway: login, workspace selection, channel management, and chat — the flow described in Gateway Model. Use it when you want the product, not a component library.

The example also reads a runtime config object off window, falling back to build-time env vars. That pattern is worth copying: it lets one built bundle be deployed against different backends by writing a small config script at serve time, instead of rebuilding per environment.

2. Just the chat window

import { ChatProvider, ChatWindow } from "@chengflow/chat";

<ChatProvider config={{
  apiBaseUrl: "https://api.example.com/api/v1",
  wsBaseUrl:  "wss://api.example.com/ws/executions",
  workspaceId: "…",
  channelId:  "weather-app",
  boundWorkflowId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
}}>
  <ChatWindow />
</ChatProvider>

ChatProvider runs useChannel(config) and injects the result into context; ChatWindow consumes it. Any component under the provider can call useChatContext().

3. Your own UI over the hook

When the chat needs to look like your product:

import { useChannel } from "@chengflow/chat";

function MyChat({ config }) {
  const {
    messages, sendMessage, isLoading, connectionStatus,
    streamingContent, resetConversation, error, supportsAttachments,
  } = useChannel(config);

  return (
    <div>
      <StatusDot status={connectionStatus} />
      {messages.map(m => <MyBubble key={m.id} message={m} />)}
      {streamingContent && <MyBubble message={{ content: streamingContent }} streaming />}
      <MyInput onSend={sendMessage} disabled={isLoading}
               allowFiles={supportsAttachments} />
    </div>
  );
}

useChannel also returns submitApproval, continueAgentReview, and submitContextMemoryReview. If your workflow contains an approval gate, you must render something for it — otherwise the execution sits in waiting_for_review with no way for the user to answer.

supportsAttachments is derived by asking whether the bound workflow contains an io/file_upload node, so the file button appears only where uploads actually go somewhere.

4. No framework

The core classes are plain TypeScript:

import { ChannelClient, SessionManager, WsClient } from "@chengflow/chat/sdk";

const sessions = new SessionManager("weather-app");
const client   = new ChannelClient(config);
const ws       = new WsClient({ url: config.wsBaseUrl, tokenProvider: getToken });

ws.connect();

const sessionId = sessions.getOrCreateSessionId();
const { conversation_id, execution_id } =
  await client.execute("weather-app", workflowId, "What is the weather?", sessionId);

sessions.setConversationId(conversation_id);
ws.send({ type: "SUBSCRIBE", scope: { type: "conversation", conversationId: conversation_id } });
if (execution_id) {
  ws.send({ type: "SUBSCRIBE", scope: { type: "execution", executionId: execution_id } });
}

Note the if (execution_id): it can be null when the message did not trigger a workflow.

Pairing external platforms

To let users connect their own Telegram bot or Slack app from your UI, the SDK ships a pairing form and an edit panel per platform — Telegram, WhatsApp, Slack, WeCom, DingTalk — plus AppLinkPlatformModals to present them together.

The generic path is better than picking a form by hand:

const pattern = await management.getChannelAuthPattern(workspaceId, channelId);
// render fields from `pattern`
const result = await management.connectChannel(workspaceId, channelId, values);
if (result.state === "configuring") {
  // OAuth redirect or QR in setup_data → then completeConnect(...)
}

getChannelAuthPattern returns the fields the platform requires, so one generic form covers every platform and a new adapter needs no frontend change. See Messaging Channels for what each pattern means.

Building a management console

import { ManagementClient, ChannelList, CreateChannelModal, AppShell } from "@chengflow/chat";

listChannels, createChannel, listWorkspaces, and listPublishedWorkflows cover a channel admin screen. AppShell and ChannelList give you the layout if you do not want to build it.

Auth

LoginPage and ResetPasswordPage are drop-in. The example routes /reset-password by reading the token query parameter:

if (window.location.pathname === "/reset-password") {
  const token = new URLSearchParams(window.location.search).get("token");
  return <ResetPasswordPage apiBaseUrl={config.apiBaseUrl} token={token} />;
}

If your application already handles auth, implement AuthTokenProvider and pass it to ChannelClient instead of using BrowserAuthSession.

i18n

I18nProvider, useI18n, and setLocale are exported from the package root — not from components — so import them from @chengflow/chat directly.

Choosing

You wantUse
A working product nowMultiChannelLayout
Chat inside your app's shellChatProvider + ChatWindow
Your own chat UIuseChannel
Non-React, or a serverChannelClient + WsClient + SessionManager

Next steps