# SDK Quickstart

> Applies to: ChengOS v0.2.0+ ｜ Last verified: 2026-09-25 ｜ Source: `chengflow-sdk/src/sdk`, `chengflow-sdk/src/hooks/use-channel.ts`, `chengflow-sdk/package.json`, `chengflow-sdk/src/sdk/ws-client.ts`

The gateway SDK (`@chengflow/chat`) puts a ChengOS-backed chat interface into a web application.
It comes in three layers, and you pick how much you want:

| Layer | What it gives you | Use when |
|---|---|---|
| **Core SDK** | Framework-agnostic TypeScript: REST client, session manager, WebSocket client | Vue, Svelte, vanilla, or your own state layer |
| **React hooks** | `useChannel`, `useWebSocket`, `useConversation` | React, with your own UI |
| **React components** | `ChatProvider`, `ChatWindow`, `MessageList`, `InputBar` | React, want it working today |

Read [the gateway model](/docs/sdk-gateway-model) first — the four identifiers below only make
sense against it.

## Install

```bash
npm install @chengflow/chat
```

The published release is **`@chengflow/chat@0.1.0`**. It is **ESM-only** (`"type": "module"`,
`import` conditions only — no CommonJS build) and needs `react` and `react-dom` 18 or newer as
peer dependencies. It has three entry points:

| Import | Contains |
|---|---|
| `@chengflow/chat` | Everything: the core SDK, React hooks, and components |
| `@chengflow/chat/sdk` | The framework-free core only — no React in your bundle |
| `@chengflow/chat/style.css` | Styles for the bundled components; import it once when you use them |

The 0.1.0 package renders assistant replies as Markdown. Math (KaTeX) and the other rich-content
formatting in the source tree arrive with the next published release; do not rely on them from
npm yet.

## Configure

Everything starts from one config object:

```ts
import type { ChannelConfig } from "@chengflow/chat";

const config: ChannelConfig = {
  apiBaseUrl: "https://your-server.example.com/api/v1",
  wsBaseUrl: "wss://your-server.example.com/ws/executions",
  workspaceId: "<workspace-uuid>",   // required, always explicit
  channelId: "weather-app",          // where messages come from
  boundWorkflowId: "<workflow-uuid>",// what runs
  externalUserId: "user-123",        // optional
};
```

`workspaceId` is required and validated — an empty value raises `VALIDATION_ERROR` rather than
silently defaulting, because a wrong workspace would mean cross-tenant routing.

Keep `channelId` and `boundWorkflowId` distinct. The first identifies the app; the second selects
the workflow.

## The fastest path: components

```tsx
import { ChatProvider, ChatWindow } from "@chengflow/chat";
import "@chengflow/chat/style.css";

export function App() {
  return (
    <ChatProvider config={config}>
      <ChatWindow />
    </ChatProvider>
  );
}
```

That is a working chat UI with streaming, history, and approval cards.

## Your own UI: the hook

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

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

  return (
    <div>
      <span>{connectionStatus}</span>
      {messages.map((m) => (
        <div key={m.id}>{m.content}</div>
      ))}
      {streamingContent && <div>{streamingContent}</div>}
      <button disabled={isLoading} onClick={() => sendMessage("Hello")}>
        Send
      </button>
      {error && <p>{error.message}</p>}
    </div>
  );
}
```

`useChannel` runs the whole send-and-await cycle: it creates an optimistic local message, calls
the execute API, subscribes to the conversation and execution scopes, consumes WebSocket events,
and updates message state.

Render `streamingContent` separately from `messages`. It holds the partially-received response
while tokens are still arriving; when the turn completes, the finished text lands in `messages`.
Treating them as one list makes text flicker or duplicate.

`supportsAttachments` is a **capability probe**: it reports whether the bound workflow actually
contains an `io/file_upload` node. Use it to show or hide your attachment button, rather than
offering uploads the workflow will discard.

## Human-in-the-loop

Three callbacks handle the cases where a run needs a person:

| Callback | For |
|---|---|
| `submitApproval(messageId, decision, scope?, reason?)` | An approval request — approve or reject, optionally scoped, optionally with a reason |
| `continueAgentReview(messageId, nextAction?)` | An agent paused for review |
| `submitContextMemoryReview(messageId, updates, trimmingMode)` | Context/memory trimming decisions |

These are not optional extras if your workflows use `utils/approver` or long agent runs — without
them a run that asks for a decision waits indefinitely.

`resetConversation()` starts a fresh conversation, abandoning the current history.

## Core SDK without React

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

const client = new ChannelClient(config);
const result = await client.execute(/* … */);
```

`ChannelClient` also exposes `resolveConversation`, `getConversationMessages`, `createMessage`,
`submitApproval`, `resumeExecution`, `getExecution`, `getExecutionResult`,
`workflowSupportsAttachments`, and `bulkUpdateMemoryControls`.

## Authentication

The browser uses a **JWT and only a JWT.** A `BrowserAuthSession` supplies the token by default;
pass your own `AuthTokenProvider` to the `ChannelClient` constructor to integrate an existing
auth system. Expiry is surfaced through an auth-expired notification so you can re-authenticate
instead of failing silently.

Do not use `chk_` channel keys from a browser — they are for server-side adapters and would put a
long-lived shared secret in client code. See [authentication](/docs/api-authentication).

## WebSocket behaviour

The bundled client handles reconnection with exponential backoff plus jitter, PING/PONG
heartbeats, multi-scope subscriptions, automatic subscription restoration after a reconnect, a
maximum retry limit, and close-code interpretation so a deliberate close is not retried like a
network failure. You do not need to implement any of that.

The client also opens every connection with a **protocol handshake**: its first frame declares
the wire protocol version (`1.0`) and the SDK version. A server that cannot speak that major
version answers `PROTOCOL_MISMATCH` and closes; the client then stops reconnecting and reports
status `protocol_mismatch`, so show an "update the SDK" message rather than a spinner. Servers
that predate the handshake ignore the frame, so a newer SDK keeps working against them.

## Deployment

Docker deployments configure the gateway through `APP_API_BASE_URL`, `APP_WS_URL`,
`APP_CHANNEL_ID`, `APP_BOUND_WORKFLOW_ID`, and `PUBLIC_APP_URL` — see
[the environment reference](/docs/config-env-reference).

## Next steps

- [Gateway model](/docs/sdk-gateway-model) — the mapping this SDK implements.
- [WebSocket API](/docs/api-websocket) — the events underneath.
