OR Key
drop another .md file to compare - side-by-side diff against openui-lang

openui-lang

The format your assistant uses to build live screens for you.
description: "Triggers on prompt mention of 'openui-lang', 'lang', 'compose UI', 'generative UI', 'shadcn', 'render shape', 'Query()', '@Filter', 'defineComponent', 'createLibrary', '[[TOOL:Lang]]'."
personal 2 files

What it does for you

The format your assistant uses to build live screens for you.

What it produces

A recent result, so you can see the kind of work it returns.

loading…

How to get it

These run inside the Snappy workspace. Want this working in your business? I set skills like this up with you, in one focused week.

Work with me
For developers how this skill is built, graded, and how it runs

at a glance- the short version

eval modeauto-shape
stages1

what's inside - the parts that make up a skill 2/4 present

A skill is just a few plain-text files. Only the main one is required. The rest are optional, added as the work needs them. This is what the skill is made of; how it runs is just below.

The skill
state/skills/openui-lang/SKILL.md present
the skill itself, in plain text
The main file. It says what the skill is and lays out the steps in plain English.
Code
state/lib/openui-lang.ts not present
code the skill can run
Optional. Many skills are just words and need no code at all.
Scripts
state/bin/openui-lang/ not present
helper scripts
Optional. Added when a skill has a few commands to run.
Loader
state/skills/openui-lang/AGENTS.md present
what the AI loads on the fly
Loaded automatically the moment this skill is needed. Kept short on purpose.

how it runs - the shared frame every skill uses 3/5 present

Every skill runs the same way. One part does the work, a separate part checks it, and a short loader hands the AI exactly what it needs for the job. Anything this skill doesn't use shows a one-line note saying why, on purpose, not by accident.

makes the work The worker
inferred
grep '"[A-Z][A-Za-z]+"' from a command
No worker named, so the first command in the skill is treated as the worker.
checks the work The reviewer
inferred
shape gate an automatic check
The check is an automatic pass or fail on the shape of the result, run separately from the work itself.
frame
learns Self-correction
present
fixes itself learns from gaps
When a run hits a gap, the skill gets edited on the spot [FIXED] or queued for a bigger rewrite [LOGGED], so it keeps getting better.
tidies up Background fixes
present
queued for rewrite runs in the background
Bigger fixes that can't be made on the spot get queued and rewritten in the background later.
remembers Run history
present
state/log/evals.ndjson auto-shape runs
Every run is written down here, so the next time this skill is used it already knows how the last runs went.
Critical rules the things this skill must not get wrong
No must-not-break rules called out for this skill. Anything important lives in the writeup below.

what it has learned - fixes written back in over time sample

When a run hits something this skill didn't handle, the fix gets written back into the skill so it doesn't happen again. FIXED means it was corrected on the spot. LOGGED means it's queued for a bigger rewrite. Either way, the skill gets a little better and never makes the same mistake twice.

  1. Loading feedback rows…

how the work flows- step by step

1 stage
grep
grep -oE '"[A-Z][A-Za-z]+"' \

SKILL.md- the skill, written out in plain English

openui-lang - First-Class Composition Language

Purpose

OpenUI Lang is the assignment-based, line-oriented DSL that the snappy-chat LLM emits to compose generative UI from primitives. It is the single source-of-truth for "how the model expresses a card / chart / list / form" on this surface. This skill exists because every fresh agent forgets that Lang is a thing - the recurring failure mode is a subagent who gets asked to "add a new shape," reaches for a per-shape .tsx file plus a server.ts regex, and re-introduces 200 LOC of dead matcher code instead of just teaching the LLM the right primitive composition.

The cockpit pivoted to OpenUI primitives + LLM composition on 2026-04-30 (see ~/projects/snappy-chat/CLAUDE.md § "Generative-UI shapes (primitives pivot)"). This skill is the structural fix: future agents who mention openui-lang in a brief inherit the full mental model from the loader.

When to Use This Skill

Activates when working with:

  • Any prompt mentioning openui-lang, OpenUI Lang, Lang, [[TOOL:Lang]]
  • The system-prompt section in state/bin/head-screen/server.ts that

