OpenUI

@openuidev/react-lang

API reference for the OpenUI Lang runtime, library, parser, and renderer.

Use this package for OpenUI Lang authoring and rendering.

Import

import {
  defineComponent,
  defineFunction,
  defineAction,
  action,
  steps,
  createLibrary,
  Renderer,
  BuiltinActionType,
  buildMessage,
  parseMessage,
  createParser,
  createStreamingParser,
} from "@openuidev/react-lang";

defineComponent(config)

Defines a single component with name, Zod schema, description, and React renderer. Returns a DefinedComponent with a .ref for cross-referencing in parent schemas.

function defineComponent<T extends z.ZodObject<any>>(config: {
  name: string;
  props: T;
  description: string;
  component: ComponentRenderer<z.infer<T>>;
}): DefinedComponent<T>;
interface DefinedComponent<T extends z.ZodObject<any> = z.ZodObject<any>> {
  name: string;
  props: T;
  description: string;
  component: ComponentRenderer<z.infer<T>>;
  /** Use in parent schemas: `z.array(ChildComponent.ref)` */
  ref: z.ZodType<SubComponentOf<z.infer<T>>>;
}

createLibrary(input)

Creates a Library from an array of defined components.

/** The result has functions, actions, and extend set (Required<Library>). */
function createLibrary(input: LibraryDefinition): Required<Library>;

Core types:

interface LibraryDefinition {
  components: DefinedComponent[];
  componentGroups?: ComponentGroup[];
  /** Custom functions, called as `@Name(...)` in programs. */
  functions?: DefinedFunction[];
  /** Custom action steps, used as `@Name(...)` in action props. */
  actions?: DefinedAction[];
  root?: string;
  /** Optional public id. `toSpec()` includes it. */
  id?: string;
}

interface ComponentGroup {
  name: string;
  components: string[];
  notes?: string[];
}

interface Library {
  readonly components: Record<string, DefinedComponent>;
  readonly componentGroups: ComponentGroup[] | undefined;
  /** Optional, so a hand-built Library object still type-checks. createLibrary always sets it. */
  readonly functions?: Record<string, DefinedFunction>;
  /** Optional, like functions. */
  readonly actions?: Record<string, DefinedAction>;
  readonly root: string | undefined;
  readonly id: string | undefined;

  prompt(options?: PromptOptions): string;
  toJSONSchema(): LibraryJSONSchema;
  toSpec(): PromptSpec;
  /** Derive a new library. Optional, like functions. */
  extend?(extension: LibraryExtension): Required<Library>;
}

interface PromptOptions {
  preamble?: string;
  additionalRules?: string[];
  examples?: string[];
  toolExamples?: string[];
  editMode?: boolean;
  inlineMode?: boolean;
  /** Enable Query(), Mutation(), @Run, built-in functions. Default: true if tools provided. */
  toolCalls?: boolean;
  /** Enable $variables, @Set, @Reset, built-in functions. Default: true if toolCalls. */
  bindings?: boolean;
  /** List the built-in functions (@Count, @Sum, ...). Default: true if toolCalls or bindings. */
  builtinFunctions?: boolean;
}

createLibrary throws when a function or action name is a built-in name, Action, Query, Mutation, or the name of a component, function, or action in the same library.

defineFunction(config)

Defines a custom function, called as @Name(...) in programs. See Functions.

function defineFunction<
  T extends z.ZodObject,
  R extends z.ZodType | undefined = undefined,
>(config: {
  name: string;
  description: string;
  /** Key order is the positional order of the arguments. */
  params: T;
  /** Printed in the prompt signature and checked at runtime. */
  returns?: R;
  /** Receives one object keyed by the params names, with defaults applied. */
  fn: (args: z.infer<T>) => R extends z.ZodType ? z.infer<R> : unknown;
}): DefinedFunction<T, R>;

A call with an invalid required argument, an fn that throws, or a result that does not match returns evaluates to null. An invalid optional argument falls back to its default.

defineAction(config)

Defines a custom action step. The renderer does not run it: it calls onAction with { type: name, params }. A step with any invalid argument is a no-op. See Actions.

