Want multi-turn conversations, model selection, and code export? Try the Playground.

Live Demo

Ask the AI to generate UI using Kumo components. The response streams in real-time as JSONL patches, rendering live Kumo components as tokens arrive.

Generative UILive
Pick a preset or type a prompt below to generate UI.
Action Events
Interact with generated UI to see action events here.
Submit Payload
No submit_form action in current tree.

App Runtime Example

This scripted chat turn uses useChatUI from @cloudflare/kumo/app-runtime/react to drive a task CRUD app with bound form state, validation, repeat rendering, and watcher-based derived state.

App state snapshot

{}

Quick Start

Stream AI-generated Kumo UI in three steps: parse JSONL, apply RFC 6902 patches, render.

import {
  useUITree,
  useRuntimeValueStore,
  createJsonlParser,
} from "@cloudflare/kumo/streaming";
import { UITreeRenderer } from "@cloudflare/kumo/generative";

function StreamingUI() {
  const { tree, applyPatches, reset } = useUITree({ batchPatches: true });
  const runtimeValueStore = useRuntimeValueStore();

  async function startStream(prompt: string) {
    reset();
    const parser = createJsonlParser();
    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
      body: JSON.stringify({ message: prompt }),
    });

    const reader = res.body?.getReader();
    if (!reader) return;
    const decoder = new TextDecoder();
    let buffer = "";

    for (;;) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true }).replace(/
/g, "");
      for (;;) {
        const newline = buffer.indexOf("
");
        if (newline === -1) break;
        const line = buffer.slice(0, newline).trim();
        buffer = buffer.slice(newline + 1);

        if (!line.startsWith("data:")) continue;
        const payload = line.slice("data:".length).trimStart();
        if (payload === "[DONE]") return;

        const parsed: unknown = JSON.parse(payload);
        let token = "";
        if (typeof parsed === "object" && parsed !== null) {
          const obj = parsed as Record<string, unknown>;
          if (typeof obj.response === "string") token = obj.response;
        }

        const ops = parser.push(token);
        if (ops.length > 0) applyPatches(ops);
      }
    }

    // Flush remaining buffer
    const remaining = parser.flush();
    if (remaining.length > 0) applyPatches(remaining);
  }

  return (
    <UITreeRenderer
      tree={tree}
      streaming={true}
      runtimeValueStore={runtimeValueStore}
    />
  );
}

What's happening

  • useUITree — manages UITree state, applies patches via functional setState
  • useRuntimeValueStore — captures form field values (Input, Select, etc.) for submit_form actions
  • createJsonlParser — parses newline-delimited JSON into RFC 6902 patch ops
  • UITreeRenderer — maps UITree elements to real Kumo React components
  • batchPatches — coalesces multiple patches into a single React render

UMD / HTML Usage

For non-React environments, the loadable UMD bundle exposes window.CloudflareKumo with a zero-framework API. React is bundled inside — the host page needs only a <script> tag and a container <div>.

<link rel="stylesheet" href="@cloudflare/kumo/loadable/style.css" />
<script src="@cloudflare/kumo/loadable/kumo-loadable.umd.js"></script>

<div id="my-ui"></div>

<script>
  const { CloudflareKumo } = window;

  // Optional: set theme
  CloudflareKumo.setTheme("dark");

  // Apply patches from your streaming source
  const parser = CloudflareKumo.createParser();

  eventSource.onmessage = (e) => {
    const ops = parser.push(e.data);
    if (ops.length > 0) {
      CloudflareKumo.applyPatchesBatched(ops, "my-ui");
    }
  };

  // Or render a complete tree at once
  CloudflareKumo.renderTree(
    { root: "card", elements: { card: { key: "card", type: "Surface", props: {}, children: [] } } },
    "my-ui"
  );
</script>

API Reference

Method Description
applyPatch(op, id) Apply one RFC 6902 patch, re-render
applyPatches(ops, id) Batch patches, single render
applyPatchesBatched(ops, id) Batch patches, coalesced rAF render
renderTree(tree, id) Replace entire UITree at once
createParser() Create JSONL streaming parser
setTheme(mode) Switch light/dark theme
getTree(id) Read current UITree state
subscribeTree(id, cb) Subscribe to UITree changes
dispatchAction(event, id) Dispatch action through built-in handlers
processActionResult(result, callbacks) Route action result to applyPatches / sendMessage / openExternal
getRuntimeValues(id) Read captured form input values
subscribeRuntimeValues(id, cb) Subscribe to form value changes
onAction(handler) Subscribe to all action events (JS callback alternative to CustomEvent)
reset(id) Clear state and unmount
The loadable bundle uses shadow DOM isolation by default for style encapsulation. Pass a container ID to scope each independent UI instance.

