OpenUI
Specification

Specification

Language specification for OpenUI Lang 1.0. The entry rule, @-calls, action expressions, stored messages, and error codes.

OpenUI Lang 1.0 extends the v0.5 specification. The statement syntax is unchanged. 1.0 adds one expression form, a lone action without an Action([...]) wrapper. It also adds a clear entry rule, custom functions and actions, a format for stored messages, and new error codes. See What's New in 1.0 for upgrade notes.

Syntax Overview

A program is a list of assignment statements, one per line:

identifier = Expression

The statement kinds are the same as in v0.5: component or value statements, $variable declarations, and Query or Mutation statements. 1.0 adds these expression forms:

FormSyntaxExample
Function call@Name(args)@Percent(done, total)
Action@Name(args)@ToAssistant("Tell me more")
Several actionsAction([actions])Action([@Run(save), @Reset($title)])

All other expressions and operators work as in v0.5.

Core Rules

  1. One statement per line: identifier = Expression
  2. Entry: the statement named root. See Entry Statement.
  3. Positional arguments: mapped to props, function params, and action params by Zod key order. null skips an optional argument.
  4. Forward references allowed: a statement can use a name defined later.
  5. @ is required for functions and actions. Count(x) is not a call. Only Action([...]) works without the @.
  6. Last definition wins: if two statements share a name, the later one is used.

Entry Statement

The entry is the statement named root. Its value must be a component: a call, a reference to one, or an expression that gives one.

$compact = false
root = $compact ? compactView : fullView
compactView = TextContent("3 open")
fullView = Card([TextContent("3 open tickets")])

With no root statement, the first statement that calls the library's root component is the entry, wherever it is. It renders, with a no-root warning:

title = TextContent("Orders")
page = Card([title])

In every other case nothing renders. The parser reports no-root on every parse. Renderers, such as the React Renderer, hold errors back until the stream ends, so a missing entry is not shown as an error while streaming.

The fallback needs the root component name. The Renderer takes it from the library. A parser on the server needs it as the second argument: createParser(library.toJSONSchema(), library.root).

Function Calls

@Name(args) calls a built-in function or a custom function from the library. The built-ins are the same as in v0.5. Built-ins are looked up first, and a library cannot reuse the name of a built-in function or action.

Custom function arguments are checked like component props:

  • A literal argument with the wrong type, or a required one that is missing or null, makes the call null. The error is type-mismatch, missing-required, or null-required.
  • A wrong optional argument is dropped and its default applies. Extra arguments report excess-args and are dropped.
  • A required argument that is null at runtime, such as an unset $variable, makes the call null with no error.
  • A wrong runtime type, a function that throws, or a result that does not match returns reports runtime-error. The call is null.

Custom functions must be pure and synchronous. The client may call them any number of times, in any order.

A call to an @Name that is not a built-in function, an action, or a library function reports unknown-function and evaluates to null. The rest of the statement still renders:

root = Card([total])
total = TextContent("Total: " + @Total(items))

See Functions.

Action Expressions

One action needs no wrapper. Several actions go in Action([...]) and run in order:

askBtn = Button("Tell me more", @ToAssistant("Tell me more about this order"))
saveBtn = Button("Save", Action([@Run(save), @Run(orders), @Reset($note)]))

@ToAssistant("Hi") is the same as Action([@ToAssistant("Hi")]). A nested Action([...]) is flattened. A plain list of actions, such as [@Set($a, 1), @ToAssistant("Hi")], is not an action. It is plain data, with no error.

Built-in actions

ActionDescription
@ToAssistant(message, context?)Send a continue_conversation event. context can be any value
@OpenUrl(url)Send an open_url event
@Set($var, value)Set a $variable. value is evaluated when the action runs
@Reset($var1, $var2)Restore $variables to declared defaults
@Run(ref)Run a Mutation or re-fetch a Query. A mutation that fails stops the rest
@Name(args)A custom action. Sends an event with type: "Name" and params

The context of @ToAssistant reaches the host unchanged in params.context:

approveBtn = Button("Approve", @ToAssistant("Approve this order", {orderId: 42}))

Each event carries the form state as of when its action runs.

Invalid actions

An action with invalid arguments is skipped and the rest still run. A mutation that runs and fails stops the rest. A custom action with any invalid argument, required or optional, is a no-op. It does not fall back to a default.

Only an @Name(...) call written in the program creates a custom action. An object, a $variable, or a query row shaped like an action never reaches the host as one.

Restricted action slots

A prop can accept only some actions. The prompt prints it with the action names:

ShareButton(label: string, share: @CopyToClipboard | @OpenUrl)

In the JSON Schema, the prop is an anyOf of $refs to the action names. A prop that takes any action is {"$ref": "#/$defs/ActionExpression"}. In 1.0 the restriction shapes the prompt and the schema only. At runtime the slot accepts any action.

See Actions.

Stored Messages

A host stores a response as text, with marker lines that start with ]]>openui:. The model never writes or sees them.

]]>openui:content?library=support%401.2.0
root = Card([title])
title = TextContent("Order 42")
]]>openui:context
{"formState":{"note":"rush"}}
]]>openui:end
LineMeaning
]]>openui:contentStarts the program, or prose with fenced programs
]]>openui:contextStarts host data, such as form state. Usually JSON
]]>openui:endThe stream finished

A ]]>openui:content marker in the middle of a line is first moved onto its own line. Other markers count only at the start of a line. With several content sections, the last one wins. Text with no markers is the content. end is absent when the host did not mark the stream as finished. A message without it still renders. See Message Protocol.

Error Codes

CodeMeaningResult
unknown-componentComponent not in the libraryComponent dropped
missing-requiredRequired prop or param missingDefault if any, else dropped
null-requiredRequired prop or param is nullSame as missing-required
type-mismatchWrong type, enum value, or data in a component-only slotDefault if any, else dropped
excess-argsMore arguments than props or paramsExtras dropped, still renders
inline-reservedQuery or Mutation used inlineEvaluates to nothing
unknown-function@Name is not a built-in, action, or library functionCall is null, statement kept
no-rootNo usable entryWarning with the fallback, or fatal
parse-failedText with no statementsFatal
runtime-errorEvaluation or a function call failedThe prop or call is null

unknown-function and no-root are new in 1.0. Query, mutation, and render errors are as in the Renderer docs.

An error with severity: "warning" did not stop the page from rendering. Today that is the no-root fallback. Skip warnings when you send errors back to the model.

Prompt Feature Flags

The flags are the same as in v0.5, plus builtinFunctions. It lists the built-in functions in a library without toolCalls or bindings. A library's own functions and actions are always listed. See System Prompts.

Streaming

Forward references and hoisting work as in v0.5. When the stream ends, the result equals a parse of the full text: statements, entry, errors, and state defaults.

On this page