OpenUI interop
Use GenUI components inside an OpenUI library, so a model writing OpenUI Lang can place them in its response.
OpenUI is an open-source generative UI framework by Thesys, released under the MIT license. Instead of calling tools, the model writes OpenUI Lang, a compact line-based language, and OpenUI's <Renderer> turns it into components from a library you define with defineComponent and createLibrary. Thesys reports that OpenUI Lang uses fewer tokens than JSON-based formats ("up to 67% fewer"). Those figures come from Thesys's own benchmarks and haven't been independently verified.
genui-openui.ts turns GenUI components into OpenUI components, so you can use them in an OpenUI app without rewriting them.
Tool calls or OpenUI?
Use GenUI with tool calls (the default) when your agent runs on the Vercel AI SDK, LangGraph or any framework with tool calling. Each component is one tool call, the user's decision goes back as that call's tool result, and the Renderer handles the lifecycle states (failed, stale, expired, superseded, empty, partial).
Use GenUI inside OpenUI when your app already renders OpenUI Lang and you want GenUI components in the same responses as your OpenUI components. The model composes the whole response in one stream; the user's decisions come back as OpenUI actions.
Installation
Install OpenUI's React runtime (GenUI already depends on Zod 4, which OpenUI requires):
pnpm add @openuidev/react-langThen add the Renderer files, which include genui-openui.ts:
npx shadcn@latest add https://your-site.com/r/renderer.json
genui-openui.ts doesn't import any @openuidev package. You pass OpenUI's functions in, so it works with the version you installed.
Define the library
Pass the tool and guidance from each component's schema file, the components themselves, and the OpenUI module:
import * as openui from "@openuidev/react-lang"
import {
openUIRules,
toOpenUILibrary,
} from "@/components/genui/genui-openui"
import { ChangePreview } from "@/components/genui/change-preview"
import {
changePreviewGuidance,
changePreviewTool,
} from "@/components/genui/change-preview.schema"
import { Checklist } from "@/components/genui/checklist"
import {
checklistGuidance,
checklistTool,
} from "@/components/genui/checklist.schema"
const entries = [
{ tool: changePreviewTool, guidance: changePreviewGuidance },
{ tool: checklistTool, guidance: checklistGuidance },
]
export const library = toOpenUILibrary({
entries,
components: {
propose_changes: ChangePreview,
show_checklist: Checklist,
},
openui,
})
// Per-component rules, for library.prompt({ additionalRules }).
export const promptOptions = { additionalRules: openUIRules(entries) }What this does:
- Names. Each component is named after its tool in PascalCase (
propose_changesbecomesProposeChanges), because OpenUI Lang reads PascalCase names as components. Passnamesto choose your own. - Descriptions. OpenUI shows each component's
descriptionnext to its signature in the system prompt. The description is the tool's first line plus its Use when and Avoid when guidance. The rules go intoadditionalRulesthroughopenUIRules, prefixed with the component name. - Argument order. The model writes OpenUI arguments by position, in the schema's key order, and OpenUI recommends putting required props first. The library reorders each schema that way (
requiredFirst: falseturns this off). Schemas with object-level refinements keep their order. The component still receives named props. - Root. Every OpenUI response starts with
root = Root(...). Unless you passroot, the library adds aGenUIStackroot that stacks its children in order, with GenUI's spacing. To use your own root, pass it inextraComponentsand name it inroot.
Build the prompt
On the client, or anywhere the library can be imported:
const system = library.prompt(promptOptions)The library imports React components, so for a backend route OpenUI recommends generating a spec with its CLI (which picks up the exported promptOptions) and building the prompt with generateSystemPrompt from @openuidev/lang-core. See OpenUI's system prompts guide.
Render and send results back
OpenUI's <Renderer> passes component actions to onAction. When the user acts in a GenUI component, its onResult fires an action of type genui_result (GENUI_RESULT_ACTION). Its params hold the tool name, the OpenUI component name, the statement id and the result. Forward it to the model, and store it so the component restores its final state on reload:
"use client"
import { Renderer } from "@openuidev/react-lang"
import {
GenUIOpenUIResults,
isGenUIResultEvent,
} from "@/components/genui/genui-openui"
import { library } from "@/lib/openui-library"
export function AssistantMessage({ message, isStreaming, results, onSend, onSaveResult }) {
return (
// One provider per message: statement ids repeat across messages.
<GenUIOpenUIResults.Provider value={results}>
<Renderer
library={library}
response={message.content}
isStreaming={isStreaming}
onAction={(event) => {
if (isGenUIResultEvent(event)) {
const { statementId, result } = event.params
if (statementId) onSaveResult(message.id, statementId, result)
onSend(`${event.humanFriendlyMessage}\n${JSON.stringify(result)}`)
} else if (event.type === "continue_conversation") {
onSend(event.humanFriendlyMessage)
}
}}
/>
</GenUIOpenUIResults.Provider>
)
}results maps statement ids to stored results for that message. Pass describeResult to toOpenUILibrary to change the message shown for a result (by default ProposeChanges: confirm).
How the wrapper maps OpenUI to GenUI
| GenUI component gets | From OpenUI |
|---|---|
| Its props | The renderer's props. Top-level nulls are dropped unless the field accepts null (the model writes null to skip an optional argument) |
status: "streaming" | useIsStreaming(), while the response streams. Otherwise undefined, so the component tracks the user's decision itself |
onResult(result) | Calls triggerAction(message, undefined, { type: "genui_result", params }) from useTriggerAction(), which reaches onAction |
result | GenUIOpenUIResults, looked up by the statement id |
Limitations
- Results come back as messages, not tool results. OpenUI has no tool call to answer, so your
onActionhandler decides how the result reaches the model. Rules like "apply nothing untilconfirm" depend on the model reading that message. - Lifecycle states don't apply. The GenUI Renderer isn't in the path, so failed, stale, expired, superseded, empty and partial states aren't shown, and expired offers stay actionable. Handle these around the message yourself (for example, make an expired message
inert), or use tool calls. - Streaming is per response.
useIsStreaming()reports the whole response, so every GenUI component in it stays in its streaming state, with actions off, until the response finishes. OpenUI also leaves a component out of the tree until its required props have arrived. - No GenUI layouts. Layout components (tabs, columns) are rejected; compose with the
GenUIStackroot or your own OpenUI layout components. - Validation differs. OpenUI checks prop types and required props while parsing. Zod refinements in a GenUI schema aren't run, and only top-level
nulls are cleaned up. - Restoring needs storage. Finished components restore only when you store results by message and statement id and provide them through
GenUIOpenUIResults.
API
| Export | What it does |
|---|---|
toOpenUILibrary(options) | createLibrary with the wrapped components, plus the GenUIStack root unless you pass root |
toOpenUIComponents(options) | Just the wrapped components, for your own createLibrary call |
openUIRules(entries, names?) | Rules for library.prompt({ additionalRules }) |
openUIDescription(entry) | The description given to defineComponent |
isGenUIResultEvent(event) | True for an ActionEvent that carries a GenUI result |
GENUI_RESULT_ACTION | "genui_result", the action type |
GenUIOpenUIResults | React context of stored results, keyed by statement id |
Options: entries, components, openui (the @openuidev/react-lang module: defineComponent, useIsStreaming, useTriggerAction, plus createLibrary for toOpenUILibrary), and optionally names, requiredFirst, describeResult, root, extraComponents and componentGroups.
OpenUI API references: defining components, the Renderer, interactivity, react-lang API.