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.1v0.5
PurposeGenerate UI responses in chatBuild interactive apps with live data
DataHardcoded in the outputFetched from your tools via Query / Mutation
StateNone - static renderReactive $variables with two-way binding
InteractivitySend message back to LLMButtons call tools directly via @Run, update state via @Set
LLM roleGenerates UI on every turnGenerates UI once, then gets out of the way
Data transformsNone@Count, @Filter, @Sort, @Each, @Sum, etc.
ComponentsLayout + 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:

  • $days is reactive state - user changes the Select, chart updates
  • Query("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.

On this page