function defineAction<const N extends string, T extends z.ZodObject>(config: {
  name: N;
  description: string;
  /** Key order is the positional order of the arguments. */
  params: T;
}): DefinedAction<T, N>;

interface DefinedAction<T, N> {
  name: N;
  description: string;
  params: T;
  /** For slots that take only some actions: `z.union([copy.ref, steps.OpenUrl.ref])` */
  ref: ActionRef;
}

action() and steps

action() returns the schema for an action prop, tagged ActionExpression. In the JSON Schema the prop is {"$ref": "#/$defs/ActionExpression"}.

props: z.object({ label: z.string(), action: action().optional() });

steps.OpenUrl.ref and steps.ToAssistant.ref name built-in steps in a restricted slot, together with the .ref of custom actions. Restricted slots shape the prompt and the JSON Schema only; the runtime accepts any step. See Actions.

library.extend(extension)

Returns a new library with the additions. The base library is unchanged. See Extending a library.

interface LibraryExtension {
  components?: {
    /** With slots ("Parent.prop", or "Parent" when it has one content slot), it joins those unions. */
    add?: (DefinedComponent | { component: DefinedComponent; slots: string[] })[];
    /** Replaces the component of the same name. Every parent points at the new one. */
    override?: DefinedComponent[];
    /** Drops it from the library, every parent union, and componentGroups. */
    remove?: string[];
  };
  functions?: { add?: DefinedFunction[] };
  actions?: { add?: DefinedAction[] };
}

extend throws when an added name exists, an overridden or removed name does not, a removal is the root or the only type of a slot, or a slot does not resolve to one content slot. The derived library has no id. Create it once at module scope, since parsers are cached per library object.

buildMessage(input) and parseMessage(raw, options?)

Write and read a stored message with ]]>openui: marker lines. See Message Protocol.

function buildMessage(input: {
  content: string;
  /** A string is written as is, anything else as JSON. Omitted when null or undefined. */
  context?: unknown;
  attributes?: Record<string, string>;
  end?: boolean;
}): string;

function parseMessage(
  raw: string,
  options?: {
    /** Hold back a last line that may be a marker cut mid-stream. */ streaming?: boolean;
  },
): {
  content: string;
  /** Parsed JSON, else raw text. null when absent. */
  context: unknown;
  attributes: Record<string, string>;
  end: boolean;
};

<Renderer />

Parses OpenUI Lang text and renders nodes with your Library.

interface RendererProps<L extends Library = Library> {
  /** OpenUI Lang text, or a stored message with ]]>openui: marker lines. */
  response: string | null;
  library: L;
  isStreaming?: boolean;
  /** Typed from the library: checking event.type narrows params for custom actions. */
  onAction?: (event: LibraryActionEvent<L>) => void;
  onStateUpdate?: (state: Record<string, unknown>) => void;
  initialState?: Record<string, any>;
  onParseResult?: (result: ParseResult | null) => void;
  toolProvider?:
    Record<string, (args: Record<string, unknown>) => Promise<unknown>> | McpClientLike | null;
  /** Custom loading indicator. Defaults to a spinner. */
  queryLoader?: React.ReactNode;
  children?: React.ReactNode;
  onError?: (errors: OpenUIError[]) => void;
}

Renderer slots

Renderer keeps query defaults or cached results visible while loading and after failures, and reports errors through onError. A spinner is shown while queries load; provide queryLoader to customize it. No error/retry view is shown by default. To customize loading feedback and opt into an error/retry presentation, use Renderer.Root with these slots:

SlotPurpose
Renderer.ContentGenerated UI; accepts div props and stays mounted during query failures.
Renderer.QueryLoadingChildren shown while queries wait or fetch; omitted children use queryLoader or the default spinner.
Renderer.QueryErrorChildren shown when queries fail; renders nothing without children.
Renderer.RetryA retry button; accepts standard button props.

Root accepts the same runtime props as Renderer and renders only its children. Put overlays in your own positioned container. Slots share context through wrappers and React portals. useRendererQuery() returns { isLoading, errors, retry, isRetrying } within that root. Renderer.Content renders defaults or cached data while queries load and hides content on failure while preserving mounted state through retries.