Component Map

The generative module maps UITree type strings to Kumo React components. This map is auto-generated from the component registry to stay in sync.

Direct Components

1:1 mapping to Kumo exports

BadgeBannerBreadcrumbsButtonClipboardTextClusterCodeEmptyFieldGridLabelLinkLoaderMeterRadioStackTableText

Stateful Wrappers

Adds internal state for controlled-only components

CheckboxCollapsibleSelectSwitchTabs

Generative Wrappers

Sensible defaults for AI context

SurfaceInputInputAreaCloudflareLogoSelect

Sub-Component Aliases

Flattened names for LLM output

TableHeaderTableHeadTableBodyTableRowTableCellTableFooterBreadcrumbsLinkBreadcrumbsCurrentSelectOptionGridItem
Type aliases: Textarea maps to InputArea, RadioGroup maps to Radio. Div is a synthetic container type rendered as a plain <div>.
The component map and drift detection tests are auto-generated via pnpm codegen:registry. Run it after modifying component props or adding new components.

Custom Components

Extend UITreeRenderer with your own React components so LLM-generated UI can include domain-specific elements (charts, maps, custom widgets) alongside built-in Kumo components. The extension model has three independent layers:

Render

Pass custom components to UITreeRenderer so it knows how to render new type strings.

Validation

Attach a Zod propsSchema to validate props at runtime through the same pipeline as built-in components.

Prompt

Provide props metadata so generatePrompt() includes your components in the LLM system prompt.

Quick Start (Render Only)

The minimal path: define a component and pass it to UITreeRenderer. No validation or prompt generation — just rendering.

import { defineCustomComponent } from "@cloudflare/kumo/generative";
import { UITreeRenderer } from "@cloudflare/kumo/generative";
import type { CustomComponentDefinition } from "@cloudflare/kumo/catalog";

// Your domain component
function BarChart({ data, color }: { data: number[]; color?: string }) {
  return <svg>{/* chart rendering */}</svg>;
}

// Define outside the render path for stable identity
const barChart = defineCustomComponent({
  component: BarChart,
  description: "Renders a bar chart from numeric data",
});

const customComponents: Readonly<Record<string, CustomComponentDefinition>> = {
  BarChart: barChart,
};

// In your component
function App() {
  const { tree } = useUITree({ batchPatches: true });

  return (
    <UITreeRenderer
      tree={tree}
      streaming={true}
      customComponents={customComponents}
    />
  );
}

Full Example (Validation + Prompt)

For production use, add a Zod schema for runtime prop validation and prop metadata for LLM prompt generation:

import { z } from "zod";
import { defineCustomComponent } from "@cloudflare/kumo/generative";
import { UITreeRenderer } from "@cloudflare/kumo/generative";
import { createKumoCatalog, initCatalog } from "@cloudflare/kumo/catalog";
import type { CustomComponentDefinition } from "@cloudflare/kumo/catalog";

// Zod schema for runtime validation
const barChartSchema = z.object({
  data: z.array(z.number()),
  color: z.string().optional(),
  title: z.string().optional(),
});

const barChart = defineCustomComponent({
  component: BarChart,
  description: "Renders a bar chart from numeric data",
  // Runtime validation — same pipeline as built-in components
  propsSchema: barChartSchema,
  // Prompt metadata — tells the LLM what props are available
  props: {
    data: { type: "number[]", description: "Data points to plot" },
    color: { type: "string", description: "Bar fill color", optional: true },
    title: { type: "string", description: "Chart title", optional: true },
  },
  category: "Visualization",
});

const customComponents = { BarChart: barChart } as const;

// Create catalog with custom components for prompt generation
const catalog = createKumoCatalog({ customComponents });
await initCatalog(catalog);

// generatePrompt() now includes BarChart docs under "### Custom"
const prompt = catalog.generatePrompt();

Caveats

  • Define outside render — component definitions must be created outside React render functions. defineCustomComponent freezes the object, and UITreeRenderer memoises the merged map by identity.
  • Consumer manages prompts — custom component docs are included in generatePrompt() output, but you are responsible for passing that prompt to your LLM.
  • No auto-codegen — custom components are not added to component-registry.json or ai/schemas.ts. Those files only cover built-in Kumo components.

Security

  • Props are sanitized — custom component props pass through the same sanitizeProps() pipeline as built-in components (event handlers and dangerous attributes are stripped).
  • Component internals are your responsibility — Kumo sanitizes the props boundary, but what your component does with those props (e.g. rendering user-provided HTML) is outside the library's security scope.
