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

# Widgets & blocks

> How FirstFlow represents in-chat UI as a declarative WidgetTree of block primitives, how it gets composed and pushed, and how blocks bind, render, and fire actions.

Everything FirstFlow renders inside your chat UI a survey, a tour card, an AI-composed guide is one **`WidgetTree`**: a small, declarative JSON document built from typed **block** primitives. The backend produces the tree (authored verbatim for tours and surveys, or composed by an LLM for guides) and pushes it to the browser over Socket.IO; [`@firstflow/react`](/react/overview) walks the tree and renders DOM.

You rarely hand-write a tree. But the shape is the contract between the server that decides what to show and the browser that shows it, so understanding it explains how dynamic text, conditional visibility, and button actions actually work and why some actions resolve in the browser while others travel back to the server.

## How it works

A `WidgetTree` is a render document, not a component. It declares a single root `block`, an optional `initialState` bag, and a header. The browser renderer is a passive interpreter: it reads the tree, seeds widget state from `initialState`, and renders each block by type. Nothing in the tree calls your code directly interactivity is expressed as **actions** that the renderer dispatches, and **refs** that read live values out of the state bag.

The tree never originates in the browser. Eligibility triggers, audience, schedule, frequency, and any classifier gate is evaluated [server-side](/concepts/triggers-and-audience) in the conversation router. When a user qualifies, the backend resolves the [experience's flow](/concepts/flows-and-nodes) to a result, composes a `WidgetTree`, validates it against the server's mirror of the block schema, and emits a `widget.show` event to that user's Socket.IO room. The browser SDK is a passive consumer: it renders what arrives and reports interactions back. That is why you cannot force a widget to appear purely from client code see [Realtime](/react/realtime).

```mermaid theme={null}
flowchart TD
  A["Host chat message<br/>(observed by @firstflow/sdk)"] --> B["Conversation router<br/>eligibility: triggers, audience,<br/>schedule, frequency, classifier"]
  B --> C["Flow walked to a result<br/>(guide / tour / survey / announcement)"]
  C --> D{Experience type}
  D -->|guide| E["WidgetCompositionService<br/>Anthropic claude-sonnet-4-6<br/>renders a WidgetTree"]
  D -->|tour / survey<br/>announcement| F["Authored WidgetTree<br/>from flow nodes"]
  E --> G["Validate against widgetTreeSchema"]
  F --> G
  G --> H["RealtimeService.pushToUser<br/>emit widget.show { tree }"]
  H --> I["@firstflow/react<br/>renders block primitives → DOM"]
  I --> J["User interacts → widget.action<br/>local resolves in browser;<br/>host/remote travel back"]
  classDef server fill:#0d9488,stroke:#0f766e,color:#fff
  classDef ai fill:#7c3aed,stroke:#6d28d9,color:#fff
  classDef client fill:#ea580c,stroke:#c2410c,color:#fff
  class B,C,F,G,H server
  class E ai
  class I,J client
  class A client
```

The two production paths differ only in who fills the tree. **Guides** declare a skeleton with placeholders and let an LLM fill it. **Tours, surveys, and announcements** are authored block-by-block in the flow editor and rendered verbatim. Both end at the same `widgetTreeSchema`, so the browser renderer treats every tree identically once it arrives.

## The WidgetTree shape

The top-level document is intentionally tiny one root block plus state and a header. It lives in [`@firstflow/widget-kit`](/widget-kit/overview) and is mirrored as a Zod schema on the backend so a composed tree is rejected before it can reach a browser.

```ts theme={null}
type WidgetTree = {
  schemaVersion: 1 | 2;                  // 1 = legacy translator output; 2 = authored (may declare slots)
  widgetId: string;
  header: { title: string; icon?: string | IconValue };
  block: Block;                          // the single root block everything nests under it
  initialState?: Record<string, unknown>;// seeds the state bag refs read from
  slots?: Slot[];                        // v2 only AI-fillable placeholders (guides)
};
```

`initialState` is the starting value of the widget's **state bag** a flat key/value object that block `bind` targets write into and `ref` reads pull from. A `text_input` bound to `{ ref: "form.email" }` writes the typed value at `form.email`; a `text` segment `{ ref: "form.email" }` reads it back. State is per-widget and lives in the browser for the life of the rendered widget.

`schemaVersion` distinguishes a legacy v0/v1 translated widget from a v2 authored tree. Only v2 trees may declare `slots`, the placeholder manifest used by guides (covered below). The `widgetId` is the stable identity the server uses when it later pushes a `state.patch` or a replacement tree to the same widget.

## Block primitives

A block is a discriminated union on `type`. Blocks nest: layout blocks hold `children`, and every block can carry the universal `BlockMixin` fields. They fall into four working categories.

| Category        | Blocks                                                               | What they do                                                                                                                                         |
| --------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Layout**      | `stack`, `row`, `card`, `carousel`                                   | Group and position children. `stack` is vertical, `row` is horizontal, `card` is a bordered container, `carousel` is a swipeable track.              |
| **Content**     | `title`, `text`, `badge`, `divider`, `media`, `embed`                | Static or ref-bound display. `media` renders emoji / image / video; `embed` renders an iframe.                                                       |
| **Interactive** | `button`, `pill_group`, `text_input`, `checkbox`, `slider`, `rating` | Collect input or fire actions. Each writes to the state bag via `bind` and/or dispatches actions.                                                    |
| **Compound**    | `repeat`, `checklist` (`checklist_item`)                             | Higher-level patterns. `repeat` clones a `template` block per item in a ref'd array; `checklist` tracks completion across `checklist_item` children. |

Every block accepts these universal fields (`BlockMixin`):

<ParamField path="visible" type="Predicate">
  Conditionally render the block. The block is omitted from the DOM when the predicate evaluates false against the current state bag. See [Predicates](#predicates-conditional-rendering) below.
</ParamField>

<ParamField path="flex" type="number">
  Layout weight, honored **only** when the block is a direct child of a `row`. Children share the row's width in proportion to their `flex` (`flex: 2` takes twice the space of `flex: 1`). It is a proportion, never a pixel size, so layouts stay responsive. Ignored outside a row.
</ParamField>

<ParamField path="width" type="number">
  Explicit render width in px (set by composer drag-resize). Unset means natural size. Prefer `flex` for responsive layouts.
</ParamField>

<ParamField path="height" type="number">
  Explicit render height in px. Unset means natural size.
</ParamField>

<ParamField path="alignSelf" type="&#x22;start&#x22; | &#x22;center&#x22; | &#x22;end&#x22;">
  This block's own alignment on its parent container's cross axis (CSS `align-self`). Distinct from a `row`'s `align`, which positions all of the row's children.
</ParamField>

A minimal authored tree a card with a heading, body text, and a button looks like this. This is the exact `block` shape the renderer consumes:

```ts theme={null}
const tree: WidgetTree = {
  schemaVersion: 2,
  widgetId: "welcome",
  header: { title: "Get started", icon: "👋" },
  initialState: {},
  block: {
    type: "card",
    children: [
      { type: "title", text: "Welcome to Acme", level: 1 },
      { type: "text", text: "Connect a data source to see your first chart.", muted: true },
      {
        type: "button",
        label: "Connect a source",
        variant: "primary",
        actions: [{ kind: "open_url", url: "https://acme.app/sources", target: "_blank" }],
      },
    ],
  },
};
```

## Dynamic values: refs

Text and inputs bind to the state bag with a **ref** a `{ ref: "path" }` pointer into `initialState` plus anything inputs have written since. A `text` block accepts a plain string, a single ref, or a `TextSegment[]` that interleaves literal runs with refs for mid-sentence values.

```ts theme={null}
// Reads `name` out of the state bag, mid-sentence:
{ type: "text", text: ["Hi ", { ref: "name" }, ", welcome!"] }

// Writes the typed value back to the bag at `form.email`:
{ type: "text_input", bind: { ref: "form.email" }, placeholder: "Email" }
```

A ref is resolved at render time against the current bag, so a `text` that reads `{ ref: "form.email" }` updates live as the user types into the input bound to the same path. Refs are how a widget shows what the user just selected, echoes their name, or reflects a slider value without any host code.

## Predicates: conditional rendering

A **predicate** is a small boolean expression evaluated against the state bag. It powers `visible` (show/hide a block) and `disabledWhen` (disable an interactive block). Operands are refs or literals; the comparison and logical operators are fixed:

```ts theme={null}
type Predicate =
  | { eq: [Ref | Value, Ref | Value] }
  | { ne: [Ref | Value, Ref | Value] }
  | { gt: [Ref | Value, Ref | Value] }
  | { lt: [Ref | Value, Ref | Value] }
  | { in: [Ref | Value, Value[]] }
  | { and: Predicate[] }
  | { or: Predicate[] }
  | { not: Predicate };
```

For example, reveal a follow-up question only after a rating is given, and keep Submit disabled until an email is entered:

```ts theme={null}
// Show this text only once `rating` exceeds 0:
{ type: "text", text: "Thanks tell us more?", visible: { gt: [{ ref: "rating" }, 0] } }

// Disable Submit until `form.email` is non-empty:
{ type: "button", label: "Submit",
  disabledWhen: { eq: [{ ref: "form.email" }, ""] },
  actions: [{ kind: "submit", name: "feedback", submits: true }] }
```

Because predicates evaluate in the browser against live state, conditional UI is instant no server round-trip is needed to reveal or disable a block.

## Actions: what happens on interaction

Interactive blocks carry **actions**. A `button` declares an ordered `actions` array; `pill_group`, `checkbox`, `slider`, and `rating` carry a single `action`. Actions split into three reaches that determine *where* they resolve.

<AccordionGroup>
  <Accordion title="Local resolve entirely in the browser">
    Mutate widget state or the widget frame without touching the server:

    * `state.set` write a value at a path in the state bag.
    * `state.reset` reset the bag (optionally to a provided object).
    * `widget.dismiss` hide the widget.
    * `widget.minimize` collapse the widget to its resting state; it stays alive and can re-expand.
    * `tree.replace` swap the entire rendered tree for a new one carried in the action.
    * `none` explicit no-op (an input still writes its `bind`, but no action fires). Use it when a separate Submit button owns the action.
  </Accordion>

  <Accordion title="Host delegate to your application">
    Hand control to your app via the widget's handlers (see [Widget](/react/widget)):

    * `submit` complete the widget; `{ name, payload?, submits? }`. With `submits: true`, analytics also emits `ff_widget_submit` (state **keys** only, never values).
    * `emit` fire a named host event with an optional payload.
    * `send_prompt` hand a populated prompt to your chat surface; **your app** decides what to do with it. FirstFlow does not own the user's chat input.
    * `open_url` open a URL (`target: "_self" | "_blank"`).
    * `flow.next` / `flow.back` advance or rewind a multi-step flow. The SDK only emits intent; the **server** walks the flow and pushes the next tree.
  </Accordion>

  <Accordion title="Remote delegate to the backend">
    A single `remote` action `{ kind: "remote"; name; payload? }` sends a named intent back to the backend for server-side handling.
  </Accordion>
</AccordionGroup>

Host and remote actions reach your code through the widget's `onAction` / `onAppEvent` handlers; local actions never surface to you. The split matters operationally: `flow.next` looks like a button click but is a **server-side** navigation the browser emits `widget.action`, the backend's flow navigation listener advances the step and composes the next tree. A click that "does nothing" usually means a host action with no handler wired, not a broken widget.

### Action ordering and terminal actions

A `button` can run several actions on one click. The runtime sorts them so non-terminal actions run first in author order, and the single **terminal** action one that completes or navigates away from the widget runs last. The terminal set is `flow.next`, `flow.back`, `widget.dismiss`, `submit`, and `tree.replace`. Only one terminal action may run per click; pinning it last prevents acting on an already-unmounted widget.

```ts theme={null}
// Records the choice, then advances the flow. flow.next is pinned last.
{ type: "button", label: "Continue", actions: [
  { kind: "state.set", path: "answers.plan", value: "pro" },
  { kind: "flow.next" },
]}
```

`widget.minimize` is intentionally **not** terminal it only collapses the frame, so it can be combined freely with any other action.

## Compound blocks

Two primitives express patterns that would be tedious to hand-build.

`repeat` renders a `template` block once per item in a ref'd array, so a tree can render a list whose length is unknown at author time:

```ts theme={null}
{ type: "repeat", items: { ref: "sources" }, template: {
  type: "row", children: [
    { type: "text", text: { ref: "item.name" }, flex: 1 },
    { type: "badge", label: { ref: "item.status" }, tone: "accent" },
  ],
}}
```

`checklist` tracks completion across `checklist_item` children, keeping an array of checked item ids on its `bind` ref. Items can auto-complete from host-app events (`completionEvent`), and the block can show progress, a continue button, and a completion action useful for an onboarding "getting started" list driven by real product events:

```ts theme={null}
{ type: "checklist", bind: { ref: "onboarding" }, showProgress: true,
  continueLabel: "Finish", continueWhen: "all_complete",
  children: [
    { type: "checklist_item", id: "connect", label: "Connect a source", completionEvent: "source_connected" },
    { type: "checklist_item", id: "invite",  label: "Invite a teammate", completionEvent: "teammate_invited" },
  ],
}
```

## Guides and slots

Guides are the one experience type whose tree an LLM fills. A v2 guide tree declares a `slots` manifest typed placeholders the author binds primitives to via `{ ref: "slots.<id>" }`:

```ts theme={null}
type SlotType = "text" | "number" | "boolean" | "stringList" | "record" | "recordList" | "media";
type Slot = { id: string; type: SlotType; label: string };
```

At runtime the backend's `WidgetCompositionService` calls Anthropic (`claude-sonnet-4-6`) to fill each declared slot from the conversation context, merges the values into `initialState` under `slots.*`, and pushes the finished tree. From the renderer's perspective a filled slot is just an ordinary `slots.*` key in the state bag the same ref mechanism that powers any other dynamic value. This is why guides feel generated while tours and surveys feel fixed: the *manifest* is authored, the *values* are composed per conversation, and both land in the same state bag the renderer reads.

## Limitations and edge cases

The browser renderer is deliberately constrained, which keeps composed widgets safe and predictable:

* **No arbitrary HTML or scripts.** A tree is a fixed union of typed blocks. Anything outside the schema is rejected by `widgetTreeSchema` on the server before it can be pushed, and is not rendered by the browser. There is no escape hatch into raw markup.
* **`flex` only inside a `row`.** Setting `flex` on a block that is not a row child has no effect; use `width`/`height` for explicit sizing.
* **One terminal action per click.** Author multiple actions freely, but exactly one of the terminal kinds runs, and it runs last regardless of author order.
* **State is per-widget and browser-local.** The state bag lives only as long as the rendered widget. To persist an answer, fire a host `submit`/`emit` or a `remote` action `state.*` alone does not reach your backend.
* **`send_prompt` is host-owned.** FirstFlow hands you the prompt; rendering it into a chat is your app's job, because FirstFlow does not own the user's chat surface.
* **Composition is validated, not trusted.** Even an AI-composed guide tree must pass the same `widgetTreeSchema` as an authored one; a malformed tree is dropped rather than rendered.

## Next

Render and handle a tree with the [FirstflowWidget component](/react/widget), build or inspect trees directly with [`@firstflow/widget-kit`](/widget-kit/overview), and see how trees arrive in the browser in [Realtime](/react/realtime). For where trees come from, read [Flows & nodes](/concepts/flows-and-nodes) and [Experiences](/concepts/experiences).
