For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt, and this page is available as Markdown at /guide/genui/openui/overview.md.
This is the dev preview website. Check out the document at lynx-stack.dev

Overview and architecture

This page explains what @lynx-js/genui/openui is, how OpenUI Lang maps to a trusted ReactLynx component tree, and what happens between a streamed Agent response and the rendered UI.

What this package is

@lynx-js/genui/openui is the ReactLynx client runtime for OpenUI Lang v0.5. It combines the framework-agnostic parser and evaluator from @openuidev/lang-core with a ReactLynx renderer and a built-in mobile component library.

The package provides:

  • <OpenUiRenderer> for raw or pre-parsed OpenUI input;
  • createOpenUiLibrary() and 26 trusted default component implementations;
  • incremental parsing for model streams;
  • reactive $variables, expression evaluation, and form state;
  • Query(), Mutation(), and multi-step Action([...]) execution;
  • structured parser, runtime, render, and tool errors;
  • prompt builders that describe the matching component contract to an Agent.

It does not host an Agent, call an LLM, define your network transport, or provide backend tools. Your application owns those pieces.

Quick start

Install the package and optional default theme in a ReactLynx app:

pnpm add @lynx-js/genui @lynx-js/react @lynx-js/lynx-ui
import { createOpenUiLibrary, OpenUiRenderer } from '@lynx-js/genui/openui';
import { useMemo } from '@lynx-js/react';

import '@lynx-js/genui/openui/styles/theme.css';

export function GeneratedView({ response }: { response: string }) {
  const library = useMemo(() => createOpenUiLibrary(), []);

  return (
    <view className='openui-light'>
      <OpenUiRenderer response={response} library={library} />
    </view>
  );
}

The simplest valid response is:

root = Stack([message])
message = TextContent("Hello OpenUI")

OpenUI uses positional arguments. The parser maps them to named props in the order of each component's Zod schema. Forward references are allowed, so the root can refer to statements that appear later in the response.

Stream model output

Pass the accumulated model text as response and the generation state as isStreaming. Reuse existing React or conversation state if your application already owns both; no separate parser is needed.

  • response is the full text received so far, not the latest delta. Append deltas; replace the text when your transport sends a cumulative snapshot.
  • Keep isStreaming={true} throughout generation. Partial UI renders, while Query execution, mutation registration, and built-in interactions wait.
  • On successful completion, apply the authoritative final text and set isStreaming={false} together. Its default is false; static or persisted complete responses can omit it.

Connect your transport

Keep text and generation state in one state object so they update together. The handlers below illustrate connecting a transport that emits deltas or full snapshots; use the handler matching your transport's text format:

import { createOpenUiLibrary, OpenUiRenderer } from '@lynx-js/genui/openui';
import { useMemo, useState } from '@lynx-js/react';

export function GeneratedView() {
  const library = useMemo(() => createOpenUiLibrary(), []);
  const [output, setOutput] = useState({
    response: null as string | null,
    isStreaming: false,
  });

  function onStart() {
    setOutput({ response: '', isStreaming: true });
  }

  function onDelta(delta: string) {
    setOutput((current) => ({
      response: (current.response ?? '') + delta,
      isStreaming: true,
    }));
  }

  function onSnapshot(fullText: string) {
    setOutput({ response: fullText, isStreaming: true });
  }

  function onDone(finalText: string) {
    setOutput({ response: finalText, isStreaming: false });
  }

  function onFailureOrCancel() {
    setOutput({ response: null, isStreaming: false });
  }

  // Connect these handlers to your transport.
  return <OpenUiRenderer library={library} {...output} />;
}

If completion provides no final text, keep the accumulated response and update only the flag: setOutput(current => ({ ...current, isStreaming: false })). Even when final text equals the last partial, the flag must change to enable queries, completion diagnostics, and interactions. Custom interactive components should honor useIsStreaming() too.

How incremental parsing works

The renderer retains a streaming parser while library stays the same. Although response supplies the full accumulated text, the parser detects appended text, caches completed statements, and reparses the unfinished tail to preview partial output. Forward references become renderable when their targets arrive. Changing or shortening previously received text resets the parser's cache; keep Library identity stable with useMemo or a module constant.

This is statement-level incremental parsing: preprocessing and result rebuilding still run on each text update, and one long unfinished statement can be reparsed repeatedly. It does not provide a component-patch protocol. Changing only isStreaming with unchanged text does not rerun parsing.

Completion, cancellation, and new sessions

For SSE, finish only after the successful done event, using its authoritative text. A connection closing by itself does not confirm success.

On failure or cancellation, clear incomplete text with { response: null, isStreaming: false }; do not enable queries or actions for an unfinished result. State updates do not cancel network requests: your transport owns cancellation and error reporting. Before starting another generation, stop the old request and reject late events, then reset the text. Clearing response does not reset runtime form state; use a new renderer key when a fresh session should reset that state.