teaches the LLM how to emit Lang

  • state/config/openui-lang-prompt.txt (the auto-generated component

signature catalog injected at run time)

  • The Lang entry in web/src/dispatch-card.tsx DISPATCH_REGISTRY
  • web/src/genui-library.tsx (re-exports openuiChatLibrary as

genuiLibrary)

  • Any per-skill state/skills/<slug>/resources/ui.openui dashboard
  • Adding a new generative-UI affordance - primitives-first, not

per-shape-file


What is OpenUI Lang

Plain-English summary: a compact text DSL that says "this UI is built from these named pieces, which compose like this." Each line is one assignment of the form identifier = Expression. The root assignment is special - it's the entry point. Everything else is forward-resolvable: references work both up and down the file.

Why the LLM emits it instead of JSX or JSON:

  • Streaming-friendly. The parser validates line-by-line, so partial

output renders incrementally as tokens arrive. JSON's strict bracket pairing and JSX's nesting both stall progressive rendering.

  • Token-efficient. Documented as ~67% fewer tokens than the

equivalent JSON tree. On a Card-with-chart-and-table dispatch this is the difference between a 1.2s and a 4s first-paint.

  • Graceful invalid-output handling. A malformed sub-expression is

dropped; the rest of the tree still renders. JSON would fail entirely.

  • Positional-only. Card([children], "subtle") not

Card(items: [...], variant: "subtle"). Less syntax for the model to hallucinate; the prop order in defineComponent({props: z.object({...})}) IS the call order.

Conceptual model (four pieces work together):

  1. Library - Zod schemas + React renderers (in our codebase:

openuiChatLibrary from @openuidev/react-ui, re-exported as genuiLibrary in web/src/genui-library.tsx).

  1. Prompt Generator - converts the library into the system-prompt

text the LLM reads. Emitted to state/config/openui-lang-prompt.txt and loaded by state/bin/head-screen/server.ts at startup.

  1. Parser - validates streaming Lang text into typed elements.
  2. Renderer - <Renderer library={genuiLibrary}> from

@openuidev/react-lang; maps parsed elements to React components.

The separation lets the LLM focus on what to render while the framework handles how.


Statement Types

Three statement shapes total. Memorize this:

# 1. Component (the common case)
welcomeCard = Card([CardHeader("Welcome"), TextContent("Hello")])

# 2. State (reactive variable)
$days = 7

# 3. Data (Query / Mutation binding)
agents = Query("get_agents", {}, {agents: []}, 30)

Root rule: every program must define root = Card(...) (or any top-level component). That assignment is the entry point.

Forward references work. You can use a name before it's defined; the parser resolves all references after the full input is parsed. This is the "hoisting" rule from state/config/openui-lang-prompt.txt:148.

Reference-or-die rule. Every variable except root MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.


Language Primitives

The compositional building blocks. One-line each. Find the full list in state/config/openui-lang-prompt.txt (auto-generated from the library).

Layout

  • Card([children], variant?) - outer container. Variants: default, sunk, subtle, raised.
  • CardHeader(title?, subtitle?) - title row inside a Card.
  • Stack([children], direction?, gap?) - flex layout. Direction: row|column. Gap: s|m|l.
  • Col([children], gap?) / Row([children], gap?) - explicit vertical/horizontal stacks.
  • Separator(orientation?, decorative?) - divider.

Lists

  • ListBlock([ListItem...]) - bulleted/numbered runs.
  • ListItem(text, secondary?)
  • Steps([StepsItem...], current?) - ordered phase markers.
  • StepsItem(label, description?, status?) - status: complete|active|pending.
  • Accordion([AccordionItem...]) / AccordionItem(title, body)
  • Tabs([TabItem...]) / TabItem(label, [body])

