Skip to main content
OpenUI is a generative UI library that lets a language model produce complete, interactive UIs in a declarative format called openui-lang. Instead of returning a chat message, the agent returns a component tree with cards, charts, tables, tabs, and forms that the Renderer turns into a real React UI. This integration is well-suited for data-rich outputs like reports, dashboards, and data explorers, where the model is both the data analyst and the UI designer.

How it works

  1. Generate the system prompt: call openuiLibrary.prompt() once at startup; it produces a complete openui-lang reference that the model uses to write valid component trees
  2. Inject on first message: send the system prompt as the opening system message when a new conversation starts
  3. Model writes openui-lang: the model responds with a program like root = Stack([header, kpis, chart]) instead of prose
  4. Render with Renderer: pass the text to OpenUI’s Renderer and the component library; it parses and renders the tree

Installation

OpenUI requires React 19+ and zustand. The frontend code is React-only; the LangGraph agent backend can be written in TypeScript or Python.

Import the component styles

Import OpenUI’s bundled styles in your CSS entry point or directly in your root component:

Generate the system prompt

OpenUI ships a openuiLibrary.prompt() function that generates the complete openui-lang reference, with all component signatures, syntax rules, streaming tips, and examples. Call it once at module load time:
The preamble overrides the default persona. Add additionalRules to inject task-specific constraints:

Inject the system prompt via useStream

Send the system prompt as the first message of every new thread. Check stream.messages.length === 0 to detect a fresh thread and prepend a system message:

Render with the Renderer

Pass the AI message’s text content directly to Renderer along with openuiLibrary:
Pass isStreaming={true} during the active stream so the Renderer handles unresolved references gracefully as definitions arrive.

The openui-lang format

The model writes a program rather than a JSON spec. Every statement is an assignment; root is the entry point. The official prompt teaches the model this format, including hoisting — writing root first so the UI shell appears immediately:
With hoisting enabled (recommended), the root line is written first so the page structure appears immediately and each section fills in as the model defines it.

Progressive rendering utilities

Wiring useStream to Renderer directly causes results in re-rendering on every streaming token and produces hundreds of no-op re-parses per response. This causes chart components to crash when their data hasn’t arrived yet. The utilities below solve these problems: Copy the full block into your project and pass stable to <Renderer>:

Follow-up queries

OpenUI’s Button component supports a continue_conversation action type. When the user clicks a follow-up button, Renderer fires onAction and the AIMessageView above submits the button’s label as the next user message, exactly the same code path as typing in the input. Add an “Explore Further” section to every report via additionalRules in the system prompt:

Build a parallel dashboard with Deep Agents

The flow above renders one OpenUI program into one surface. For richer apps, a Deep Agents coordinator can delegate to several specialist agents that each stream their own OpenUI panel concurrently, all over one useStream connection. The OpenUI parallel dashboard example turns one dashboard brief into independently streaming Stripe, PostHog, GitHub, and Calendar panels, with no custom graph or stream-demultiplexing code.

Share one OpenUI library

Use the same library object on the server (to generate the panel prompt) and on the client (as the Renderer prop) so the components the model is told about always match the ones the renderer can draw:
library.ts

Define the coordinator and panel agents

createDeepAgent builds a coordinator whose only job is routing: it picks the specialists a brief needs and emits all of their task() calls in one message so the panels run concurrently. Each panel subagent shares one pre-generated OpenUI system prompt and receives only the tools for its data domain.
agent.ts
The coordinator never writes openui-lang. Each panel agent calls its tools, then returns one complete program that starts with root so its renderer can paint before the model finishes the remaining statements.

Register the graph

Point langgraph.json at the exported coordinator:
langgraph.json

Discover and render panels on the frontend

One useStream connection carries the coordinator and every panel. The panels are not hardcoded: each parallel task() call surfaces as a stream.subagents snapshot. For each snapshot, scope a useMessages(stream, snapshot) projection so a panel receives only its own subagent’s messages, then feed its OpenUI program into an isolated Renderer:
App.tsx
Because the SDK keeps subagent token events out of the root store and each Panel is memoized on its snapshot identity, tokens from one panel never re-render another.

Best practices

  • Generate the system prompt at module load: not inside a React component; the prompt is several kilobytes and should be computed once
  • Inject the system prompt only on fresh threads: check stream.messages.length === 0 and skip injection on subsequent turns to avoid duplicating the prompt in the thread history
  • Use hoisting order: write root = Stack([...]) first; the UI shell appears immediately and sections fill in progressively as the model defines each one
  • Gate on complete statements: avoid re-rendering the Renderer on every token; update only when a full statement (name = ComponentCall(...)) has arrived
  • Verify chart data before rendering: chart components need their Series and label arrays defined before they’re included in the stable snapshot
  • Keep camelCase variable names: the openui-lang parser only accepts camelCase identifiers; reinforce this in the system prompt’s additionalRules
  • Delegate panels in one message: when fanning out to Deep Agents specialists, emit all task() calls in a single coordinator message so the panels stream concurrently rather than one at a time
  • Scope each panel to its subagent: discover panels from stream.subagents and pass each snapshot to useMessages(stream, snapshot) so a panel renders only its own subagent’s output