{recipe.title}
-
{recipe.ingredients.map((ingredient) => (
- {ingredient} ))}
Hello, {{ props?.name }}!
`, }) export class GreetingComponent { @Input() props: { name?: string } | null = null; @Input() renderNode: ((value: unknown) => unknown) | null = null; @Input() statementId: string | undefined; } const Greeting = defineComponent({ name: "Greeting", description: "Greet the user by name.", props: z.object({ name: z.string() }), component: GreetingComponent, }); export const library = createLibrary({ components: [Greeting], root: "Greeting", }); const systemPrompt = library.prompt({ preamble: "You are a helpful assistant.", examples: ['root = Greeting("Alice")'], }); const schema = library.toJSONSchema(); ``` `createLibrary(definition)` accepts `components` and an optional `root`, `id`, or `componentGroups`. It returns the component registry, `prompt(options?)`, and `toJSONSchema()`. Use `PromptOptions` for prompt options such as `preamble`, `additionalRules`, and `examples`. The order of schema properties determines the positional argument order in OpenUI Lang. Generate the model prompt from the same definitions the renderer uses. For a Node backend, share the schema definitions and use `@openuidev/lang-core` to generate prompts without importing Angular component classes. The example demonstrates this split. ## `Renderer` [#renderer] `Renderer` is an alias for `OpenUiRendererComponent`. Import it in the host component's `imports` and use the `
## Installation [#installation]
### CDN (via `react-lang`) [#cdn-via-react-lang]
If your app uses [`@openuidev/react-lang`](/docs/api-reference/react-lang), the widget shows up automatically in development.
### Package Installation [#package-installation]
Install the package and render `Hello, {props.name}!
``` ```ts title="library.ts" import { createLibrary, defineComponent } from "@openuidev/svelte-lang"; import { z } from "zod/v4"; import GreetingComponent from "./Greeting.svelte"; const Greeting = defineComponent({ name: "Greeting", description: "Greet the user by name.", props: z.object({ name: z.string() }), component: GreetingComponent, }); export const library = createLibrary({ components: [Greeting], root: "Greeting", }); const systemPrompt = library.prompt({ preamble: "You are a helpful assistant.", examples: ['root = Greeting("Alice")'], }); const schema = library.toJSONSchema(); ``` `createLibrary(definition)` accepts `components` and an optional `root`, `id`, or `componentGroups`. It returns the component registry, `prompt(options?)`, and `toJSONSchema()`. Use `PromptOptions` for prompt options such as `preamble`, `additionalRules`, and `examples`. The order of schema properties determines the positional argument order in OpenUI Lang. Generate the model prompt from the same definitions the renderer uses. For a SvelteKit server route, share the schema definitions and use `@openuidev/lang-core` to generate prompts without importing `.svelte` files. ## `Renderer` [#renderer] Pass the accumulated response text as it streams. Callbacks are ordinary props. ```svelte title="AssistantMessage.svelte"Hello, {{ props.name }}!
``` ```ts title="library.ts" import { createLibrary, defineComponent } from "@openuidev/vue-lang"; import { z } from "zod/v4"; import GreetingComponent from "./Greeting.vue"; const Greeting = defineComponent({ name: "Greeting", description: "Greet the user by name.", props: z.object({ name: z.string() }), component: GreetingComponent, }); export const library = createLibrary({ components: [Greeting], root: "Greeting", }); const systemPrompt = library.prompt({ preamble: "You are a helpful assistant.", examples: ['root = Greeting("Alice")'], }); const schema = library.toJSONSchema(); ``` `createLibrary(definition)` accepts `components` and an optional `root`, `id`, or `componentGroups`. It returns the component registry, `prompt(options?)`, and `toJSONSchema()`. Use `PromptOptions` for prompt options such as `preamble`, `additionalRules`, and `examples`. The order of schema properties determines the positional argument order in OpenUI Lang. Generate the model prompt from the same definitions the renderer uses. For a server route, share the schema definitions and use `@openuidev/lang-core` to generate prompts without importing `.vue` files. ## `Renderer` [#renderer] Pass the accumulated response text as it streams. Callbacks are ordinary props, not emitted events, so bind them with `:on-action` rather than `@action`. ```vue title="AssistantMessage.vue"OpenUI Gateway provides a unified API to access models across different providers through a single endpoint. It adds reliability to OpenUI Lang generation with automatic failover and model-output correction.
### Generate [#generate]
The user describes what they want. Your backend sends the request to an LLM along with a system prompt that includes your component library and tool descriptions. The LLM responds with openui-lang code, a compact declarative format that describes the UI layout, data sources, and interactions.
### Execute [#execute]
The Renderer parses the generated code. When it encounters a `Query("list_tickets")`, the runtime calls your tool directly, no LLM roundtrip. When the user clicks a button that triggers `@Run(createResult)`, the runtime executes the mutation against your tool. When a `$variable` changes from a dropdown, all dependent queries re-fetch automatically.
The LLM generated the wiring. The runtime executes it.
GPT-5.2, temperature 0. Same system prompt and user prompt for every scenario. Each format is derived from the same LLM output, not independently generated.
The LLM generates OpenUI Lang. Thesys C1 JSON is a normalized AST projection (`component` +
`props`) that drops parser metadata (`type`, `typeName`, `partial`, `__typename`). The YAML
payload and Vercel JSON-Render output are two serializations of the same json-render spec
projection (`root`, `elements`, optional `state`): JSONL emits RFC 6902 patches, while YAML is
serialized with yaml.stringify(..., \{ indent: 2 }).
All formats measured with tiktoken using the gpt-5 model encoder —
the same tokenizer family as GPT-5.2. Whitespace and formatting is included as-is in the
count. For YAML, the benchmark counts the document payload only and excludes the outer
yaml-spec fence.
Assumes constant throughput (60 tok/s). Real latency also depends on TTFT and network. Streaming advantage is most visible for the last element to render, not just overall time.
1. **System prompt includes OpenUI Lang spec**: Your backend appends the generated component library prompt alongside your system prompt, instructing the LLM to respond in OpenUI Lang instead of plain text or JSON.
2. **LLM generates OpenUI Lang**: Instead of returning markdown, the model outputs a compact, line-oriented syntax (e.g., `root = Stack([chart])`) constrained to your component library.
3. **Streaming render**: On the client, the `
OpenUI Lang connects to your backend through **tools**. A tool is any function your server
exposes (a database query, an API call, a calculation). The LLM generates `Query` and `Mutation`
statements that call these tools. The runtime executes them directly and feeds the results into your UI.
## Reading data with Query [#reading-data-with-query]
```text
data = Query("list_tickets", {}, {rows: []})
```
What each argument means:
1. `"list_tickets"` - the tool name (matches what your server exposes)
2. `{}` - arguments to pass to the tool
3. `{rows: []}` - default value (what renders before the tool responds)
The default value is important - it lets the UI render immediately while data loads.
## Using Query results [#using-query-results]
Query results are just data. Access fields with dot notation:
```text
tbl = Table([Col("Title", data.rows.title), Col("Status", data.rows.status)])
chart = LineChart(data.rows.day, [Series("Views", data.rows.views)])
```
`data.rows.title` extracts the `title` field from every row - like a column pluck.
### Reactive queries [#reactive-queries]
Pass a `$variable` in the query args - the query re-runs when the variable changes:
```text
$days = "7"
data = Query("analytics", {days: $days}, {rows: []})
filter = Select("days", $days, [SelectItem("7", "7 days"), SelectItem("30", "30 days")])
```
User picks "30" → `$days` updates → Query re-fetches with `{days: "30"}` → chart updates.
### Auto-refresh [#auto-refresh]
Add a fourth argument for periodic refresh (in seconds):
```text
health = Query("get_server_health", {}, {cpu: 0, memory: 0}, 30)
```
This re-fetches every 30 seconds - great for monitoring dashboards.
## Writing data with Mutation [#writing-data-with-mutation]
```text
createResult = Mutation("create_ticket", {title: $title, priority: $priority})
```
Mutations are NOT executed on page load. They only run when triggered by `@Run`:
```text
submitBtn = Button("Create", Action([@Run(createResult), @Run(tickets), @Reset($title)]))
```
This button does three things in order:
1. `@Run(createResult)` - executes the mutation (creates the ticket)
2. `@Run(tickets)` - re-fetches the tickets query (refreshes the table)
3. `@Reset($title)` - clears the form
### Error handling [#error-handling]
Check `result.status` for mutation feedback:
```text
createResult.status == "error" ? Callout("error", "Failed", createResult.error) : null
createResult.status == "success" ? Callout("success", "Created", "Ticket added.") : null
```
## How tools connect to the runtime [#how-tools-connect-to-the-runtime]
The Renderer accepts a `toolProvider` prop that handles tool execution. Pass a function map or an [MCP](https://modelcontextprotocol.io/docs/getting-started/intro) client:
```tsx
// Function map
When a `$variable` changes:
1. All inputs bound to it update their display
2. All `Query` calls that reference it in their args re-fetch
3. All expressions that reference it re-evaluate
4. The UI re-renders with new values
No event listeners. No useEffect. No wiring. Just declare and reference.
# Reliability
> Build reliable LLM-generated interfaces by measuring, reducing, and correcting generation errors.
Source: https://www.openui.com/docs/openui-lang/reliability
Generative UI turns LLM output directly into interfaces that users see and interact with. The model receives a description of the available components and their properties, then generates an interface that follows that contract. Most generations follow it correctly, but an LLM can occasionally invent a component, use an unsupported value, leave a reference unresolved, or stop before completing the interface.
Learn more in [developer tools docs](/docs/openui-lang/developer-tools).
Use these findings to prioritize improvements to your component library, system prompt, and model selection.
## Accessing conversations [#accessing-conversations]
To build your own thread switcher or history view, read the data with hooks instead of touching storage. `useThread` selects from the current thread; `useThreadList` selects from all threads.
```tsx
import { useThread, useThreadList } from "@openuidev/react-ui";
function ChatCount() {
const count = useThreadList((s) => s.threads.length);
return {count} chats;
}
```
Both are selector hooks: pass a function that picks the slice you need, and the component re-renders only when that slice changes. They work anywhere inside `AgentInterface`. See [Hooks](/docs/agent/reference/hooks) for the full state.
# Generative UI
> Render streamed OpenUI Lang as interactive application components inside a conversation.
Source: https://www.openui.com/docs/agent/core-concepts/generative-ui
An assistant message is normally markdown text. With OpenUI Lang, the agent can compose the
components your application defines: tables, forms, charts, cards, or domain-specific controls.
The interface renders progressively as the response streams.
Build the component library with OpenUI Lang, then pass the same library to Agent Interface:
```tsx
import { AgentInterface } from "@openuidev/react-ui";
import { library } from "./library";
## A custom tool timeline [#a-custom-tool-timeline]
`ToolCallTimeline` mounts as soon as the first tool activity appears, before the answer. It receives live updates for the whole turn and replaces the built-in timeline. It remains available after the turn finishes.
The exported `ToolCallTimelineComponent` type defines its props:
| Prop | Type | Meaning |
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------ |
| `activities` | `ToolActivity[]` | The turn's tool calls paired with their available results and current statuses. |
| `steps` | `TimelineStep[]` | Ordered rows: `{ type: "text", id, text }` or `{ type: "activity", activity }`. |
| `isLast` | `boolean` | Whether this turn is currently live. It is `false` for completed turns, including the most recent one. |
| `awaitingResponse` | `boolean` | Whether the turn is live and its answer has not started, includes the delay after tool results arrive. |
This example starts collapsed and updates its tool count as activities arrive:
```tsx
import {
AgentInterface,
MarkDownRenderer,
TimelineEntry,
type ToolCallTimelineComponent,
} from "@openuidev/react-ui";
const ResearchSteps: ToolCallTimelineComponent = ({ activities, steps, isLast }) => (
## Wire items to pages [#wire-items-to-pages]
A navigating item only changes the route. Pair it with a `Route` to render something there.
```tsx
# Introduction
> A complete React SDK for building streaming agent interfaces with OpenUI Lang.
Source: https://www.openui.com/docs/agent/getting-started/introduction
Agent Interface is OpenUI's optional React SDK for teams that want a complete chat surface instead of integrating the renderer into assistant-ui, CopilotKit, or a custom message UI.
It provides conversation layout, threads, streaming messages, tool activity, and generated OpenUI components in one customizable interface. OpenUI Lang remains the interchange format, and you can connect Agent Interface to your existing agent backend.
## When to use Agent Interface [#when-to-use-agent-interface]
Choose Agent Interface when you want OpenUI to provide the complete agent UI shell. If an application already has a conversation framework, follow the [Build Agents guides](/docs/build-agents) to add the OpenUI Lang renderer instead.
## The integration boundary [#the-integration-boundary]
`