The mental model

In ordinary React, your source code chooses a component and passes props:

<Card>
  <TextContent text='Hello' />
</Card>;

In OpenUI, the Agent writes the same intent as declarative assignments:

root = Card([message])
message = TextContent("Hello")

The renderer never evaluates generated JavaScript. It parses the text against the JSON Schema produced by your Library, turns valid component calls into an element tree, and looks up each element's trusted ReactLynx implementation by name. A component that is not in the Library cannot be rendered.

The Library is therefore the contract on both sides:

Agent prompt                              ReactLynx client
Library signatures                       Library implementations
Stack(children, direction?, ...)   <──►   Stack -> trusted renderer
Text(text, variant?)               <──►   Text  -> trusted renderer

Keep the prompt Library and renderer Library aligned whenever you add or override a component. A name or prop-order mismatch can produce text that the client cannot validate.

OpenUI Lang v0.5 in one minute

An OpenUI program contains one assignment per line. There are three statement families:

StatementExamplePurpose
Componentheader = CardHeader("Orders")Declares a renderable component.
State$status = "open"Declares reactive client state and its default value.
Dataorders = Query("list_orders", { status: $status }, { rows: [] })Declares a read or write tool operation.

Expressions can contain primitives, arrays, objects, references, member access, operators, ternaries, and built-ins such as @Count(...). The renderer needs a statement named root; by default its component must be Stack because that is the default Library root.

State values are reactive. When $status changes, expressions that read it are evaluated again and Queries whose arguments depend on it are refreshed.

$status = "open"
orders = Query("list_orders", { status: $status }, { rows: [] })
root = Stack([summary, refresh])
summary = TextContent("Orders: " + @Count(orders.rows))
refresh = Button("Refresh", Action([@Run(orders)]), "secondary")

For the complete language grammar and built-ins, read the OpenUI Lang v0.5 specification.

The end-to-end picture

OpenUI is a loop between an Agent that writes declarative UI and a client that parses, evaluates, and renders it.

       ┌──────────────── Your application ────────────────┐
user   │                                                  │
input ─┼─► Transport ──prompt/action──► Agent service     │
       │      ▲                              │             │
       │      │   accumulated OpenUI text    │             │
       │      └──────────────────────────────┘             │
       │      │                                            │
       │      ▼                                            │
       │ <OpenUiRenderer response={text} ...>               │
       │      │                                            │
       │      ├─ parser ─► AST ─► evaluator ─► UI tree     │
       │      ├─ Store ($state + form state)               │
       │      └─ QueryManager ─► your toolProvider         │
       │                    │                               │
       │                    └─ onAction ─► host/transport ──┤
       └───────────────────────────────────────────────────┘
  1. Your transport sends the user request to an Agent service.
  2. The service calls a model with an OpenUI system prompt built from the same component contract the client supports.
  3. The transport accumulates text deltas as response and passes it with isStreaming to <OpenUiRenderer>.
  4. The streaming parser validates completed statements and resolves references.
  5. The runtime initializes state and evaluates expressions. After generation completes, it runs Queries through your toolProvider.
  6. The renderer walks the evaluated root and mounts trusted ReactLynx components from the Library.
  7. User interactions execute action steps. Host-facing actions leave through onAction; state and tool steps stay inside the runtime.

Inside the client

response creates the complete v0.5 runtime, with isStreaming controlling when queries and interactions become available:

response text
     │
     ▼
createStreamingParser(library.toJSONSchema(), library.root)
     │
     ├─ ParseResult: root + state/query/mutation statements + metadata
     │
     ▼
useOpenUIState
     ├─ Store: $variables and form fields
     ├─ QueryManager: Query/Mutation execution and cache
     ├─ EvaluationContext: state and data reference resolution
     └─ structured errors
     │
     ▼
RenderNode ──name lookup──► library.components[name].component

Important runtime behavior:

  • Incremental parsing. The parser caches completed statements while the accumulated response grows. Forward references become renderable when their targets arrive.
  • Stable Library. Create the Library once with useMemo or outside the component. Changing its identity creates a new parser and can reset parsing work.
  • Streaming guard. Pass isStreaming={true} while generation is active. Query execution and mutation registration wait for stable output, and built-in interactions are disabled.
  • Reactive state. $variables and form values live in one external Store. onStateUpdate can persist its snapshots; $-prefixed values in initialState hydrate reactive declarations.
  • Queries and mutations. Query defaults and prefetched initialQueryResults participate in the first synchronous render after their response finishes streaming. A configured tool provider revalidates Queries after commit and whenever referenced state changes. Mutations are registered but run only when an action calls @Run(mutationRef).
  • Sequential actions. @Run, @Set, @Reset, @ToAssistant, and @OpenUrl execute in order. A failed mutation stops the remaining steps.
  • Soft component failure. An element whose component name is not registered renders nothing. onError receives other parse, evaluation, render, and tool failures after streaming settles.