Renderer accepts slot children to replace its default content composition. Without children, Renderer keeps its default views. Calling preventDefault() in the retry button’s click handler cancels the retry.

<Renderer.Root response={response} library={library} toolProvider={tools}>
  <div style={{ position: "relative" }}>
    <Renderer.Content />
    <Renderer.QueryLoading>Loading data…</Renderer.QueryLoading>
    <Renderer.QueryError>
      <p role="alert">Data is unavailable.</p>
      <Renderer.Retry>Try again</Renderer.Retry>
    </Renderer.QueryError>
  </div>
</Renderer.Root>

Tool Provider

Handles Query() and Mutation() tool calls at runtime. The toolProvider prop accepts two forms:

  • Function map: Record<string, (args) => Promise<unknown>>. The simplest option.
  • MCP client: any object implementing callTool({ name, arguments }) (e.g. from @modelcontextprotocol/sdk)

The Renderer detects which form was passed and normalizes internally.

Error types

type OpenUIErrorSource = "parser" | "runtime" | "query" | "mutation";

interface OpenUIError {
  source: OpenUIErrorSource;
  code: string;
  message: string;
  statementId?: string;
  component?: string;
  path?: string;
  /** Tool name for query and mutation errors. */
  toolName?: string;
  hint?: string;
  /** "warning" when the page still rendered; absent means error. */
  severity?: "warning";
}

class ToolNotFoundError extends Error {
  toolName: string;
  availableTools: string[];
}

Error codes: unknown-component, missing-required, null-required, type-mismatch, excess-args, inline-reserved, unknown-function, no-root, tool-not-found, tool-error, mcp-error, parse-failed, parse-exception, runtime-error, render-error.

Actions

enum BuiltinActionType {
  ContinueConversation = "continue_conversation",
  OpenUrl = "open_url",
}

interface ActionEvent {
  type: string;
  params: Record<string, any>;
  humanFriendlyMessage: string;
  formState?: Record<string, any>;
  formName?: string;
}

Action steps (runtime types from the evaluator):

type ActionStep =
  | { type: "run"; statementId: string; refType: "query" | "mutation" }
  | { type: "continue_conversation"; message: string; context?: unknown }
  | { type: "open_url"; url: string }
  | { type: "set"; target: string; valueAST: ASTNode }
  | { type: "reset"; targets: string[] }
  | { type: "custom_action"; name: string; params: Record<string, unknown> };

Every step call evaluates to a plan, { steps: ActionStep[] }. A step that cannot run gives a plan with no steps.

Step typeTriggered byDescription
"run"@Run(ref)Execute a Mutation or re-fetch a Query. refType indicates which.
"set"@Set($var, val)Change a $variable. valueAST is evaluated at click time.
"reset"@Reset($a, $b)Restore $variables to declared defaults.
"continue_conversation"@ToAssistant("msg")Send message to LLM. Optional context of any type.
"open_url"@OpenUrl("url")Open URL in new tab.
"custom_action"@Name(...)A custom action. Reaches onAction as { type: name, params }.

Parser APIs

Both createParser and createStreamingParser accept a LibraryJSONSchema (from library.toJSONSchema()).

interface LibraryJSONSchema {
  $defs?: Record<
    string,
    {
      properties?: Record<string, unknown>;
      required?: string[];
    }
  >;
}

function createParser(schema: LibraryJSONSchema, rootName?: string): Parser;
function createStreamingParser(schema: LibraryJSONSchema, rootName?: string): StreamParser;

interface Parser {
  parse(input: string): ParseResult;
}

interface StreamParser {
  /** Pass complete once the stream has ended. */
  push(chunk: string, complete?: boolean): ParseResult;
  /** Set the full text so far. Only the new part is parsed. */
  set(fullText: string, complete?: boolean): ParseResult;
  getResult(complete?: boolean): ParseResult;
}

Pass the root component name as rootName, usually library.root. A program without a root statement then falls back to the first statement that calls that component. Without rootName, such a program reports no-root and has no root.

