Introducing @std/dynamic: a JSON format for composable dashboards
@std/dynamic is the composable dashboard format powering genui.sh. Here's the shape, the primitives, and why it's designed the way it is.
Most "render from JSON" APIs give you two choices. You either get a rigid template you fill in by slot — the PDFMonkey / DocRaptor / APITemplate.io approach — or raw Markdown / HTML, which is expressive but produces documents, not dashboards.
There's a real middle ground, and it's what I've been building for genui.sh. I'm calling it @std/dynamic, and it's the format I use internally to compose every dashboard the API renders. The landing page has a live example — a real dashboard generated from a single POST, rendered by the same code any user gets.
This post is a quick tour of the format: the shape, the primitives, and the reasoning behind the design choices.
The pitch
@std/dynamic lets you describe a dashboard as a flat dictionary of elements, keyed by ID, each with a type, a props object, and an optional children array pointing at other keys. One of the elements is designated as the root.
That's it. The whole format.
{
"template": "@std/dynamic",
"title": "Weekly sales",
"content": {
"root": "page",
"elements": {
"page": {
"type": "Stack",
"props": { "direction": "vertical", "gap": "md" },
"children": ["heading", "metrics"]
},
"heading": {
"type": "Heading",
"props": { "level": "h1", "text": "Weekly sales" }
},
"metrics": {
"type": "Grid",
"props": { "columns": 2, "gap": "md" },
"children": ["revenue", "orders"]
},
"revenue": {
"type": "Metric",
"props": {
"label": "Revenue",
"value": "$142,840",
"trend": "up",
"trendValue": "+12%"
}
},
"orders": {
"type": "Metric",
"props": {
"label": "Orders",
"value": "1,248",
"trend": "up",
"trendValue": "+8%"
}
}
}
}
}
POST that to /v1/artifacts/share and you get back a hosted URL rendering the dashboard. Primitives compose recursively — a Grid can contain Cards which contain Charts, which is how you build multi-layer layouts from a single POST.
The primitives
There are ~20 components in the current catalog, grouped by purpose:
Layout — Card, Grid, Stack, Tabs
Content — Heading, Text, Markdown, Alert, Divider, Link, List
Data — Chart (bar, line, area, pie, donut), Table (sortable, searchable, exportable), Metric (KPI card with trend), Badge, Progress, Avatar
Interactive — Input, Button, FormField, Accordion
Every component has a Zod schema for its props, and validation happens server-side before anything renders. Invalid trees fail fast with a 400 that tells you which element was malformed. This matters for the next section.
Why flat-dict-plus-root, not nested JSON
The obvious way to describe a UI tree is nested:
{
"type": "Stack",
"children": [
{ "type": "Heading", "props": { ... } },
{ "type": "Grid", "children": [
{ "type": "Metric", "props": { ... } },
{ "type": "Metric", "props": { ... } }
]}
]
}
This is what React does internally. It's fine for hand-authoring, but it has three real problems once you're generating trees programmatically:
- Emitting deeply-nested JSON recursively is annoying in almost every language, especially when you're building a tree from database rows.
- LLMs are bad at deeply nested JSON. The error rate on a 5-level nested structure is noticeably higher than on a flat dictionary with sibling references.
- Diffing and updating a flat dict (keyed by ID) is trivial; diffing a nested tree is not.
The flat-dict shape solves all three. You emit elements in any order, reference them by key, and the renderer walks the graph starting from root. It also means you can generate a tree by appending to a dictionary, which matches how most ORM-to-UI code naturally works.
Why this is good for LLM tool use
The format was designed from day one to be emitted by a language model. Three properties make it work well:
1. The full grammar fits in a small system prompt. You can describe the 20 components, their prop types, and the tree structure in ~800 tokens. That's small enough to include in every tool-use call without eating into your context budget.
2. Strict Zod validation on the server. If an LLM emits a malformed tree — wrong prop name, missing required field, nested component where hasChildren: false — the API returns a 400 with a specific error path. The agent can retry with the correction. No silent rendering of broken dashboards.
3. No CSS, no custom rendering logic. The LLM doesn't have to reason about layout, color, spacing, or responsive breakpoints. It picks from a fixed set of primitives with documented behavior. This is exactly the constraint that makes LLM output reliable — a narrow, strongly-typed action space.
A Claude or GPT tool call can now return a dashboard to the user instead of a paragraph of JSON. The tool-use result is a URL. The user clicks it. The user is not staring at {"revenue": 142840, "orders": 1248} wondering what to do with it.
What's not in the format (yet)
- No custom CSS or arbitrary HTML. On purpose. The whole point is that the renderer owns the visual language so the output stays consistent.
- No user-defined components. Also on purpose, at least for now. A community component catalog might make sense later, but the bar is high — every new primitive makes the system prompt bigger and the LLM's reliability lower.
- No client-side state or routing.
InputandButtonrender with real DOM, but they don't have submit handlers wired to user code. You'd use the format to render an interactive card that POSTs back to your API, not to build a full app. - No theming knobs. The renderer uses the genui.sh design tokens. If you want your own theme, white-labeling is on the roadmap for the Pro tier.
These are real limits. If you need user-defined components or custom CSS, @std/dynamic is the wrong tool and you should reach for a full frontend framework. If you're emitting dashboards from a pipeline, a workflow, or an agent, the constraints are features.
Try it
The playground at genui.sh/dashboard/playground has a live JSON editor on one side and a rendered preview on the other. You can switch the template dropdown to @std/dynamic and start composing — every edit re-renders in real time.
There's also a system-prompt generator at genui.sh/dashboard/templates that produces the exact @std/dynamic grammar for your chosen component subset, ready to paste into an LLM's system prompt or a tool-use schema.
Grab a free key (50 artifacts/month, no credit card) and POST your first tree at genui.sh. Feedback welcome at support@genui.sh, especially on the primitive catalog — which components you reach for first, and which ones you expected and didn't find.