Who owns what

PieceRuns inOwnerResponsibility
Agent serviceServerYour applicationProduces raw OpenUI Lang using the system prompt and component/tool contract.
Transport adapterClient shellYour applicationDelivers deltas or snapshots, confirms completion, handles cancellation, and forwards conversation actions.
Message storeClientThis packageAccumulates OpenUI text and exposes generation state to the renderer.
LibraryClient + Agent contractSharedNames components, fixes positional prop order, provides JSON Schema, and maps names to trusted renderers.
Parser/evaluatorClientThis package + lang-coreParses statements, validates props, resolves state/data expressions, and reports structured errors.
Store/QueryManagerClientThis package + lang-coreOwns reactive/form state and executes tool-backed Query/Mutation statements.
<OpenUiRenderer>ClientThis packageRenders the evaluated root and wires state, tools, and actions to ReactLynx components.
toolProviderClient integrationYour applicationMaps tool names to async functions or an MCP-compatible client.

<OpenUiRenderer> props

Pass the full accumulated response and set isStreaming={true} during generation. isStreaming defaults to false for complete responses.

PropTypeRequiredPurpose
responsestring | nullyesAccumulated raw OpenUI Lang text. Enables the v0.5 runtime.
libraryLibraryyesComponent schemas and trusted ReactLynx implementations.
isStreamingbooleannoMarks partial model output and disables unstable interactions/tool work.
onAction(event: ActionEvent) => voidnoReceives assistant and URL actions that the host must handle.
onStateUpdate(state: Record<string, unknown>) => voidnoReceives reactive/form state snapshots for persistence.
initialStateRecord<string, unknown>noHydrates $variables and form state.
initialQueryResultsRecord<string, unknown>noSupplies prefetched Query results for the first synchronous render.
onParseResult(result: ParseResult | null) => voidnoExposes the latest raw AST and parser metadata.
toolProviderfunction map, MCP-like client, or nullnoExecutes tools referenced by Query/Mutation statements.
queryLoaderReactNodenoReplaces the default indicator while Queries are in flight.
onError(errors: OpenUIError[]) => voidnoReceives deduplicated structured errors after streaming.

<OpenUiRenderer result={parseResult} library={library}> remains available for legacy/static callers. It can render a pre-parsed element tree and forward simple actions, but it does not create the v0.5 QueryManager or fully execute @Run, @Set, and @Reset. Do not use it for a new v0.5 integration.

Prefetched Query results

Use initialQueryResults when Query data is already available before the ReactLynx first screen. Each key is the Query assignment name (statement ID), so Queries that call the same tool with different arguments remain distinct:

orders = Query("list_orders", { status: "open" }, { rows: [] })
<OpenUiRenderer
  response={response}
  library={library}
  initialQueryResults={{ orders: prefetchedOrders }}
/>;

The prefetched value, including null, false, or 0, takes precedence over the Query default during the first synchronous render. If toolProvider is present, the runtime starts its normal Query execution after commit and replaces the fallback when that request settles. Omit the provider when the prefetched result is an immutable snapshot that should not be revalidated.

Tool providers

For local functions, pass a name-to-function map:

<OpenUiRenderer
  response={response}
  library={library}
  toolProvider={{
    list_orders: async ({ status }) => {
      return await api.listOrders({ status: String(status) });
    },
    save_order: async (args) => await api.saveOrder(args),
  }}
/>;

You may also pass an MCP-compatible object with callTool({ name, arguments }). The runtime extracts structured MCP tool results before expression evaluation. Missing tools and tool failures are reported through onError.

Exports

ImportWhat you get
@lynx-js/genui/openuiRenderer, parser/runtime, hooks, public types, and a Library factory backed by the default catalog.
@lynx-js/genui/openui/explicitThe runtime surface and a catalog-free Library factory for explicitly selected definitions.
@lynx-js/genui/openui/catalogAggregate re-exports of all built-in component definitions.
@lynx-js/genui/openui/catalog/agentBuilt-in definitions with null renderers for Node schema and prompt generation.
@lynx-js/genui/openui/catalog/agent/<Component>One server-side definition and its schema dependencies.
@lynx-js/genui/openui/catalog/<Component>One built-in definition and its component-level dependencies, for example catalog/Stack.
@lynx-js/genui/openui/promptHeadless prompt Library, prompt builder, default prompt, and prompt-specific types.
@lynx-js/genui/openui/styles/theme.cssOptional light/dark CSS custom-property tokens.

Component styles, the core renderer stylesheet, and the Material Icons font are implementation details imported by the relevant modules. Do not import private files under styles/catalog or dist/core.

Where to go next