When a custom component name collides with a built-in (e.g. naming yours Button), the custom component wins and a console warning is emitted in development. This is intentional — it lets you override built-ins when needed.

System Prompt Generation

The catalog's generatePrompt() builds a complete system prompt for LLMs, including component documentation, JSONL/RFC 6902 format instructions, design rules, and working examples. It's derived from the component registry — always in sync.

import { createKumoCatalog, initCatalog } from "@cloudflare/kumo/catalog";

const catalog = createKumoCatalog();
await initCatalog(catalog);

// Full prompt with all components, examples, and format instructions
const prompt = catalog.generatePrompt();

// Subset of components only
const focused = catalog.generatePrompt({
  components: ["Button", "Input", "Surface", "Text"],
});

// Skip examples to reduce token count
const compact = catalog.generatePrompt({
  includeExamples: false,
  maxPropsPerComponent: 5,
});

What the prompt includes

  • Component docs — props grouped by category (Layout, Content, Interactive, etc.)
  • Prop scoring — top 10 props per component, ranked by relevance (required, enum, common names)
  • JSONL format — how the LLM should emit newline-delimited JSON patch ops
  • RFC 6902 schema — the UITree structure, element format, and patch semantics
  • Design rules — accessibility, semantic grouping, no emoji in content
  • Working examples — counter UI and form examples the LLM can learn from
  • Action system — built-in actions the LLM can attach to elements
The full prompt is under 15K tokens. Use the components option to narrow it for domain-specific use cases.

Action System

The streaming action system handles user interactions with AI-generated UI. Built-in actions cover common patterns; the host can extend with custom handlers.

Action Trigger Effect
increment Button click Increments a numeric value at a JSON Pointer path
decrement Button click Decrements a numeric value at a JSON Pointer path
submit_form Button click Collects runtime form values and sends as a message
navigate Link / button click Opens URL (http/https/relative only — javascript: blocked)

React

Wire useRuntimeValueStore to capture form field values, then connect sendMessage to your streaming function so submit_form feeds collected values back into the conversation.

import { useRef, useCallback } from "react";
import {
  useUITree,
  useRuntimeValueStore,
  BUILTIN_HANDLERS,
  dispatchAction,
  processActionResult,
  type ActionEvent,
  type JsonPatchOp,
  type UITree,
} from "@cloudflare/kumo/streaming";
import { UITreeRenderer } from "@cloudflare/kumo/generative";

function InteractiveUI() {
  const { tree, applyPatches, reset } = useUITree({ batchPatches: true });
  const runtimeValueStore = useRuntimeValueStore();

  // Refs prevent stale closures — the handleAction callback
  // captures the latest tree/applyPatches without re-subscribing.
  const treeRef = useRef<UITree>(tree);
  treeRef.current = tree;
  const applyPatchesRef = useRef(applyPatches);
  applyPatchesRef.current = applyPatches;

  const startStream = useCallback(async (prompt: string) => {
    reset();
    // ... your streaming logic (see Quick Start)
  }, [reset]);
  const startStreamRef = useRef(startStream);
  startStreamRef.current = startStream;

  function handleAction(event: ActionEvent) {
    const result = dispatchAction(
      BUILTIN_HANDLERS, event, treeRef.current
    );
    if (!result) return;

    processActionResult(result, {
      applyPatches: (patches: readonly JsonPatchOp[]) =>
        applyPatchesRef.current(patches),
      // submit_form calls sendMessage with collected form values —
      // wire it to start a new stream turn.
      sendMessage: (msg: string) => startStreamRef.current(msg),
      openExternal: (url: string, target?: string) =>
        window.open(url, target ?? "_blank"),
    });
  }

  return (
    <UITreeRenderer
      tree={tree}
      streaming={false}
      onAction={handleAction}
      runtimeValueStore={runtimeValueStore}
    />
  );
}

UMD / HTML

In UMD mode, actions fire as CustomEvents on window. Listen for kumo-action, dispatch through the built-in registry, and process the result.