Content

  • TextContent(text, size?) - sizes: small | default | large | small-heavy | large-heavy. Supports markdown.
  • MarkDownRenderer(textMarkdown, variant?) - full GFM markdown.
  • Callout(variant, title, description, visible?) - info|warning|error|success|neutral.
  • TextCallout(variant?, title?, description?) - lighter inline callout.
  • CodeBlock(language, codeString) - syntax-highlighted code.
  • Image(alt, src?) / ImageBlock(src, alt?) / ImageGallery([{src,alt,details}]).

Tables

  • Table([Col...]) - column-oriented (each Col holds its own data array).
  • Col(label, data, type?) - type: string|number|action.

Charts (positional series shape)

  • BarChart(labels[], series[], variant?, xLabel?, yLabel?) - grouped|stacked.
  • LineChart(labels[], series[], variant?, xLabel?, yLabel?) - linear|natural|step.
  • AreaChart(labels[], series[], variant?, xLabel?, yLabel?).
  • PieChart(labels[], series[]) / RadarChart(labels[], series[]).
  • RadialChart(labels[], series[]) / ScatterChart(labels[], series[]).
  • HorizontalBarChart(labels[], series[], variant?, xLabel?, yLabel?).
  • Series(category, values[]) - one data series.

Tags / Buttons / Inputs

  • Tag(label, variant?) / TagBlock([Tag...]).
  • Button(label, action?) / Buttons([Button...]) - action is an Action expression.
  • Input(label, $binding<string>) / TextArea(label, $binding<string>).
  • Select(label, options[], $binding) / RadioGroup / CheckBoxGroup / Slider / DatePicker.
  • Form([FormControl...]) / FormControl(label, [child]).

The LLM has the full ~80 primitive vocabulary at run time. Find every registered name with:

grep -oE '"[A-Z][A-Za-z]+"' \
  ~/projects/snappy-chat/web/node_modules/@openuidev/react-ui/dist/genui-lib/openuiChatLibrary.js \
  | sort -u

Or look up the d.ts at web/node_modules/@openuidev/react-ui/dist/genui-lib/openuiChatLibrary.d.ts.


Operators

Arithmetic: +, -, *, /, % Comparison: ==, !=, >, <, >=, <= Logical: &&, || Unary: !, - Ternary: cond ? a : b


@Builtins (closed allowlist)

Functions prefixed with @. Enforced by state/lint/builtin-whitelist.ts - invented operators silently fail to render. Do not extend this list in prose - it must round-trip with the lint allowlist.

Aggregation

  • @Count(array) - number of elements.
  • @Sum(numbers[]), @Avg(numbers[]), @Min(numbers[]), @Max(numbers[]).

Array

  • @First(array), @Last(array) - pluck endpoints.
  • @Filter(array, field, op, value) - op is a comparison operator string.
  • @Sort(array, field, direction?) - direction: asc|desc.
  • @Each(array, varName, template) - iterate; bind each row to varName.

Math

  • @Round(number, decimals?), @Abs(number), @Floor(number), @Ceil(number).

Actions (mutation triggers; only inside Action expressions)

  • @Run(ref) - fire a Mutation by name.
  • @Set($var, value) / @Reset($var, $var2, ...) - write reactive state.
  • @ToAssistant(msg) - send a follow-up user message.
  • @OpenUrl(url) - navigate.

Reactive State and Bindings

Declare state with $name = default:

$days = 7
agents = Query("get_agents", {days: $days}, {agents: []}, 30)

Passing $days to a Slider or Input creates two-way binding - user input updates $days, Query re-fetches automatically. Args marked $binding<type> in primitive signatures accept a $variable reference.


Data Fetching

Query - runs on load, refetches when $variable args change:

data = Query("tool_name", {arg: value}, {default_shape: []}, 30)

Four positional args: tool name, arg object, default shape (rendered while loading), cache TTL in seconds.

Mutation - declared but only fires via @Run():

saveResult = Mutation("save_thread", {title: $title})

Trigger: Button("Save", Action([@Run(saveResult)])).

Member access (dot-pluck):

agents = Query("get_agents", {}, {agents: []}, 30)
names = agents.agents.name        # plucks .name from every row → array
count = @Count(agents.agents)

System-Prompt Patterns (canonical)

