Embedding Chatterfly
Add interactive workflow and HITL (human-in-the-loop) sessions to any website or third-party application using the @chatterfly/widget React component.
Overview
The widget lets external participants — customers, reviewers, survey respondents — interact with a running Chatterfly workflow without needing a Chatterfly account. It connects directly to the backend over a WebSocket, renders any HITL session that appears on the run (chat, form, input fields), and hands results back to the workflow automatically.
The package is framework-agnostic React and has zero runtime dependencies beyond react and react-dom. It ships CSS Modules styles in a separate style.css export that you import once.
How it works
- Your server starts a workflow run via
POST /api/v1/runsand mints a short-lived participant token scoped to that run. - Your server passes the
runIdandparticipantTokento the browser (e.g. embedded in your page HTML or returned from an API call). - The
<ChatterflyWidget>component opens a WebSocket to the Chatterfly backend and streams live run events. - When the workflow reaches a HITL step, the widget automatically renders the appropriate UI — a chat window, a form, or individual input fields.
- Submitted responses are sent back to the backend and the workflow resumes. The widget transitions to a completion state when the run finishes.
Installation
Install the package from npm:
npm install @chatterfly/widgetImport the stylesheet once in your app entry point:
import "@chatterfly/widget/style.css";Minting a participant token
Participant tokens are short-lived JWTs scoped to a single run. They must be issued server-side using your API key — never from the browser.
cfw_, minted from the Ship tab). Unlike per-deployment keys (cf_), it survives redeploys and resolves to the workflow's latest active deployment — so your backend never changes when you redeploy. Pass workflow_id in the body; only add deployment_id to pin a specific snapshot.1. Start a run (optional)
If you are starting the run from your own backend:
POST https://your-chatterfly.example.com/api/v1/runs
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json
{
"workflow_id": "wf_abc123",
"input": { "name": "Alice", "issue": "Billing query" }
}Response:
{ "run_id": "run_xyz789" }2. Mint a participant token
POST https://your-chatterfly.example.com/api/v1/runs/run_xyz789/tokens
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json
{
"role": "customer"
}Response:
{ "participant_token": "eyJhbGci..." }Basic usage
import "@chatterfly/widget/style.css";
import { ChatterflyWidget } from "@chatterfly/widget";
export function SupportChat({ runId, token }: { runId: string; token: string }) {
return (
<ChatterflyWidget
runId={runId}
participantToken={token}
apiBase="https://your-chatterfly.example.com"
onCompleted={(output) => console.log("Done:", output)}
onError={(err) => console.error("Error:", err)}
/>
);
}The widget is self-contained and responsive. It adapts its height to content and fills the width of its container.
Configuration & props
| Prop | Type | Required | Description |
|---|---|---|---|
| runId | string | Yes* | The workflow run to connect to. *Not required with workflowId/deploymentId. |
| participantToken | string | Yes* | JWT issued by POST /runs/:id/tokens. *Not required with workflowId/deploymentId. |
| workflowId | string | No | Serverless auto-start; resolves the latest live deployment, survives redeploys. |
| apiBase | string | Yes | Backend base URL. Required — no localhost default. |
| initialInput | Record<string, unknown> | No | Seeds the run's declared inputs on auto-start. |
| className | string | No | Extra CSS class applied to the root element. |
| onCompleted | (output?: Record<string, unknown>) => void | No | Called when the run reaches a terminal completed state, with the final run state. |
| onError | (err: WidgetError) => void | No | Called on connection, config, or runtime errors. See Events & errors below. |
| persistSession | boolean | { storage?, key? } | No | Resume across page refresh for serverless embeds. Default true (sessionStorage) — pass false to opt out. |
| clickToStart | boolean | { label?, description?, icon? } | No | Gate auto-start behind a “Start conversation” button instead of minting a run on mount. |
| resumePrompt | boolean | { title?, continueLabel?, restartLabel? } | No | Show a continue/restart choice card instead of silently resuming a persisted active session. |
| showCompletedHistory | boolean | No | Replay a persisted terminal run read-only with a “Start new conversation” CTA. Default true. |
| connectionPolicy | { idleDisconnectMs?, pollIntervalMs?, hibernateAfterMs? } | "always-on" | No | Tunes the dormant/hibernating connection lifecycle. See Connection Lifecycle. |
Events & errors
Beyond onCompleted/onError, the widget exposes a firehose and granular callbacks, plus an imperative ref:
const ref = useRef<ChatterflyWidgetHandle>(null);
<ChatterflyWidget
ref={ref}
{...props}
onStatusChange={(status) => setStatus(status)}
onSessionOpened={(session) => track("hitl_session_opened", session)}
onMessage={(msg) => track("bot_message", msg)}
onEvent={(event) => analytics.track(event.type, event)}
/>
ref.current?.restart();
ref.current?.disconnect();
ref.current?.sendMessage("hi");onError receives a WidgetError { code, message } — codes include config_missing_api_base, workflow_not_public, no_live_deployment, network, token_expired, and run_failed. It stringifies to message, so existing onError={(err) => console.error(String(err))} handlers keep working.
Theming
Override the widget's appearance with the typed theme prop, or with CSS custom properties on any ancestor element:
<ChatterflyWidget {...props} theme={{ primary: "#0ea5e9", radius: "4px" }} />/* In your stylesheet or a <style> tag */
.my-widget-container {
--cf-primary: #5B4AE4; /* core indigo */
--cf-accent: #F28B30; /* signal orange */
--cf-bg: #ffffff;
--cf-fg: #0C1B3A;
--cf-border: rgba(12, 27, 58, 0.12);
--cf-radius: 10px;
}.cf-* CSS Module class names and respect the custom properties above. See the Theming & Slots page for the full token table, dark mode, and chrome slots (header, footer, bubble overrides). For a docked chat-bubble launcher instead of an inline widget, see Floating Launcher. For how the widget manages its WebSocket connection over the run's lifetime (dormancy, reconnects, token expiry), see Connection Lifecycle.Custom input types
Workflows can specify input_type: "x-signature" (or any x- prefix) for domain-specific fields. Register a React component to handle them:
import { registerInputType } from "@chatterfly/widget";
import { SignaturePad } from "./SignaturePad";
// Register once, before rendering any widget
registerInputType("x-signature", SignaturePad);Your component receives:
interface InputComponentProps {
field: FieldSpec; // full field definition from the workflow
value: unknown; // current value
onChange: (v: unknown) => void;
disabled?: boolean;
}If no matching component is registered, the widget falls back to a plain TextArea.
