Evolution Guide
How OpenUI Lang evolved from static UI generation to interactive, data-driven apps.
v0.1 → v0.5
OpenUI Lang started as a way to generate static UI from LLM output, a token-efficient alternative to JSON for rendering chat responses. v0.5 turns it into a language for building standalone interactive apps that run independently of the LLM.
The shift
| v0.1 | v0.5 | |
|---|---|---|
| Purpose | Generate UI responses in chat | Build interactive apps with live data |
| Data | Hardcoded in the output | Fetched from your tools via Query / Mutation |
| State | None - static render | Reactive $variables with two-way binding |
| Interactivity | Send message back to LLM | Buttons call tools directly via @Run, update state via @Set |
| LLM role | Generates UI on every turn | Generates UI once, then gets out of the way |
| Data transforms | None | @Count, @Filter, @Sort, @Each, @Sum, etc. |
| Components | Layout + content | + Modal, auto-dismiss Callout |
From chat response to standalone app
v0.1: Static UI generation
The LLM generates a component tree. It renders once. User wants changes? Ask the LLM again.
root = Stack([header, chart])
header = CardHeader("Q4 Revenue")
chart = BarChart(["Oct", "Nov", "Dec"], [Series("Revenue", [120, 150, 180])])Data is hardcoded. No interactivity beyond clicking a button to send a message back to the LLM.
v0.5: Interactive app with live data
The LLM generates code that connects to your tools. The runtime fetches data, handles user interactions, and updates the UI - all without going back to the LLM.
$days = "7"
filter = Select("days", $days, [SelectItem("7", "7 days"), SelectItem("30", "30 days")])
data = Query("analytics", {days: $days}, {rows: []})
chart = LineChart(data.rows.day, [Series("Revenue", data.rows.revenue)])
kpi = Card([TextContent("Total", "small"), TextContent("" + @Sum(data.rows.revenue), "large-heavy")])
root = Stack([CardHeader("Revenue Dashboard"), filter, Stack([kpi], "row"), chart])What's different:
$daysis reactive state - user changes the Select, chart updatesQuery("analytics", {days: $days})fetches live data from your MCP tools@Sum(data.rows.revenue)computes the KPI from live data- No LLM roundtrip when the user changes the filter
What v0.5 adds
Reactive state
Declare variables, bind them to inputs, reference them in expressions. Everything updates automatically.
$search = ""
searchBox = Input("search", $search, "Search...")
filtered = @Filter(data.rows, "title", "contains", $search)See Reactive State.
Data fetching
Query reads data from your tools. Mutation writes. The runtime calls your MCP endpoint directly - no LLM involved.
tickets = Query("list_tickets", {}, {rows: []})
createResult = Mutation("create_ticket", {title: $title})See Queries & Mutations.
Built-in functions
@-prefixed functions for transforming data inline: @Count, @Filter, @Sort, @Sum, @Each, @Round, and more.
openCount = @Count(@Filter(tickets.rows, "status", "==", "open"))
sorted = @Sort(tickets.rows, "created", "desc")See Built-in Functions.
Action composition
Buttons can run mutations, refresh queries, set state, and reset forms - all in a single action.
submitBtn = Button("Create", Action([@Run(createResult), @Run(tickets), @Set($success, true), @Reset($title)]))Reactive component props ($binding)
Components can accept $variables as props for reactive binding. For example, a Modal's open prop or a Callout's visible prop can be bound to a $variable, and the component reads and writes the variable directly.
This is a library-level feature (component authors use useStateField), not a language change. The language just passes the $variable as a positional argument.
Incremental editing
LLM outputs only changed statements. The parser merges by name - existing code stays intact.
See Incremental Editing.
What stayed the same
The core language is unchanged:
- Line-oriented assignment syntax:
identifier = Expression - Positional arguments mapped by Zod schema key order
- Forward references and streaming-first rendering
- Component resolution and validation
v0.5 is a superset - all v0.1 code is valid v0.5 code.