> ## Documentation Index
> Fetch the complete documentation index at: https://help.statisfy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Digital Worker Chat

> Embed the DigitalWorkerChat widget, pin onboarding threads, render inline forms, and drive conversations with the headless client.

The chat components let your customers converse with one of your [Digital Workers](/agent-studio/agents/overview) from inside your app — with streaming replies, inline forms, conversation history, unread badges, and live replies from a human CSM.

All components on this page must be rendered inside a [`<StatisfyProvider>`](/sdk/v0/getting_started).

## `DigitalWorkerChat`

The full chat widget. Outside a Statisfy portal, the only prop you need is the worker to talk to; inside a portal, see [Config mode](#config-mode-inside-a-statisfy-portal).

```tsx theme={null}
import { DigitalWorkerChat } from '@statisfy/digital-workers-react'

<DigitalWorkerChat
  workerId="your-digital-worker-uuid"
  workerName="Onboarding Assistant"
  greeting="Hi! Ask me anything about getting set up."
  className="dw-h-[600px]"
/>
```

### Props

| Prop                   | Type                      | Default            | Description                                                                                                                                                                                                                                                         |
| ---------------------- | ------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workerId`             | `string`                  | —                  | UUID of the Digital Worker to converse with. Required for non-portal hosts. **Ignored (with a dev-mode warning) when `configId` is set** — in a portal the worker comes from `portal.runtime.json`.                                                                 |
| `configId`             | `string`                  | —                  | Stable instance identity for portal runtime config, e.g. `"support"`. **Required in Statisfy portals** — see [Config mode](#config-mode-inside-a-statisfy-portal). Omit for non-portal hosts.                                                                       |
| `workerName`           | `string`                  | `"Digital Worker"` | Display name in the header.                                                                                                                                                                                                                                         |
| `greeting`             | `string`                  | —                  | First message shown before the user types.                                                                                                                                                                                                                          |
| `className`            | `string`                  | —                  | Extra classes on the root container (e.g. height).                                                                                                                                                                                                                  |
| `initialThreadId`      | `string`                  | —                  | Pin a deterministic thread on the first turn. Consulted only before the first message; later turns continue the server's thread.                                                                                                                                    |
| `formRenderer`         | `FormRenderer`            | —                  | Render callback for inline `present_form` tool calls. See [Inline forms](#inline-forms).                                                                                                                                                                            |
| `autoLoadConversation` | `boolean`                 | `false`            | On mount, load prior history **and** the orchestrator's current next action (e.g. the form to show) without faking a user turn.                                                                                                                                     |
| `autoKickoff`          | `boolean`                 | `false`            | For a **fresh** pinned thread with no history, post a system-trigger `kickoff` turn so the agent greets and presents the first action without the user typing. A resumed thread never re-kicks. Requires `autoLoadConversation` **and** a pinned `initialThreadId`. |
| `kickoffPrompt`        | `string`                  | generic trigger    | System-stimulus text for the kickoff turn. Fed to the agent as its inbound turn but never persisted or shown as a user message. Only used when `autoKickoff` fires.                                                                                                 |
| `scope`                | `string`                  | —                  | Conversation scope sent to the backend. `'onboarding'` keys the account's single onboarding conversation (resolved by scope, not thread id). Omit for general chat.                                                                                                 |
| `userKey`              | `string`                  | —                  | Stable id for the signed-in end-user (e.g. your own user id). Namespaces per-device client state — resumed thread and per-thread read state — so switching users on a shared browser can't resume another user's conversations. Omit for single-user embeds.        |
| `presented`            | `boolean`                 | `true`             | Whether the chat is currently on-screen. Pass your launcher's open state; while `false`, inbound replies accrue as unread instead of being auto-read.                                                                                                               |
| `onUnreadChange`       | `(count: number) => void` | —                  | Fires when the total unread count across the customer's threads changes. Badge your own launcher with it.                                                                                                                                                           |
| `onClose`              | `() => void`              | —                  | When set, renders a close (✕) button in the header that calls this. Omit for always-visible inline embeds.                                                                                                                                                          |
| `history`              | `boolean`                 | `true`             | Enable the history panel, unread badge + "new reply" toast, and the background poll that powers them.                                                                                                                                                               |
| `newChat`              | `boolean`                 | `true`             | Show a "New chat" affordance.                                                                                                                                                                                                                                       |

### Config mode (inside a Statisfy portal)

In a Statisfy-hosted [Portal](/portals/overview), you don't hardcode the worker — the portal's `portal.runtime.json` holds it, so a non-developer can repoint the chat from **Settings** without a code change. Pass a `configId` instead of a `workerId`:

```tsx theme={null}
// Inside a portal template — the worker comes from portal.runtime.json.
<DigitalWorkerChat configId="support" />
```

```json theme={null}
// portal.runtime.json
{
  "shared": {},
  "components": {
    "digital_worker_chat": {
      "support": { "worker_id": "your-digital-worker-uuid" }
    }
  }
}
```

Config is keyed by `(component type, configId)`, so two instances sharing a `configId` intentionally share one config slot.

<Warning>
  The `configId` must be a **literal string**. The portal save gate extracts it into the component manifest and **rejects a missing or non-literal `configId`** — `configId={someVariable}` fails validation. When the config value is unset, the component renders its not-configured state instead of chatting.
</Warning>

### Embedding in a launcher

For a collapsible chat bubble, pass your open state to `presented` and badge the launcher with `onUnreadChange`:

```tsx theme={null}
function ChatLauncher() {
  const [open, setOpen] = useState(false)
  const [unread, setUnread] = useState(0)

  return (
    <>
      <button onClick={() => setOpen(true)}>
        Chat {unread > 0 && <span className="badge">{unread}</span>}
      </button>
      {open && (
        <DigitalWorkerChat
          workerId="your-digital-worker-uuid"
          presented={open}
          onUnreadChange={setUnread}
          onClose={() => setOpen(false)}
        />
      )}
    </>
  )
}
```

<Note>
  **Unread is tracked client-side** (per-thread last-seen counts in `localStorage`, refreshed by a background poll roughly every 30s). The active thread auto-marks read while `presented` is `true`.
</Note>

### Live CSM replies

When a human CSM replies to a conversation from Statisfy, the chat detects the new message on its next poll and surfaces it in the open thread — and as unread elsewhere. No extra wiring is needed; just keep `history` enabled (the default).

## `OnboardingModule`

A thin wrapper around `DigitalWorkerChat` for onboarding surfaces. It:

* Reads the `customer_id` from the session token and pins `initialThreadId` to `onboarding:<customer_id>` — so the same customer always resumes the same onboarding thread.
* Enables `autoLoadConversation`, so the chat opens with the right form (or closing message) already on screen and prior history restored across refreshes.
* Defaults `autoKickoff` to `true` with an onboarding-specific `kickoffPrompt`, so the agent greets and presents the first step without the customer typing.
* Sets `scope` to `'onboarding'`.
* Forces `history` and `newChat` **off**: onboarding is a single pinned thread per account, so there's no thread switcher, no "New chat", and no per-user unread badge.
* Defaults `formRenderer` to `DefaultFormRenderer`.

```tsx theme={null}
import { OnboardingModule } from '@statisfy/digital-workers-react'

<OnboardingModule workerId="your-onboarding-worker-uuid" />

// Inside a portal, use configId instead (component type: onboarding_module).
<OnboardingModule configId="onboarding" />
```

`OnboardingModuleProps` is `DigitalWorkerChatProps` **minus** `initialThreadId`, `autoLoadConversation`, `history`, `newChat`, and `scope` — all five are derived or forced by the module, so accepting them would create two sources of truth. Every other chat prop (`configId`, `workerName`, `greeting`, `className`, `formRenderer`, `autoKickoff`, `kickoffPrompt`, `presented`, `onUnreadChange`, `onClose`, `userKey`) passes straight through.

## Inline forms

When the agent needs structured input it emits a `present_form` tool call. Provide a `formRenderer` to render it as a real form; on submit, the chat posts a new turn with the collected values as `structured_input`.

The SDK ships a ready-made renderer, `DefaultFormRenderer`:

```tsx theme={null}
import { DigitalWorkerChat, DefaultFormRenderer } from '@statisfy/digital-workers-react'

<DigitalWorkerChat workerId="…" formRenderer={DefaultFormRenderer} />
```

To render forms in your own design system, supply a custom `FormRenderer`:

```tsx theme={null}
import type { FormRenderer } from '@statisfy/digital-workers-react'

const myFormRenderer: FormRenderer = (formSpec, ctx) => (
  <form
    onSubmit={(e) => {
      e.preventDefault()
      const data = Object.fromEntries(new FormData(e.currentTarget))
      ctx.onSubmit(data) // posts a new turn with structured_input
    }}
  >
    <p>{formSpec.prompt}</p>
    {formSpec.fields.map((f) => (
      <label key={f.key}>
        {f.label}
        <input name={f.key} placeholder={f.placeholder} required={f.mandatory} />
      </label>
    ))}
    <button type="submit" disabled={ctx.disabled}>Submit</button>
  </form>
)
```

`FormSpec` has `{ prompt, fields }`; each `FormField` has `{ key, label, type, options?, placeholder?, mandatory? }`. The `ctx.onSubmit(values)` call replays the turn with those values; `ctx.disabled` is `true` while a submission is in flight.

<Note>
  If you omit `formRenderer`, `present_form` calls render as a generic "tool running" chip and the agent falls back to free-text input.
</Note>

## Headless client

Need full control of the conversation UI? Skip the components and drive the transport directly with `createDigitalWorkerClient`. (The provider isn't required for the headless client — you pass config explicitly.)

```ts theme={null}
import { createDigitalWorkerClient } from '@statisfy/digital-workers-react'

const client = createDigitalWorkerClient({
  baseUrl: 'https://api.statisfy.app',
  workerId: 'your-digital-worker-uuid',
  getToken: async () => sessionToken,
  publishableKey: 'pk_live_your_key'
})
```

The client exposes four methods:

### `sendMessage(content, threadId?, structuredInput?, formPrompt?)`

One-shot send; resolves with the full reply.

```ts theme={null}
const res = await client.sendMessage('What are my open tasks?')
// res: { reply, thread_id, conversation_id, agent_message_id, pending_approval?, next_action? }
console.log(res.reply, res.thread_id)
```

### `streamMessage(options)`

Streams the reply token-by-token over Server-Sent Events. Drive your UI from the handler callbacks.

```ts theme={null}
await client.streamMessage({
  content: 'Walk me through onboarding',
  threadId: existingThreadId, // optional
  handlers: {
    onTextDelta: (chunk) => appendToBubble(chunk),
    onToolCallStarted: (tool) => showToolChip(tool),
    onToolCallCompleted: (tool) => clearToolChip(tool),
    onToolCallError: (tool, err) => showError(tool, err),
    onDone: ({ threadId, conversationId, agentMessageId, nextAction }) => finish(threadId),
    onError: (err) => showError(err.message)
  },
  signal: abortController.signal // optional; abort on unmount / thread switch
})
```

### `loadConversation({ threadId, signal? })`

Load a thread's prior messages plus the orchestrator's current next action.

```ts theme={null}
const snapshot = await client.loadConversation({ threadId })
// snapshot: { thread_id, conversation_id, messages[], next_action? }
```

Each message includes `role` (`user` | `assistant` | `system`), `content`, and a `sender_kind` (`customer` | `agent` | `teammate`) so you can distinguish AI replies from a human CSM's.

### `listConversations({ source?, limit?, signal? })`

List the customer's conversations for a history panel.

```ts theme={null}
const { conversations, total } = await client.listConversations({ limit: 20 })
// each: { thread_id, conversation_id, title, last_message_at, message_count }
```

Statisfy doesn't store read-state — compute unread client-side by diffing each thread's `message_count` against the last count you saw.

See the [API Reference](/sdk/v0/api_reference) for the underlying endpoints, request/response shapes, and error codes.