<script>
  const CONTAINER = "my-ui";

  // Listen for action events from the rendered UI
  window.addEventListener("kumo-action", function (e) {
    var detail = e.detail;

    // Dispatch through built-in handlers (increment, decrement, submit_form, navigate)
    var result = CloudflareKumo.dispatchAction(detail, CONTAINER);
    if (result == null) return;

    CloudflareKumo.processActionResult(result, {
      applyPatches: function (patches) {
        CloudflareKumo.applyPatches(patches, CONTAINER);
      },
      // submit_form calls sendMessage with collected form values —
      // wire it to start a new stream turn.
      sendMessage: function (content) {
        streamChat(content);
      },
    });
  });

  // React to runtime value changes (e.g. update a submit preview)
  CloudflareKumo.subscribeRuntimeValues(CONTAINER, function () {
    var values = CloudflareKumo.getRuntimeValues(CONTAINER);
    console.log("Current form values:", values);
  });
</script>

Form Submission Flow

When the LLM attaches a submit_form action to a Button, clicking it triggers a data collection pipeline:

  1. Capture — Input/Textarea/Select/Checkbox changes are written to the runtimeValueStore as users type
  2. Snapshot — On submit click, all captured values are injected into event.context.runtimeValues
  3. DispatchdispatchAction matches submit_form, collects scoped field values, returns a MessageResult
  4. SendprocessActionResult calls your sendMessage callback with the serialized form payload

The payload sent to sendMessage is a JSON string containing actionName, sourceKey, and fields (the collected input values). Your server can parse this to process the form submission or feed it back into the LLM conversation.

URL Security

The navigate action enforces a URL allowlist: only http://, https://, and relative paths are permitted. javascript:, data:, file:, and protocol-relative URLs are blocked and logged as warnings.

Catalog Overview

The Kumo catalog module enables rendering UI from JSON structures, designed specifically for AI-generated interfaces. It provides runtime validation, data binding, conditional rendering, and action handling for JSON-based UI trees.

Schemas Derived from Your Codebase

Unlike approaches that require maintaining separate schema definitions, Kumo automatically derives validation schemas from your actual component TypeScript types. When you update a component's props, the validation schemas update automatically via the component registry codegen process. No manual synchronization required - your schemas are always in sync with your components.

Schemas are auto-generated in @cloudflare/kumo/ai/schemas from component TypeScript types. Run pnpm codegen:registry after modifying component props to regenerate.

How It Works

The catalog module uses a pipeline that extracts component metadata from your TypeScript source code:

Component TSX
TypeScript Types
Codegen Script
Zod Schemas

The generated schemas in ai/schemas.ts include:

  • Props schemas for each component (e.g., ButtonPropsSchema)
  • Enum values for variant props
  • UI element and tree structure schemas
  • Dynamic value, visibility, and action schemas

Installation

import {
  createKumoCatalog,
  initCatalog,
  resolveProps,
  evaluateVisibility,
} from "@cloudflare/kumo/catalog";

Creating a Catalog

Create a catalog instance that validates AI-generated JSON against the auto-generated schemas:

import { createKumoCatalog, initCatalog } from "@cloudflare/kumo/catalog";

// Create a catalog with optional actions
const catalog = createKumoCatalog({
  actions: {
    submit_form: { description: "Submit the current form" },
    delete_item: { description: "Delete the selected item" },
  },
});

// Initialize schemas (required before sync validation)
await initCatalog(catalog);

// Validate AI-generated JSON
const result = catalog.validateTree(aiGeneratedJson);
if (result.success) {
  // Render the validated tree
  renderTree(result.data);
}

UI Tree Format

The UI tree uses a flat structure optimized for LLM generation and streaming. Elements reference each other by key rather than nesting, enabling progressive rendering as elements stream in.

{
  "root": "card-1",
  "elements": {
    "card-1": {
      "key": "card-1",
      "type": "Surface",
      "props": { "className": "p-4" },
      "children": ["heading-1", "text-1", "button-1"]
    },
    "heading-1": {
      "key": "heading-1",
      "type": "Text",
      "props": {
        "variant": "heading2",
        "children": "Welcome"
      },
      "parentKey": "card-1"
    },
    "text-1": {
      "key": "text-1",
      "type": "Text",
      "props": {
        "children": { "path": "/user/name" }
      },
      "parentKey": "card-1"
    },
    "button-1": {
      "key": "button-1",
      "type": "Button",
      "props": {
        "variant": "primary",
        "children": "Get Started"
      },
      "parentKey": "card-1",
      "action": {
        "name": "submit_form"
      }
    }
  }
}
Why a flat structure?
  • Elements can be rendered as soon as they arrive (streaming)
  • Easy updates without deep tree traversal
  • Simple serialization/deserialization
  • Natural fit for how LLMs generate token-by-token

Dynamic Values (Data Binding)

Props can reference values from a data model using JSON Pointer paths. This allows the AI to declare data bindings that your application resolves at render time.

import { resolveProps, resolveDynamicValue } from "@cloudflare/kumo/catalog";

