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 = ExpressionThe 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:
| Form | Syntax | Example |
|---|---|---|
| Function call | @Name(args) | @Percent(done, total) |
| Action | @Name(args) | @ToAssistant("Tell me more") |
| Several actions | Action([actions]) | Action([@Run(save), @Reset($title)]) |
All other expressions and operators work as in v0.5.
Core Rules
- One statement per line:
identifier = Expression - Entry: the statement named
root. See Entry Statement. - Positional arguments: mapped to props, function params, and action params by Zod key order.
nullskips an optional argument. - Forward references allowed: a statement can use a name defined later.
@is required for functions and actions.Count(x)is not a call. OnlyAction([...])works without the@.- 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 callnull. The error istype-mismatch,missing-required, ornull-required. - A wrong optional argument is dropped and its default applies. Extra arguments report
excess-argsand are dropped. - A required argument that is
nullat runtime, such as an unset$variable, makes the callnullwith no error. - A wrong runtime type, a function that throws, or a result that does not match
returnsreportsruntime-error. The call isnull.
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
| Action | Description |
|---|---|
@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| Line | Meaning |
|---|---|
]]>openui:content | Starts the program, or prose with fenced programs |
]]>openui:context | Starts host data, such as form state. Usually JSON |
]]>openui:end | The 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
| Code | Meaning | Result |
|---|---|---|
unknown-component | Component not in the library | Component dropped |
missing-required | Required prop or param missing | Default if any, else dropped |
null-required | Required prop or param is null | Same as missing-required |
type-mismatch | Wrong type, enum value, or data in a component-only slot | Default if any, else dropped |
excess-args | More arguments than props or params | Extras dropped, still renders |
inline-reserved | Query or Mutation used inline | Evaluates to nothing |
unknown-function | @Name is not a built-in, action, or library function | Call is null, statement kept |
no-root | No usable entry | Warning with the fallback, or fatal |
parse-failed | Text with no statements | Fatal |
runtime-error | Evaluation or a function call failed | The 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.