The OpenUI docs ship a getSystemPrompt(library, options) helper that produces a complete system prompt from a registered library. Our implementation does the equivalent at startup - state/config/openui-lang-prompt.txt is the auto-generated artifact.

Feature flags (set on the prompt generator):

FlagEnablesDefault
toolCallsQuery(), Mutation(), @Runtrue if tools provided
bindings$variables, @Set, @Resettrue if toolCalls enabled
editModeIncremental edit (patches only)false
inlineModeText + fenced code responsesfalse

Customizable additions: preamble (project context), additionalRules (domain-specific), toolExamples (worked Query/Mutation examples).

In snappy-chat we additionally inject:

  • The 22-shape phrasing index (server.ts § SYSTEM_PROMPT shape-guide)
  • The DATA-DRIVEN COMPOSITION rule (server.ts ~line 7919) - when the

user asks for counts/runs/scores, the LLM MUST use Query() not literal numbers.

  • Per-skill resources/ui.openui dashboards routed via

GET /skills/<slug>/openui (see head-screen/AGENTS.md).


Variants - Where the Library Choice Matters

Different ecosystems publish different openui*Library exports. Pick by target surface:

VariantWhen to lean on itLibrary export
shadcn-chatChat surface integrating with shadcn/ui design tokens - server emits abstract Lang, client maps to shadcn primitivesshadcnComponentGroups, custom createLibrary({...})
vercel-ai-chatStreaming chat with multi-step tool calling via @ai-sdk/openai and useChat - transport-agnostic, <Renderer /> stays unchangedopenuiChatLibrary
dashboardKPI cards, tables, charts driven by MCP-style tool registry; LLM composes UIs that themselves call backend toolsopenuiChatLibrary + tool registry
react-emailStatic, non-interactive content with inline-style requirement (email clients strip <style>) - 44 components in emailLibraryemailLibrary (from @openuidev/react-email)
react-nativeNative mobile via Expo - twin libraries (real renderers on device, null renderers for backend prompt generation, since CLI can't import RN in Node)openuiNativeLibrary

snappy-chat uses the vercel-ai-chat variant - openuiChatLibrary from @openuidev/react-ui, re-exported as genuiLibrary. The LLM emits Lang inside [[TOOL:Lang]]…[[/TOOL]]; the Lang entry in DISPATCH_REGISTRY runs <Renderer library={genuiLibrary}>.

For per-skill dashboards we lean toward the dashboard variant - KPI tiles, Query-driven tables, charts. See state/skills/<slug>/resources/ui.openui files (e.g. email-send-tick, slack, snappy-recover).


Worked Example - Query + $variable + @Count + Card

This is the canonical pattern the LLM should produce when asked "how many agents have run?":

$days = 7
agents = Query("get_agents", {days: $days}, {agents: []}, 30)
filtered = @Filter(agents.agents, "lastRun", "!=", null)
$total = "" + @Count(filtered)

root = Card([
  CardHeader("Agents", "Last " + $days + " days"),
  Stack([
    Card([TextContent("Active", "small"), TextContent($total, "large-heavy")], "subtle"),
    Slider("Window (days)", $days, 1, 30, 1)
  ], "row", "m")
])

What this demonstrates:

  • Query for live data (NEVER literal counts)
  • $variable for derived display values
  • @Filter + @Count to aggregate without server round-trip
  • String coercion for KPI text: "" + @Count(...)
  • Reactive: dragging the Slider updates $days → Query refetches → Count

updates


Anti-Patterns

These hard-block. Each has a lint or has cost a session of debugging.

  • No raw HTML / JSX strings. Lang is the only path. The previous

KNOWN_COMPONENTS regex catalog in server.ts is being deprecated precisely because it bypassed Lang.

  • No name=value or name: value arg syntax. Args are POSITIONAL

ONLY. Colon syntax silently breaks the parser. Burned 2026-04-29T07:42:54Z.

  • No invented @Operators. Whitelist enforced by

state/lint/builtin-whitelist.ts. Adding @Mean to prose without also adding it to the lint allowlist = silent render failure.

  • No string concatenation hacks for literal numbers. When the user

asks for a count, USE Query() + @Count(). Hardcoded "47 dispatches" is an LLM fabrication and the DATA-DRIVEN COMPOSITION rule explicitly forbids it.

  • No defining variables you don't reference. Unreferenced ≠

rendered. Always thread defined variables back through the parent's children array.

  • No per-shape .tsx file as the default path for new UI. The

legacy recipe (per-shape file + DISPATCH_REGISTRY entry + server.ts matcher + welcome chip) is dead. Default = describe what you want in a prompt, let the LLM compose primitives.

  • No getSystemPrompt() direct calls in a per-request hot path.

Generated once at startup and cached in state/config/openui-lang-prompt.txt.


Where it Lives in snappy-chat

Three files own the Lang surface end-to-end:

  1. Server prompt - state/bin/head-screen/server.ts
  • LANG_PROMPT_PATH constant points at

state/config/openui-lang-prompt.txt

  • Loaded once at boot, injected into the system prompt for every

/dispatch/chat request

  • DATA-DRIVEN COMPOSITION rule + 22-shape phrasing index live in

the same prompt block (~line 7900)

  1. Client renderer - web/src/dispatch-card.tsx
  • langDispatchEntry() wraps <Renderer library={genuiLibrary}>
  • Lang entry in DISPATCH_REGISTRY is the only path for primitive

compositions

  • Five "promoted canned shapes" (DispatchCard, AgentDetail,

ProgressList, PhaseDisclosure, HTMLPreview) also route through Lang

  • Threaded shape memory + patch merging in lang-history.ts
  1. Library - web/src/genui-library.tsx
  • genuiLibrary = openuiChatLibrary (from @openuidev/react-ui)
  • 5 promoted shapes registered alongside via defineComponent
  • Three-line re-export pattern for promoted-shape files (TS

isolatedModules constraint): export { View, Component }, export type { Props }, import { Component } (local binding for the components array)

Per-skill dashboards: state/skills/<slug>/resources/ui.openui - plain Lang text, served via GET /skills/<slug>/openui.


Resource Files

This skill is prose-only; companion files live in this folder:

  • AGENTS.md - the per-turn loader. Critical Rules + commands.
  • prompt-fragment.md - the compact system-prompt injection (≤500 tokens)

fired when openui-lang intent is detected.


  • crayon-sdk - the broader Crayon/C1/OpenUI ecosystem

reference. OpenUI Lang is the language layer; crayon-sdk covers the outer chat shell, SSE wire format, thread persistence.

  • openui-mcp - Context7 MCP for live OpenUI docs lookup

during work.

prompt and runs the DISPATCH_REGISTRY Lang entry.

authoring guide. Read for historical context only; do NOT extend.


Skill Status: ACTIVE Eval: auto-shape (frontmatter + body presence; failure mode is silent: agent doesn't load Lang model and writes per-shape .tsx files) Reference Implementation: snappy-chat (web/src/dispatch-card.tsx Lang entry + state/config/openui-lang-prompt.txt)

AGENTS.md- what the AI loads when this skill comes up

openui-lang - loader

Per-turn rules for openui-lang. Full reference: state/skills/openui-lang/SKILL.md. Prose-only skill - no lib, no sidecar; eval=auto-shape. Critical rules are load-bearing; do not skip.

Critical Rules

  1. OpenUI Lang IS the composition language. Never use raw HTML strings, JSX strings, or template-string concatenation when generative UI is the target. The LLM emits Lang text inside [[TOOL:Lang]]…[[/TOOL]] markers; the Lang entry in web/src/dispatch-card.tsx DISPATCH_REGISTRY is the ONLY path for primitive compositions. Verified by state/lint/dispatch-contract.ts.
  1. Args are POSITIONAL ONLY. Card([children], "subtle") works; Card(items: [...], variant: "subtle") and Card(items=[...]) silently break the parser. Order matters; names do not. The prop order in defineComponent({props: z.object({...})}) IS the call order. Burned 2026-04-29T07:42:54Z.
  1. Closed @builtin allowlist. @Count, @Sum, @Avg, @Min, @Max, @Round, @Abs, @Floor, @Ceil, @Filter, @Sort, @First, @Last, @Each for data; @Run, @Set, @Reset, @ToAssistant, @OpenUrl for actions. Verified by state/lint/builtin-whitelist.ts. Inventing @Mean, @GroupBy, etc. = silent render failure. Don't extend the list in prose without round-tripping the lint allowlist.
  1. Query() for live data, NEVER raw literal numbers. When the user asks for counts / runs / scores / aggregates, the model MUST emit Query("tool_name", {args}, {default_shape}, cache_seconds) plus @Count / @Filter / @Sum - not "47 dispatches" hardcoded. This is the DATA-DRIVEN COMPOSITION rule in server.ts ~line 7919. Hallucinated numbers are the canonical failure mode.
  1. Three statement types. (a) Component: name = Component(args); (b) State: $name = default; (c) Data: name = Query(...) / Mutation(...). Root rule: every program defines root = ... (entry point). Forward references work - define order doesn't matter, but every variable except root MUST be referenced somewhere or it's silently dropped.
  1. $variable for derived/reactive values. Two-way binding works for inputs marked $binding<type> in signatures. Pattern: $days = 7; agents = Query("get_agents", {days: $days}, ...); Slider("Window", $days, 1, 30, 1) - slider drag updates $days, Query refetches automatically. Use $total = "" + @Count(...) to coerce numbers to strings for KPI text.
  1. Member access (dot-pluck) replaces map(). agents.agents.name plucks .name from every row in the agents.agents array. Combine with @Filter: @Filter(agents.agents, "lastRun", "!=", null). There is no inline map() - dot-pluck is the only path for column projection.
  1. Default UI path = primitives via Lang, NOT per-shape .tsx files. The legacy recipe (per-shape file + DISPATCH_REGISTRY entry + server.ts regex + welcome chip) is being deprecated. New affordance = describe in a prompt, let the LLM compose. Pre-canned shape only justified for high-frequency UX with a fixed schema (the 5 promoted: DispatchCard, AgentDetail, ProgressList, PhaseDisclosure, HTMLPreview). All five route THROUGH Lang.
  1. Variant pointers. Chat surface integration → vercel-ai-chat variant (snappy-chat uses this - openuiChatLibrary). KPI tiles + tables + charts → dashboard variant. Email rendering with inline-style constraint → react-email (44-component emailLibrary). Mobile native → react-native twin-library pattern. shadcn design system → custom createLibrary({componentGroups: shadcnComponentGroups}).
  1. Snappy-chat surface = three files. Server prompt: state/bin/head-screen/server.ts (loads state/config/openui-lang-prompt.txt at boot, ~line 8250). Client renderer: web/src/dispatch-card.tsx (langDispatchEntry() + Lang registry entry). Library: web/src/genui-library.tsx (genuiLibrary = openuiChatLibrary). Don't add new entries elsewhere - these three are the boundary.
  1. System prompt is auto-generated, cached at boot. state/config/openui-lang-prompt.txt is regenerated from the registered library; do NOT hand-edit. To add a new primitive, register it via defineComponent in genui-library.tsx, then regen the prompt file. Hot-edit attempts will be overwritten next boot.
  1. Skill-owned OpenUI resources live at state/skills/<slug>/resources/*.openui. Plain Lang text (NOT JSON, NOT JSX). ui.openui is the default skill dashboard. Named files such as schedule.openui are saved right-canvas surfaces and must start with // surface, // intents, and // response metadata so the saved-surface registry can route natural requests without asking the model to recreate the UI. Examples: email-send-tick/resources/ui.openui, slack/resources/ui.openui, linkedin-post/resources/schedule.openui.
  1. getSystemPrompt(library, options) flags. toolCalls enables Query()/Mutation()/@Run. bindings enables $variables/@Set/@Reset (auto-true if toolCalls). editMode for incremental patches. inlineMode for text + fenced code responses. Customize via preamble, additionalRules, toolExamples. Don't pass these per-request - generate once at startup.
  1. Lang is positional everywhere - including server-side translators. When server.ts builds Lang text from a payload (e.g. the legacy emitTriple path), positional args MUST be preserved. A translator that emits Comp(name="x", value=1) at the server breaks identically to a model that emits it.
  1. Threaded shape memory + patch merging. web/src/lang-history.ts keeps per-thread Lang shape memory; mergeStatements() in dispatch-card.tsx blends incremental patches before render. The LLM may emit a partial patch (refining one statement) instead of re-emitting the full tree. Don't assume each [[TOOL:Lang]] body is self-contained - merge against history.

Commands

operationcommand
main referencestate/skills/openui-lang/SKILL.md
canonical prompt fragmentstate/skills/openui-lang/prompt-fragment.md
auto-generated prompt (cached)state/config/openui-lang-prompt.txt
server-side load pointstate/bin/head-screen/server.ts (LANG_PROMPT_PATH)
client renderer entryweb/src/dispatch-card.tsx (langDispatchEntry, DISPATCH_REGISTRY.Lang)
library exportweb/src/genui-library.tsx (genuiLibrary = openuiChatLibrary)
primitive vocabularyweb/node_modules/@openuidev/react-ui/dist/genui-lib/openuiChatLibrary.d.ts
list all registered names`grep -oE '"[A-Z][A-Za-z]+"' web/node_modules/@openuidev/react-ui/dist/genui-lib/openuiChatLibrary.js \sort -u`
default skill resourcestate/skills/<slug>/resources/ui.openui
named saved surfacestate/skills/<slug>/resources/<name>.openui with // surface, // intents, // response comments
dashboard endpointGET /skills/<slug>/openui
dispatch endpointPOST /dispatch/chat (system prompt includes Lang fragment)
verify shape emit`curl -s -X POST 127.0.0.1:3147/dispatch/chat -d '{"intent":"show me a kpi card"}' \jq .toolCalls`
companion docs~/projects/snappy-chat/CLAUDE.md § "Generative-UI shapes (primitives pivot)"
eval logstate/log/evals.ndjson (skill: openui-lang, eval_mode: auto-shape)
writebackstate/log/loader-feedback.log

Statement Cheat-Sheet

NeedLang
Container with titleCard([CardHeader("Title"), child])
Vertical stackStack([a, b, c], "column", "m")
Horizontal rowStack([a, b, c], "row", "m")
KPI tileCard([TextContent("Label", "small"), TextContent($val, "large-heavy")], "subtle")
Live countdata = Query("tool", {}, {rows: []}, 30); $n = "" + @Count(data.rows)
Filtered count$active = "" + @Count(@Filter(data.rows, "status", "==", "active"))
Two-way slider$days = 7; Slider("Window", $days, 1, 30, 1)
Iterated cards@Each(data.rows, "row", Card([TextContent(row.name)]))
Action buttonButton("Save", Action([@Run(saveResult)]))
Tab panelTabs([TabItem("One", [body1]), TabItem("Two", [body2])])
Steps trackerSteps([StepsItem("Plan", null, "complete"), StepsItem("Build", null, "active")], 1)
Bar chartBarChart(["A", "B", "C"], [Series("count", [3, 5, 2])])

Variant Pointer

Use caseVariantLibrary
snappy-chat (this repo)vercel-ai-chatopenuiChatLibrary
Skill-owned resourcesdashboard / saved surfaceopenuiChatLibrary + Query/Mutation-driven data
Static HTML emailreact-emailemailLibrary (44 components, inline styles)
Native mobilereact-nativetwin libraries (real + null renderers)
shadcn design systemshadcn-chatcustom createLibrary({componentGroups: shadcnComponentGroups})

Known Pitfalls

  • Args POSITIONAL. Colon / equals syntax silently breaks (rule 2).
  • Unreferenced variables silently dropped. Always thread back through the parent's children array (rule 5).
  • Invented @Operators silently fail. Stick to the closed allowlist (rule 3).
  • Hardcoded numbers = LLM fabrication. Query() + @Count() for live values (rule 4).
  • System prompt is cached at boot. Hand-edits to openui-lang-prompt.txt get clobbered at next regen (rule 11).
  • Patch vs. full re-emit. LLM may emit a patch; merge against thread history rather than assuming self-contained (rule 15).
  • Per-shape .tsx files are the rare path. Don't reach for them when Lang primitives plus a saved resources/*.openui surface suffice (rules 8 and 12).

Self-Test

An agent reading this should correctly:

  1. [ ] Know prose-only, eval=auto-shape; "RUN THIS" briefs → [SKIPPED]
  2. [ ] Use POSITIONAL args only; never name=value / name: value
  3. [ ] Stick to the closed @builtin allowlist; no invented operators
  4. [ ] Use Query() for live data, never literal counts
  5. [ ] Define root = ...; reference every variable; forward-ref OK
  6. [ ] Use $variables for reactive state; "" + @Count(...) for string KPIs
  7. [ ] Pluck columns via data.rows.field, not inline map
  8. [ ] Default new UI = describe in prompt, NOT a per-shape .tsx file
  9. [ ] Pick variant by surface (vercel-ai for chat, dashboard for KPIs, etc.)
  10. [ ] Know the three-file boundary in snappy-chat (server prompt + dispatch-card + genui-library)
  11. [ ] Don't hand-edit openui-lang-prompt.txt (auto-regen)
  12. [ ] Skill-owned resources live at state/skills/<slug>/resources/*.openui; named surfaces carry // surface, // intents, and // response
  13. [ ] Use getSystemPrompt(library, options) flags at startup, not per-request
  14. [ ] Server-side translators preserve positional args
  15. [ ] Merge patch emissions against thread history via mergeStatements()

<!-- kernel-ok: historical attribution for the PID-loader pattern. -->

Self-correcting loader (PID feedback)

This is the rule that makes loaders learn. Ported from snappy-kernel, where it demonstrably worked: the snappy-dom-cartographer skill mapped Skool admin UI once, every later ship-agent inherited the full map for free.

Before you finish, do two things:

1. Fix gaps (P - proportional). MANDATORY. If this loader didn't cover your case - if you had to read another file, run an undocumented command, work around a wrong selector, or discover a quirk - you MUST attempt an Edit to this AGENTS.md before you log.

  • Default action: edit this file. Surgical. Add the missing line, table

row, or code example where it belongs. No restructuring.

  • Only LOGGED is allowed when: the fix needs >10 lines, spans multiple

files, or requires a structural rewrite. In that case the state/regen/drain.sh queue picks it up asynchronously.

  • "I didn't have time" / "it's minor" / "the next agent will figure it out"

are NOT valid reasons. Minor gaps compound - that's the whole point of this loop.

  • The goal: the next agent never has to leave the loader.

2. Log the result.

echo "[$(date -u +%FT%TZ)] openui-lang: <what was missing or fixed> [FIXED|LOGGED] action_kind=<kind>" >> state/log/loader-feedback.log
  • The class token between [ts] and : MUST be the literal folder slug

(openui-lang) - producer slug, writeback class, and grade class must be equal so state/lib/controller-tune.ts can pair the brief.

  • FIXED = you patched this loader inline (P-fix).
  • LOGGED = too large for inline; the PostToolUse enqueue + Stop-hook drain

will rewrite the loader from scratch on next session-end.

  • action_kind: shape-ok | skill-ran | loader-rewritten | pattern-elevated

Do not skip this. Every agent run must leave the system better than it found it. The loader is the setpoint; you are the sensor; the gap is the error signal; closing the gap is the correction.

api.ts- the code it can call

⚠ no api.ts - this skill has no typed action surface

scripts- helper scripts it can run

prose-only skill - 7 inline code blocks live in SKILL.md above (no state/bin/ sidecar yet).

how we check it- the checks, plus the last 10 runs

rubric auto-shape no rubric declared
recent no runs actor/auditor: unverifiable
deps none declared

no recent runs logged - the eval contract is declared but nothing has been graded yet