// Data model backing the UI
const dataModel = {
  user: {
    name: "Alice",
    isAdmin: true,
  },
  items: [
    { id: 1, title: "First Item" },
    { id: 2, title: "Second Item" },
  ],
};

// AI-generated props with dynamic references
const props = {
  children: { path: "/user/name" },
  disabled: false,
};

// Resolve all dynamic values
const resolved = resolveProps(props, dataModel);
// { children: "Alice", disabled: false }

// Or resolve individual values
const name = resolveDynamicValue({ path: "/user/name" }, dataModel);
// "Alice"

Visibility Conditions

Elements can be conditionally rendered based on data values, authentication state, or complex logic expressions.

import {
  evaluateVisibility,
  createVisibilityContext
} from "@cloudflare/kumo/catalog";

const ctx = createVisibilityContext(
  // Data model
  { user: { isAdmin: true, role: "editor" } },
  // Auth state
  { isSignedIn: true }
);

// Simple boolean
evaluateVisibility(true, ctx); // true

// Path check (truthy test)
evaluateVisibility({ path: "/user/isAdmin" }, ctx); // true

// Auth check
evaluateVisibility({ auth: "signedIn" }, ctx); // true
evaluateVisibility({ auth: "signedOut" }, ctx); // false

// Equality check
evaluateVisibility({
  eq: [{ path: "/user/role" }, "editor"]
}, ctx); // true

// Complex logic
evaluateVisibility({
  and: [
    { path: "/user/isAdmin" },
    { auth: "signedIn" },
    { gt: [{ path: "/items/length" }, 0] }
  ]
}, ctx);

Available Operators

Operator Description
path Truthy check on data path
auth "signedIn" or "signedOut"
eq / neq Equality / inequality comparison
gt / gte Greater than / greater than or equal
lt / lte Less than / less than or equal
and / or / not Boolean logic combinators

Catalog Actions

Elements can declare actions that your application handles. The AI describes the intent, and your handlers execute the logic.

// In your UI tree element
{
  "key": "delete-btn",
  "type": "Button",
  "props": {
    "variant": "destructive",
    "children": "Delete"
  },
  "action": {
    "name": "delete_item",
    "params": {
      "itemId": { "path": "/selected/id" }
    },
    "confirm": {
      "title": "Delete Item",
      "message": "Are you sure you want to delete this item?",
      "variant": "danger",
      "confirmLabel": "Delete",
      "cancelLabel": "Cancel"
    },
    "onSuccess": {
      "set": { "/selected": null }
    }
  }
}

// Register actions when creating the catalog
const catalog = createKumoCatalog({
  actions: {
    delete_item: {
      description: "Delete an item by ID",
      params: {
        itemId: { type: "string", description: "Item ID to delete" }
      }
    }
  }
});

Validation

The catalog validates AI-generated JSON against auto-generated Zod schemas derived from component TypeScript types.

// Validate a complete tree
const result = catalog.validateTree(aiJson);

if (result.success) {
  console.log("Valid tree:", result.data);
} else {
  console.error("Validation errors:", result.error);
  // [{ message: "Invalid enum value", path: ["elements", "btn-1", "props", "variant"] }]
}

// Validate a single element
const elementResult = catalog.validateElement({
  key: "btn-1",
  type: "Button",
  props: { variant: "primary" }
});

// Check available components
catalog.hasComponent("Button"); // true
catalog.hasComponent("Foobar"); // false

// List all component names
console.log(catalog.componentNames);
// ["Badge", "Banner", "Button", ...]

Type Exports

All types are exported for TypeScript integration:

import type {
  // Core types
  UIElement,
  UITree,
  DynamicValue,
  DynamicString,
  DynamicNumber,
  DynamicBoolean,

  // Visibility
  VisibilityCondition,
  LogicExpression,

  // Actions
  Action,
  ActionConfirm,
  ActionHandler,
  ActionHandlers,
  ActionDefinition,

  // Auth & Data
  AuthState,
  DataModel,

  // Catalog
  KumoCatalog,
  CatalogConfig,
  ValidationResult,
} from "@cloudflare/kumo/catalog";

Key Benefits

Auto-Generated Schemas

Validation schemas are derived directly from component TypeScript types. No separate schema definitions to maintain.

Always in Sync

When you update component props, the schemas update automatically via the component registry codegen process.

Streaming-Friendly

Flat tree structure enables progressive rendering as LLM responses stream in token-by-token.

Type Safety

Full TypeScript support with exported types for UIElement, UITree, DynamicValue, and more.