const parser = createParser(library.toJSONSchema(), library.root);

Core parsed types:

interface ElementNode {
  type: "element";
  typeName: string;
  props: Record<string, unknown>;
  partial: boolean;
}

/**
 * Parser-level validation errors (schema mismatches).
 */
type ValidationErrorCode =
  | "missing-required"
  | "null-required"
  | "unknown-component"
  | "inline-reserved"
  | "excess-args"
  | "type-mismatch"
  | "unknown-function"
  | "no-root";

interface ValidationError {
  code: ValidationErrorCode;
  component: string;
  path: string;
  message: string;
  statementId?: string;
  /** "warning" when the program still renders, e.g. a no-root fallback entry. */
  severity?: "warning";
}

interface ParseResult {
  root: ElementNode | null;
  /** Set when the entry picks a component at runtime (`root = $t ? A : B`). */
  rootExpression?: ASTNode;
  meta: {
    incomplete: boolean;
    /** References used but not yet defined (dropped as null in output). */
    unresolved: string[];
    /** Value statements defined but not reachable from the entry. Excludes $state, Query, and Mutation. Empty when there is no entry. */
    orphaned: string[];
    statementCount: number;
    /**
     * Validation errors:
     * - "missing-required" — required prop not provided
     * - "null-required" — required prop explicitly null
     * - "unknown-component" — component not in library schema
     * - "inline-reserved" — Query/Mutation used inline instead of top-level
     * - "excess-args" — more positional args than schema params (extras dropped, component still renders)
     */
    errors: ValidationError[];
  };
  /** Extracted Query() statements with tool name, args AST, defaults AST */
  queryStatements: QueryStatementInfo[];
  /** Extracted Mutation() statements with tool name, args AST */
  mutationStatements: MutationStatementInfo[];
  /** Declared $variables with their default values */
  stateDeclarations: Record<string, unknown>;
}

Context hooks (inside renderer components)

// Reactive state binding — preferred for form inputs and $variable-bound components
function useStateField(
  name: string,
  value?: unknown,
): {
  value: unknown;
  setValue: (value: unknown) => void;
};

function useRenderNode(): (value: unknown) => React.ReactNode;
function useTriggerAction(): (
  userMessage: string,
  formName?: string,
  /** An evaluated action prop (ActionPlan), or a legacy { type, params } object. */
  action?: ActionPlan | { type?: string; params?: Record<string, any> },
) => void | Promise<void>;
function useIsStreaming(): boolean;
function useIsQueryLoading(): boolean;
function useGetFieldValue(): (formName: string | undefined, name: string) => any;
function useSetFieldValue(): (
  formName: string | undefined,
  componentType: string | undefined,
  name: string,
  value: any,
  shouldTriggerSaveCallback?: boolean,
) => void;
function useFormName(): string | undefined;
function useSetDefaultValue(options: {
  formName?: string;
  componentType?: string;
  name: string;
  existingValue: any;
  defaultValue: any;
  shouldTriggerSaveCallback?: boolean;
}): void;

Form validation APIs

interface FormValidationContextValue {
  errors: Record<string, string | undefined>;
  getFieldError: (name: string) => string | undefined;
  validateField: (name: string, value: unknown, rules: ParsedRule[]) => boolean;
  registerField: (name: string, rules: ParsedRule[], getValue: () => unknown) => void;
  unregisterField: (name: string) => void;
  validateForm: () => boolean;
  clearFieldError: (name: string) => void;
}

function useFormValidation(): FormValidationContextValue | null;
function useCreateFormValidation(): FormValidationContextValue;
function validate(
  value: unknown,
  rules: ParsedRule[],
  customValidators?: Record<string, ValidatorFn>,
): string | undefined;
function parseRules(rules: unknown): ParsedRule[];
function parseStructuredRules(rules: unknown): ParsedRule[];
const builtInValidators: Record<string, ValidatorFn>;

Context providers for advanced usage:

const FormValidationContext: React.Context<FormValidationContextValue | null>;
const FormNameContext: React.Context<string | undefined>;

On this page