Streaming
Render AI-generated UI from JSON using Kumo's auto-generated schemas. Enable progressive rendering as LLM responses stream in.
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.
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_formactions - 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 |
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 Textarea maps to InputArea,
RadioGroup maps to Radio.
Div is a synthetic container type rendered as a plain <div>.
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.
defineCustomComponentfreezes the object, andUITreeRenderermemoises 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.jsonorai/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.
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
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:
- Capture — Input/Textarea/Select/Checkbox changes are written to the
runtimeValueStoreas users type - Snapshot — On submit click, all captured values are injected into
event.context.runtimeValues - Dispatch —
dispatchActionmatchessubmit_form, collects scoped field values, returns aMessageResult - Send —
processActionResultcalls yoursendMessagecallback 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.
@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:
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"
}
}
}
} - 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.