Skip to Content
GuidesProteus

Proteus

Proteus is a JSON-based UI specification for building interactive tool interfaces. You define your UI as a JSON document, and the Proteus renderer (@optiaxiom/proteus) turns it into interactive Axiom components.

Document structure

A Proteus document is a JSON object with $type: "Document". At minimum it needs a body. Every element has a $type that identifies the component. The children property holds nested content: a string, number, another element, or an array of elements.

Welcome to your dashboard

Proteus turns a JSON description into a fully interactive Axiom UI.

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer element={{ $type: "Document", body: [ { $type: "Text", children: "Proteus turns a JSON description into a fully interactive Axiom UI.", }, ], title: "Welcome to your dashboard", }} /> </Box> ); }

Documents also support title, subtitle, appName, appIcon, and actions (buttons rendered at the bottom).

Opal

Create your test plan

Select how you'd like to define the page or experience.

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; import { useState } from "react"; export function App() { const [data, setData] = useState<Record<string, unknown>>({}); return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={data} element={{ $type: "Document", actions: [ { $type: "Action", appearance: "primary-opal", children: "Create Test Plan", }, ], appName: "Opal", blocking: true, body: [ { $type: "Field", children: { $type: "Input", name: "url", placeholder: "Add a URL", }, label: "URL", }, { $type: "Field", children: { $type: "Textarea", name: "test_idea", placeholder: "e.g., Add quantity badges to product thumbnails to show how many of each item they're buying", }, label: "Test Idea", }, ], subtitle: "Select how you'd like to define the page or experience.", title: "Create your test plan", }} onDataChange={setData} /> </Box> ); }

Components

Use Group as a flexbox container to arrange child elements. It supports flexDirection, gap, alignItems, and justifyContent. Card, CardHeader, and CardLink provide card-based layouts. Separator adds a visual divider.

Add quantity badges to product thumbnails

TSK-8526

D-Congress 2026 - Digital screen content

TSK-9102

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer element={{ $type: "Document", body: { $type: "Group", border: "1", borderColor: "border.tertiary", children: [ { $type: "Group", children: [ { $type: "Text", children: "Add quantity badges to product thumbnails", fontWeight: "600", }, { $type: "Text", children: "TSK-8526", color: "fg.tertiary", fontSize: "sm", }, ], flexDirection: "column", gap: "4", p: "12", }, { $type: "Separator", borderColor: "border.tertiary" }, { $type: "Group", children: [ { $type: "Text", children: "D-Congress 2026 - Digital screen content", fontWeight: "600", }, { $type: "Text", children: "TSK-9102", color: "fg.tertiary", fontSize: "sm", }, ], flexDirection: "column", gap: "4", p: "12", }, ], flexDirection: "column", rounded: "md", }, }} /> </Box> ); }

Heading renders section headings, Text renders inline or block text, and Link renders a hyperlink with an href prop.

Key Insight

Initial performance metrics reveal a 12% drop in user retention post-update. Immediate deep-dive into Android Scrum Project's V2.2 onboarding flow is critical to reverse the trend and secure Q3 engagement goals.

View task TSK-98
"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer element={{ $type: "Document", body: [ { $type: "Heading", children: "Key Insight", level: "4" }, { $type: "Text", children: "Initial performance metrics reveal a 12% drop in user retention post-update. Immediate deep-dive into Android Scrum Project's V2.2 onboarding flow is critical to reverse the trend and secure Q3 engagement goals.", color: "fg.secondary", fontSize: "sm", }, { $type: "Link", children: "View task TSK-98", href: "https://example.com/task/tsk-98", }, ], }} /> </Box> ); }

DataTable renders structured data with columns, sorting, and pagination. Chart renders bar or line charts. Image and ImageCarousel display images. Avatar, Badge, and Time handle user avatars, status tags, and formatted timestamps.

Salesforce CRM

Q4 2024 Sales Performance

October 1 - December 31, 2025

Rank
Representative
Deals Closed
Revenue
1Sarah Chen47$2.8M
2Michael Wong42$2.5M
3Jenn Taylor39$2.3M
4David Smith35$2.1M
5Emily Johnson33$2.0M
"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer element={{ $type: "Document", appName: "Salesforce CRM", body: { $type: "DataTable", columns: [ { accessorKey: "rank", header: "Rank", size: 80 }, { accessorKey: "representative", header: "Representative" }, { accessorKey: "deals", header: "Deals Closed", size: 120 }, { accessorKey: "revenue", header: "Revenue", size: 120 }, ], data: [ { deals: "47", rank: "1", representative: "Sarah Chen", revenue: "$2.8M", }, { deals: "42", rank: "2", representative: "Michael Wong", revenue: "$2.5M", }, { deals: "39", rank: "3", representative: "Jenn Taylor", revenue: "$2.3M", }, { deals: "35", rank: "4", representative: "David Smith", revenue: "$2.1M", }, { deals: "33", rank: "5", representative: "Emily Johnson", revenue: "$2.0M", }, ], }, subtitle: "October 1 - December 31, 2025", title: "Q4 2024 Sales Performance", }} /> </Box> ); }

Icon renders a named icon from the icons map passed to the renderer. If name is instead an http(s):// or data: URL not present in the map, it falls back to rendering an <img> with that source — so you can drop in a remote or inline SVG without registering it. Registered icons always take precedence.

By default a DataTable column renders its value as text. Give a column a cell instead to render a Proteus node for that column. Inside a cell, read the current row with DataTableRow{ $type: "DataTableRow", path: "status" } reads the row’s status field, and omitting path gives the whole row object (the same way MapIndex exposes the current index inside a Map). This lets a table column render a Badge, Icon, link, or any composition of elements, with Show picking what to render per row. A change-history / diff view, for example, is just a DataTable whose “Change” column is a badge whose intent depends on the row:

Flag Configuration Changes

Field
Old
New
Change
StatusPausedRunningmodified
DescriptionUpdated experiment for Q3 launchadded
Legacy Flagenabledremoved
"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="lg" w="full"> <ProteusDocumentRenderer data={{ changes: [ { field: "Status", newValue: "Running", oldValue: "Paused", type: "modified", }, { field: "Description", newValue: "Updated experiment for Q3 launch", oldValue: "", type: "added", }, { field: "Legacy Flag", newValue: "", oldValue: "enabled", type: "removed", }, ], }} element={{ $type: "Document", body: { $type: "DataTable", columns: [ { accessorKey: "field", header: "Field", size: 150 }, { accessorKey: "oldValue", header: "Old", size: 110 }, { accessorKey: "newValue", header: "New", size: 170 }, { accessorKey: "type", // `cell` is a regular Proteus node. `DataTableRow` reads the // current row inside it, and a Show/else chain picks the badge // intent per row. cell: { $type: "Badge", children: { $type: "DataTableRow", path: "type" }, intent: { $type: "Show", children: "success", else: { $type: "Show", children: "danger", else: "warning", when: { "==": [ { $type: "DataTableRow", path: "type" }, "removed", ], }, }, when: { "==": [{ $type: "DataTableRow", path: "type" }, "added"], }, }, }, header: "Change", size: 120, }, ], data: { $type: "Value", path: "/changes" }, }, title: "Flag Configuration Changes", }} /> </Box> ); }

Give a DataTable a rowSelection prop — a JSON pointer such as "/selection" — to add a select-all header checkbox and a per-row checkbox column. As rows are toggled, the selected row objects are written as an array to that pointer, so an action can read them back with Value:

{ "$type": "DataTable", "rowSelection": "/selection", "columns": [{ "accessorKey": "name", "header": "Task Name" }], "data": { "$type": "Value", "path": "/tasks" } }

An action can then read the selection via { "$type": "Value", "path": "/selection" } — for example to send the selected rows to an interaction, or to disable a submit button until at least one row is selected.

Wrap inputs with Field to add a label. Available inputs include Input, Textarea, Select (composed from SelectTrigger and SelectContent), Switch, Checkbox, Range, and Question (a multi-question dynamic form). Switch and Checkbox are boolean controls bound to a data path via name.

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; import { useState } from "react"; export function App() { const [data, setData] = useState<Record<string, unknown>>({}); return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={data} element={{ $type: "Document", body: [ { $type: "Field", children: { $type: "Select", children: [ { $type: "SelectTrigger", w: "full" }, { $type: "SelectContent" }, ], name: "target_by", options: [ { label: "URL", value: "url" }, { label: "Saved Pages", value: "page" }, ], }, label: "Target by", }, { $type: "Field", children: { $type: "Input", name: "url", placeholder: "Add a URL", }, label: "URL", }, { $type: "Field", children: { $type: "Textarea", name: "test_idea", placeholder: "e.g., Add quantity badges to product thumbnails to show how many of each item they're buying", }, label: "Test Idea", }, { $type: "Field", children: { $type: "Switch", name: "include_metadata" }, label: "Include Metadata", }, ], }} onDataChange={setData} /> </Box> ); }

Action renders a button with an onClick handler (see Interactions below). Use appearance (primary, danger, subtle, etc.) to style the button.

Content Marketing Platform

Version 2.2 Performance Optimization

Android Scrum Campaign / TSK-98

Initial performance metrics reveal a 12% drop in user retention post-update.

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer element={{ $type: "Document", actions: [ { $type: "Action", children: "Comment" }, { $type: "Action", appearance: "primary", children: "View task", }, ], appName: "Content Marketing Platform", body: [ { $type: "Text", children: "Initial performance metrics reveal a 12% drop in user retention post-update.", }, ], subtitle: "Android Scrum Campaign / TSK-98", title: "Version 2.2 Performance Optimization", }} /> </Box> ); }

All components accept Axiom styling props for spacing, colors, layout, and typography:

TOTAL REVENUE

$204M

+12% vs last quarter

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer element={{ $type: "Document", body: { $type: "Group", border: "1", borderColor: "border.tertiary", children: [ { $type: "Text", children: "TOTAL REVENUE", color: "fg.tertiary", fontSize: "xs", textTransform: "uppercase", }, { $type: "Text", children: "$204M", fontSize: "2xl", fontWeight: "700", }, { $type: "Text", children: "+12% vs last quarter", color: "fg.success.strong", fontSize: "sm", }, ], flexDirection: "column", gap: "4", p: "12", rounded: "xl", }, }} /> </Box> ); }

Data binding

Reads a field from the tool response using a JSON Pointer path. Paths starting with / are absolute (from the root of the tool response). Paths without a leading / are relative (resolved from the current context, like inside a Map).

TOTAL REVENUE

$204M

+12% vs last quarter

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={{ label: "TOTAL REVENUE", trend: "+12% vs last quarter", trendColor: "fg.success.strong", value: "$204M", }} element={{ $type: "Document", body: { $type: "Group", border: "1", borderColor: "border.tertiary", children: [ { $type: "Text", children: { $type: "Value", path: "label" }, color: "fg.tertiary", fontSize: "xs", textTransform: "uppercase", }, { $type: "Text", children: { $type: "Value", path: "value" }, fontSize: "2xl", fontWeight: "700", }, { $type: "Text", children: { $type: "Value", path: "trend" }, color: { $type: "Value", path: "trendColor" }, fontSize: "sm", }, ], flex: "1", flexDirection: "column", gap: "4", p: "12", rounded: "xl", }, }} /> </Box> ); }

Iterates over an array, rendering children once per item. Inside a Map, relative paths resolve against the current item:

TOTAL REVENUE

$204M

+12% vs last quarter

TOTAL CLOSE RATE

83%

-3% vs last quarter

TOTAL DEALS CLOSED

230

+18 vs last quarter

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={{ metrics: [ { label: "TOTAL REVENUE", trend: "+12% vs last quarter", trendColor: "fg.success.strong", value: "$204M", }, { label: "TOTAL CLOSE RATE", trend: "-3% vs last quarter", trendColor: "fg.error.strong", value: "83%", }, { label: "TOTAL DEALS CLOSED", trend: "+18 vs last quarter", trendColor: "fg.success.strong", value: "230", }, ], }} element={{ $type: "Document", body: { $type: "Group", children: { $type: "Map", children: { $type: "Group", border: "1", borderColor: "border.tertiary", children: [ { $type: "Text", children: { $type: "Value", path: "label" }, color: "fg.tertiary", fontSize: "xs", textTransform: "uppercase", }, { $type: "Text", children: { $type: "Value", path: "value" }, fontSize: "2xl", fontWeight: "700", }, { $type: "Text", children: { $type: "Value", path: "trend" }, color: { $type: "Value", path: "trendColor" }, fontSize: "sm", }, ], flex: "1", flexDirection: "column", gap: "4", p: "12", rounded: "xl", }, path: "/metrics", }, gap: "16", }, }} /> </Box> ); }

MapIndex gives you the current iteration index inside a Map. Concat takes a children array and joins each resolved value into a single string. Zip takes a sources object — each key becomes a property name, and the values (arrays or expressions resolving to arrays) are zipped row-wise into an array of objects, useful as Chart.data or DataTable.data.

UTM Creation

Click the button to see how Concat formats the parameter values into a single message.

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box, toaster } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={{ parameters: [ { name: "Campaign Name", value: "spring-sale-2024" }, { name: "Landing Page URL", value: "https://example.com/landing" }, { name: "Channel", value: null }, ], }} element={{ $type: "Document", actions: [ { $type: "Action", appearance: "primary-opal", children: "Run agent", onClick: { message: { $type: "Map", children: { $type: "Concat", children: [ { $type: "Value", path: "name" }, ": ", { $type: "Show", children: "[Not specified]", when: { "!": { $type: "Value", path: "value" } }, }, { $type: "Show", children: { $type: "Value", path: "value" }, when: { "!!": { $type: "Value", path: "value" } }, }, ], }, path: "/parameters", separator: "\n", }, }, }, ], body: [ { $type: "Text", children: "Click the button to see how Concat formats the parameter values into a single message.", }, ], title: "UTM Creation", }} onMessage={(msg) => { toaster.create(typeof msg === "string" ? msg : JSON.stringify(msg)); }} /> </Box> ); }

This template renders a list of search results dynamically from a tool response:

Content Marketing Platform

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={{ table_data: [ { cmp_url: "https://example.com/task/id-123", owner: "John Doe", reference: "TSK-8526", status: "Overdue", title: "Add quantity badges to product thumbnails", }, { cmp_url: "https://example.com/task/id-345", owner: "Jane Doe", reference: "TSK-9102", status: "In Progress", title: "D-Congress 2026 - Digital screen content", }, ], total_results: 2, }} element={{ $type: "Document", appName: "Content Marketing Platform", body: { $type: "Map", children: { $type: "Card", children: { $type: "CardHeader", children: { $type: "CardLink", children: { $type: "Value", path: "title" }, href: { $type: "Value", path: "cmp_url" }, }, description: { $type: "Value", path: "reference" }, }, }, path: "/table_data", }, title: [{ $type: "Value", path: "/total_results" }, " results found"], }} /> </Box> ); }

Conditional rendering

Use Show to render content only when a condition is met. Add an else to render a fallback when the condition is false — this works for element children and, since Show also resolves as a value, for props too. Chain nested Show/else to express multi-way choices (e.g. mapping a value to one of several outputs, like a Badge intent):

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; import { useState } from "react"; export function App() { const [data, setData] = useState<Record<string, unknown>>({ target_by: "url", }); return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={data} element={{ $type: "Document", body: [ { $type: "Field", children: { $type: "Select", children: [ { $type: "SelectTrigger", w: "full" }, { $type: "SelectContent" }, ], name: "target_by", options: [ { label: "URL", value: "url" }, { label: "Saved Pages", value: "page" }, ], }, label: "Target by", }, { // Show renders `children` when the condition holds, otherwise the // `else` branch — here a whole different field. $type: "Show", children: { $type: "Field", children: { $type: "Input", name: "url", placeholder: "Add a URL", }, label: "URL", }, else: { $type: "Field", children: { $type: "Select", children: [ { $type: "SelectTrigger", w: "full" }, { $type: "SelectContent" }, ], name: "saved_page", options: [ { label: "Home page", value: "home" }, { label: "Marketplace", value: "marketplace" }, { label: "Product Details", value: "product_details" }, ], }, label: "Saved Page", }, when: { "==": [{ $type: "Value", path: "/target_by" }, "url"], }, }, ], }} onDataChange={setData} /> </Box> ); }

Supported operators: ==, !=, <, <=, >, >=, !! (truthy), ! (falsy). Combine conditions with and and or:

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; import { useState } from "react"; export function App() { const [data, setData] = useState<Record<string, unknown>>({ target_by: "url", url: "https://example.com", }); return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={data} element={{ $type: "Document", body: [ { $type: "Field", children: { $type: "Select", children: [ { $type: "SelectTrigger", w: "full" }, { $type: "SelectContent" }, ], name: "target_by", options: [ { label: "URL", value: "url" }, { label: "Saved Pages", value: "page" }, ], }, label: "Target by", }, { $type: "Show", children: { $type: "Field", children: { $type: "Textarea", name: "test_idea", placeholder: "e.g., Add quantity badges to product thumbnails", }, label: "Test Idea", }, when: { or: [ { and: [ { "==": [{ $type: "Value", path: "/target_by" }, "url"], }, { "!!": { $type: "Value", path: "/url" } }, ], }, { and: [ { "==": [{ $type: "Value", path: "/target_by" }, "page"], }, { "!!": { $type: "Value", path: "/saved_page" } }, ], }, ], }, }, ], }} onDataChange={setData} /> </Box> ); }

Interactions

Sends the named interaction back to your server, where it can be handled and responded to. Because the handler owns the document’s data, the response can meaningfully change what the card renders — here approving or requesting changes updates status, and Show/Value swap the actions for a resolved state:

Content Marketing Platform

Version 2.2 Performance Optimization

Android Scrum Campaign / TSK-98

Initial performance metrics reveal a 12% drop in user retention post-update.

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; import { useState } from "react"; export function App() { // The interaction handler owns the card's state. Each named interaction maps // to a data update, and the document re-renders off that data via `Show` and // `Value` — so clicking a button meaningfully changes what the card shows. const [data, setData] = useState<Record<string, unknown>>({ status: "" }); return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={data} element={{ $type: "Document", // While the task is pending we show Approve / Request changes; once // resolved (via `else`) we swap in a single Reopen action that clears // the status and returns the card to its pending state. actions: { $type: "Show", children: [ { $type: "Action", appearance: "danger", children: "Request changes", onClick: { interaction: "request_changes" }, }, { $type: "Action", appearance: "primary", children: "Approve", onClick: { interaction: "approve" }, }, ], else: { $type: "Action", appearance: "subtle", children: "Reopen", onClick: { interaction: "reopen" }, }, when: { "!": { $type: "Value", path: "/status" } }, }, appName: "Content Marketing Platform", body: [ { $type: "Text", children: "Initial performance metrics reveal a 12% drop in user retention post-update.", }, { $type: "Show", children: { $type: "Alert", children: "Approved — the task has been moved to Done.", intent: "success", }, when: { "==": [{ $type: "Value", path: "/status" }, "approved"] }, }, { $type: "Show", children: { $type: "Alert", children: "Changes requested — sent back to the assignee.", intent: "danger", }, when: { "==": [{ $type: "Value", path: "/status" }, "changes"], }, }, ], subtitle: "Android Scrum Campaign / TSK-98", title: "Version 2.2 Performance Optimization", }} onInteraction={(name) => { if (name === "approve") { setData({ status: "approved" }); } else if (name === "request_changes") { setData({ status: "changes" }); } else if (name === "reopen") { setData({ status: "" }); } }} /> </Box> ); }

Sends a text message back to the LLM:

New Chat

How can I help you today?

Content Marketing Platform

Version 2.2 Performance Optimization

Android Scrum Campaign / TSK-98

Initial performance metrics reveal a 12% drop in user retention post-update.

Press an action above to send a message
"use client"; import { IconArrowUp, IconPlus } from "@optiaxiom/icons"; import { ProteusDocumentRenderer, type StructuredMessage, } from "@optiaxiom/proteus"; import { Box, Button, Flex, Paper, Spinner, Text } from "@optiaxiom/react"; import { useRef, useState } from "react"; type Message = { content: string; role: "assistant" | "user"; }; // The message actions in the document send these strings back to the LLM. Each // preset user message maps to a canned assistant reply so the mock chat can // respond without a real backend. const replies: Record<string, string> = { "I'd like to leave a comment on this task.": "Sure — what would you like the comment to say? I'll post it to TSK-98 once you confirm.", "Show me the full details for TSK-98.": "TSK-98 tracks the 12% retention drop after v2.2. Root cause is a slower cold-start on Android; a fix is in review, targeted for v2.2.1.", }; const messageToText = (message: string | StructuredMessage) => typeof message === "string" ? message : message.parts.map((part) => part.content).join(""); export function App() { const [messages, setMessages] = useState<Message[]>([]); const [pending, setPending] = useState(false); const threadRef = useRef<HTMLDivElement | null>(null); const onMessage = (message: string | StructuredMessage) => { const text = messageToText(message); setMessages((prev) => [...prev, { content: text, role: "user" }]); setPending(true); // A real app would stream the reply from the LLM here — we resolve a preset // response after a short delay to mimic the round-trip. setTimeout(() => { setMessages((prev) => [ ...prev, { content: replies[text] ?? "Got it — I'll take a look and get back to you shortly.", role: "assistant", }, ]); setPending(false); threadRef.current?.scrollTo({ behavior: "smooth", top: threadRef.current.scrollHeight, }); }, 600); }; return ( <Box bg="bg.default" border="1" borderColor="border.secondary" display="flex" flexDirection="column" maxW="md" overflow="hidden" rounded="lg" w="full" > <Box bg="bg.secondary" borderB="1" borderColor="border.secondary" display="flex" flexDirection="column" p="16" > <Text fontWeight="600">New Chat</Text> <Text color="fg.secondary" fontSize="sm"> How can I help you today? </Text> </Box> <Flex gap="16" maxH="lg" overflow="auto" p="16" ref={threadRef}> {/* The document is the assistant's opening message — the message actions it declares push preset user messages into the thread below. */} <ProteusDocumentRenderer collapsible={false} element={{ $type: "Document", actions: [ { $type: "Action", children: "Comment", onClick: { message: "I'd like to leave a comment on this task.", }, }, { $type: "Action", appearance: "primary", children: "View task", onClick: { message: "Show me the full details for TSK-98.", }, }, ], appName: "Content Marketing Platform", body: [ { $type: "Text", children: "Initial performance metrics reveal a 12% drop in user retention post-update.", }, ], subtitle: "Android Scrum Campaign / TSK-98", title: "Version 2.2 Performance Optimization", }} onMessage={onMessage} /> {messages.map((message, index) => message.role === "user" ? ( <Paper alignSelf="end" bg="bg.accent.subtle" key={index} px="16" py="8" w="3/4" > <Text fontSize="sm">{message.content}</Text> </Paper> ) : ( <Text alignSelf="start" fontSize="sm" key={index} w="3/4"> {message.content} </Text> ), )} {pending && ( <Flex alignSelf="start" flexDirection="row" gap="8"> <Spinner size="sm" /> <Text color="fg.tertiary" fontSize="sm"> Thinking… </Text> </Flex> )} </Flex> {/* Prompt is disabled — this demo only sends the document's preset messages, so there's nothing to type. */} <Flex borderColor="border.secondary" borderT="1" flexDirection="row" gap="8" p="12" > <Button aria-label="Add attachment" disabled icon={<IconPlus />} /> <Box alignItems="center" color="fg.tertiary" display="flex" flex="1" fontSize="sm" > Press an action above to send a message </Box> <Button appearance="primary" aria-label="Send message" disabled icon={<IconArrowUp />} /> </Flex> </Box> ); }

Triggers a file download:

Strategic Pitch Presentation

Google Slides

Slide preview of Strategic Pitch Presentation
"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={{ download_url: "https://example.com/files/strategic-pitch-presentation.pptx", }} element={{ $type: "Document", actions: [ { $type: "Action", appearance: "primary", children: "Download", onClick: { action: "download", url: { $type: "Value", path: "/download_url" }, }, }, ], body: [ { $type: "Image", alt: "Slide preview of Strategic Pitch Presentation", src: "https://placehold.co/560x315", }, ], subtitle: "Google Slides", title: "Strategic Pitch Presentation", }} /> </Box> ); }

Actions can also edit form data directly, without a round-trip to your server. setValue writes value at a JSON-pointer path, replacing any existing value; pushValue appends to the array at path; and removeValue removes the array element at path. Paths resolve like Value (absolute /x, relative to the current context, or '' for the current context itself).

In-place data edits

Current status: draft

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; import { useState } from "react"; export function App() { const [data, setData] = useState<Record<string, unknown>>({ status: "draft", }); return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={data} element={{ $type: "Document", actions: [ { $type: "Action", appearance: "primary", children: "Mark as published", // `setValue` writes `value` at `path`, replacing any existing // value. `pushValue` / `removeValue` cover appending to and // removing from arrays. onClick: { action: "setValue", path: "/status", value: "published", }, }, ], body: [ { $type: "Text", children: [ "Current status: ", { $type: "Value", path: "/status" }, ], }, ], title: "In-place data edits", }} onDataChange={setData} /> </Box> ); }

Scripting

For logic that shouldn’t require a server round-trip, a document can ship JavaScript in a scripts map. Each module registers named handlers with register(name, fn); trigger one from any event source with { script: "module:handler", params }. Handlers run in a sandboxed Web Worker whose only capability is ctx.emit — they get exactly the authority the document already has (emitting the existing Proteus events), so treat scripts with the same trust as the rest of the document.

Scripted actions

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; import { useState } from "react"; export function App() { const [data, setData] = useState<Record<string, unknown>>({ tags: [] }); return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={data} element={{ $type: "Document", actions: [ { $type: "Action", appearance: "primary", children: "Add tag", // Triggers a named handler from the `scripts` map. The handler // decides which events to emit — here it appends the input value. onClick: { params: { tag: { $type: "Value", path: "/tag" } }, script: "main:addTag", }, }, ], body: [ { $type: "Field", children: { $type: "Input", name: "tag", placeholder: "Enter a tag", }, label: "Tag", }, { $type: "Group", children: { $type: "Map", children: { $type: "Badge", children: { $type: "Value", path: "" }, }, path: "/tags", }, flexDirection: "row", gap: "8", }, ], // Scripts run in a sandboxed Web Worker. A handler's only capability // is `ctx.emit`, so it can affect the document only through the // existing Proteus events. scripts: { main: [ "register('addTag', (ctx, params) => {", " const tag = params.tag;", " if (!tag) return;", " ctx.emit({ action: 'pushValue', path: '/tags', value: tag });", "});", ].join("\n"), }, title: "Scripted actions", }} onDataChange={setData} /> </Box> ); }

watch(path, fn) is the push counterpart to register: it runs edge-triggered whenever the value at path changes (use '/' to watch all data). A watcher can only ctx.emit existing events and does not react to changes its own emit causes, so it can’t loop.

Reactive watcher

Tick every item to unlock the next step.

"use client"; import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; import { useState } from "react"; export function App() { const [data, setData] = useState<Record<string, unknown>>({ items: [ { done: false, label: "Accept terms" }, { done: false, label: "Verify email" }, { done: false, label: "Add payment method" }, ], }); return ( <Box maxW="md" w="full"> <ProteusDocumentRenderer data={data} element={{ $type: "Document", body: [ { $type: "Text", children: "Tick every item to unlock the next step.", color: "fg.secondary", }, { $type: "Group", children: { $type: "Map", children: { $type: "Switch", children: { $type: "Value", path: "label" }, name: "done", }, path: "/items", }, flexDirection: "column", gap: "8", }, { $type: "Show", children: { $type: "Alert", children: "All items complete — you're good to go!", intent: "success", }, when: { "!!": { $type: "Value", path: "/all_done" } }, }, ], // A watcher reacts to data changes (edge-triggered) — the push // counterpart to `register`. It runs once on the false→true edge and // its own setValue write does not re-trigger it, so it can't loop. scripts: { main: [ "watch('/items', (ctx, current) => {", " const items = current || [];", " const done = items.length > 0 && items.every((i) => i.done === true);", " ctx.emit({ action: 'setValue', path: '/all_done', value: done });", "});", ].join("\n"), }, title: "Reactive watcher", }} onDataChange={setData} /> </Box> ); }

Theming

Pass themeOverride to re-skin a document without touching its element tree. It takes a partial of the Axiom theme contract — any subset of any category (colors, borderRadius, fontFamily, spacing, …) — and applies those tokens as scoped CSS variables on a wrapper around the document. Only the tokens you list are overridden; everything else inherits the ambient theme, and the overrides never leak to the rest of the page.

Because a token like bg.accent (the primary button’s face) is paired with a dark foreground token, keep overridden surfaces that hold text light enough for the text to remain legible.

System Properties

Display Properties

General · Device Manager · Hardware Profiles

The same document, re-skinned into a Windows 98 dialog purely through the themeOverride prop — the element tree is unchanged.

ReadyError
import { ProteusDocumentRenderer } from "@optiaxiom/proteus"; import { Box } from "@optiaxiom/react"; export function App() { return ( <Box maxW="md" p="24" style={{ background: "#008080" }} w="full"> <ProteusDocumentRenderer element={{ $type: "Document", actions: [ { $type: "Action", appearance: "primary", children: "OK" }, { $type: "Button", children: "Cancel" }, ], appName: "System Properties", body: [ { $type: "Text", children: "The same document, re-skinned into a Windows 98 dialog purely through the themeOverride prop — the element tree is unchanged.", fontSize: "sm", }, { $type: "Group", children: [ { $type: "Badge", children: "Ready", intent: "success" }, { $type: "Badge", children: "Error", intent: "danger" }, ], flexDirection: "row", gap: "8", }, ], subtitle: "General · Device Manager · Hardware Profiles", title: "Display Properties", }} // A partial of the `theme` contract. Only the tokens listed here are // overridden — everything else inherits the ambient theme, and the // overrides are scoped to this document. Note the primary button face // (`bg.accent`) stays light: its label uses a dark `fg.*` token, so a // dark face would be unreadable. themeOverride={{ borderRadius: { xs: "0", sm: "0", md: "0", lg: "0", xl: "0", full: "0", }, colors: { "bg.accent": "#7bc5c5", "bg.accent.hovered": "#5fb0b0", "bg.accent.subtle": "#000080", "bg.default": "#c0c0c0", "bg.page": "#c0c0c0", "bg.pill.default": "#c0c0c0", "border.default": "#000000", "border.secondary": "#808080", "border.tertiary": "#808080", "fg.default": "#000000", "fg.secondary": "#000080", "fg.tertiary": "#404040", }, fontFamily: { heading: "'MS Sans Serif', Tahoma, Geneva, sans-serif", sans: "'MS Sans Serif', Tahoma, Geneva, sans-serif", }, }} /> </Box> ); }

Embedding external UI

The Federated element loads a remote React component at runtime via Module Federation  and renders it inline in the document. Point entry at the remote’s remoteEntry.js (or mf-manifest.json), and use exposeKey to pick which exposed module to render (defaults to "."). Provide a fallback — any Proteus node — to render if the remote fails to load.

{ "$type": "Document", "body": [ { "$type": "Federated", "entry": "https://cdn.example.com/widget/remoteEntry.js", "exposeKey": "./Dashboard", "fallback": [ { "$type": "Text", "children": "Couldn't load the dashboard widget.", "color": "fg.error" } ] } ] }

The remote receives the document’s current form data as props, so a federated widget can read the same state the rest of the document binds to. There’s no live demo here because the element needs a real remote served over the network; wire it against your own remoteEntry.js to try it.

The Bridge element embeds sandboxed HTML — such as an MCP UI  or OpenAI apps widget — inside an isolated iframe. Give it a resource URI; the renderer resolves it through the useResource hook you pass to ProteusDocumentRenderer, which returns the HTML to render. The iframe auto-resizes to its content, or you can pin a height. The embedded widget communicates back through the standard bridge protocol (calling tools, sending follow-up messages), which surface as the renderer’s onInteraction and onMessage callbacks.

<ProteusDocumentRenderer element={{ $type: "Document", body: [{ $type: "Bridge", height: 200, resource: "ui://my-widget" }], }} onInteraction={(name, params) => { /* widget called a tool */ }} onMessage={(message) => { /* widget sent a follow-up message */ }} useResource={(uri) => ({ data: { mimeType: "text/html+skybridge", text: "<!doctype html>…" }, isError: false, })} />

Proteus Designer

The Proteus Designer is a visual editor for building Proteus documents. You can build a component tree, edit properties in the inspector, see a live preview of how the document renders, and copy the final JSON into your resource handler. You can also provide sample data to test how dynamic values resolve.

Element reference

actions

Actions available for this document

Node
appearance

Visual treatment of the document shell. 'default' renders the document as a card with background, border, and padding. 'inline' strips the surrounding chrome so the document blends into its host container.

"default" | "inline" | Expression
appIcon

URL or data URI for the application icon (e.g., 'https://example.com/icon.png' or 'data:image/svg+xml,...'). Rendered as an <img> element.

string
appName

The official name of the application

string
blocking

If true, hides chat prompt and forces user interaction with document. User can press ESC or close to abandon.

boolean
body*

The main content of the document.

Node
compact

If true, constrains the body to a max height and makes it scrollable when content overflows.

boolean
data

Initial data for the document. Not used by the renderer directly — intended for the outer component managing state to use as the starting data.

object
meta

Additional metadata not directly consumed by Proteus. Use this to pass along any extra data.

any
scripts

Map of module name to JavaScript source. Each module runs in a sandboxed Web Worker and registers named handlers via register(name, fn). Trigger a handler from any event source with { script: 'module:handler', params }; handlers receive a single ctx ({ emit, getValue, params }) and can only affect the document by emitting the existing Proteus events through ctx.emit.

object
subtitle

A brief description or tagline that provides additional context about the Proteus document's purpose.

Node
title

A concise heading that encapsulates the essence of the Proteus document's content or intended action.

Node
titleIcon

URL or data URI for an icon displayed alongside the title in a block-style header.

string
alignItems

Set the element's align-items CSS property. Defaults to center when flexDirection='row', and stretch when flexDirection='column'.

"stretch" | "center" | "end" | "start" | "normal" | Expression
children
Node
flexDirection

Set the element's flex-direction CSS property. Default: 'row' (CSS standard)

"column" | "column-reverse" | "row" | "row-reverse" | Expression
children
Node
addonAfter

Display content inside the header after children.

Node
addonBefore

Display content inside the header before children.

Node
children
Node
description

Add secondary text after the primary title.

Node
lineClamp

Truncate the text at specific number of lines.

"2" | "1" | "3" | "4" | Expression
children
Node
href

The link href.

string | Expression
onClick

Action triggered when link is clicked

EventHandler

Accepts only $type plus the standard styling props.

children
Node
level

Heading level (1-4) that controls both the semantic HTML tag and font size. - level="1": renders <h1> with fontSize="4xl" (default) - level="2": renders <h2> with fontSize="3xl" - level="3": renders <h3> with fontSize="2xl" - level="4": renders <h4> with fontSize="xl" Use asChild to decouple the semantic level from visual appearance.

"2" | "1" | "3" | "4" | Expression
children
Node
lineClamp

Truncate the text at specific number of lines.

"2" | "1" | "3" | "4" | Expression
truncate

Whether to truncate the text and add an ellipsis at the end.

boolean | Expression
children
Node
href

The link href.

string | Expression
columns
Array | Expression
data
Array | Expression | Zip
rowSelection

JSON pointer where the selected rows are stored (e.g. '/selection'). When set, the table renders a select-all header checkbox and a per-row checkbox column, and writes the selected row objects as an array to this pointer so an action can read them via Value.

string
path

JSON pointer path to a field within the current row (e.g. 'status' or '/status'). When omitted, resolves to the whole row object. Only meaningful inside a DataTable column's cell.

string
data

Chart data records, either inline, a ProteusExpression, or a ProteusZip transformation

Array | Expression | Zip
layout

Chart layout direction

"horizontal" | "vertical"
series
Array | Expression
type

Chart type

"bar" | "line"
xAxisKey

Key in data records for x-axis labels

string
alt

Alternative text for the image

Expression | string
src

The image source URL

Expression | string
images*

Array of image data to display in the carousel

Array | Expression
title

Accessible label for the carousel region.

Expression | string
children
Node
colorScheme

Control the avatar fallback background and text colors.

"purple" | "neutral" | Expression
fallback

The fallback icon to display when no name or image is given.

"opal" | "team" | "user" | Expression
name

Use name to generate initials to show inside the avatar.

string | Expression
size

Control the size of the avatar.

"xs" | "sm" | "md" | "lg" | "xl" | "3xl" | "2xs" | Expression
src

Render the image inside the avatar.

string | Expression
children
Node
intent

Control the appearance by selecting between the different badge types.

"information" | "success" | "warning" | "danger" | "neutral" | "primary" | Expression
date*

The date to display. Can be a Date object or an ISO 8601 string. Unparseable values render nothing at runtime instead of throwing.

string | Expression
showDate

Whether to show the date part of the value. Defaults to true.

boolean | Expression
showTime

Whether to show the time part of the value. Defaults to false.

boolean | Expression
children
Node
description

Provide description and help text for the field.

Node
info

Display a help icon with additional context for the input.

Node
label

The label of the field.

Node
required

Display an asterisk for required inputs.

boolean | Expression
addonAfter

Display content inside the input at the end.

Node
addonBefore

Display content inside the input at the start.

Node
appearance

Control the appearance of the input.

"number" | "default" | Expression
autoFocus

Whether the input should be focused on mount.

boolean | Expression
name

The name of the form control element.

string | Expression
placeholder

The placeholder text to use when control has no value.

string | Expression
required

Whether selecting this input is required.

boolean | Expression
type

The input type.

"number" | "color" | "button" | "checkbox" | "radio" | "hidden" | "text" | "reset" | "range" | "search" | "time" | "image" | "tel" | "url" | "email" | "date" | "submit" | "month" | "datetime-local" | "week" | "file" | "password" | string | Expression
maxRows

Limits the height of the textarea when resize=auto is used.

1 | 2 | 3 | 4 | 5 | Expression
name

The name of the form control element.

string | Expression
placeholder

The placeholder text to use when control has no value.

string | Expression
required

Whether selecting this input is required.

boolean | Expression
resize

Control whether resizing mode is manual, automatic, or disabled.

"none" | "auto" | "vertical" | Expression
rows

The number of rows to display.

number | Expression
children
Node
name

The name of the inner select element.

string | Expression
options*

The select items/options we want to render.

Array
required

Whether the select value is required.

boolean | Expression
children
Node

Accepts only $type plus the standard styling props.

children
Node
description

Add secondary text after the label.

Node
name

The name of the form control element.

string | Expression
required

Whether selecting this input is required.

boolean | Expression
children
Node
description

Add secondary text after the label.

Node
name

The name of the form control element.

string | Expression
required

Whether selecting this input is required.

boolean | Expression
marks

The marks to display on the range steps.

Array
max

The maximum value for the range.

number | Expression
min

The minimum value for the range.

number | Expression
step

The stepping interval for the range.

number | Expression
interaction

Name of the interaction to fire when the user finishes or cancels. When set, submit and cancel send a structured { questions, answers } payload via the named interaction so a calling tool can read the answers programmatically. When omitted, a human-readable transcript is sent via the message event instead.

string | Expression
questions*

Array of questions data

Expression
appearance

Control the appearance by selecting between the different button types.

"default" | "danger" | "primary" | "subtle" | "danger-outline" | "default-opal" | "inverse" | "primary-opal" | Expression
children
Node
disabled

Whether the button is disabled.

boolean | Expression
icon

Display an icon before or after the button content or omit children to only show the icon.

Node
type

The default behavior of the button.

"button" | "reset" | "submit" | Expression
aria-label

Accessible label for screen readers. Required when the button renders an icon with no text children.

string
onClick

Action triggered when button is clicked

EventHandler
appearance

Control the appearance by selecting between the different button types.

"default" | "danger" | "primary" | "subtle" | "danger-outline" | "default-opal" | "inverse" | "primary-opal" | Expression
children
Node
disabled

Whether the button is disabled.

boolean | Expression
icon

Display an icon before or after the button content or omit children to only show the icon.

Node
type

The default behavior of the button.

"button" | "reset" | "submit" | Expression
aria-label

Accessible label for screen readers. Required when the button renders an icon with no text children.

string
onClick

Action triggered when button is clicked

EventHandler
formatter

Optional formatter to apply to the resolved value. Can be a string shorthand or an object with type and options for Intl formatters.

"DateTime" | "Number" | object
path*

Path to a value in the data. Absolute paths start with '/' and resolve from the root (e.g., '/title', '/options/0/label'). Inside a Map template, paths without a leading '/' are relative to the current item (e.g., 'title' resolves to each item's 'title' field).

string
children

Template object to render for each item in the array. Value paths inside this template are relative to the current item (e.g., path='title' resolves to each item's 'title' field). Use a leading '/' to reference top-level data (e.g., path='/title' resolves to the root data's 'title').

Node
flat

When true, flattens the result array by one level. Useful when each mapped item resolves to an array and you want a single flat list.

boolean
path*

JSON pointer path to the source array in the data (e.g., '/results')

string
separator

Optional separator to render between items. Can be a string or a ProteusNode for more complex separators.

Node

Accepts only $type plus the standard styling props.

children

Array of values to concatenate into a single string. Each item is resolved and joined together.

Array
sources*

Map of output property names to array sources. Each source should resolve to an array of the same length.

object
children

Content to show when condition is true

Node
else

Content to render when the condition is false. Omitting it renders nothing. Chain nested Show/else to express multi-way choices (e.g. mapping a value to one of several outputs).

Node
when

Single condition or array of conditions (AND logic). Each condition is an object with one operator key.

Condition | Array
fallback

Content rendered on platforms without iframe support (Teams, Slack, mobile). If omitted, a default 'View in Opal web' message is shown.

Node
height

Height of the iframe in pixels

number
resource*

Resource URI identifying the MCP app to render (e.g., 'ui://sample-widget')

string
entry*

URL to the remote application's remoteEntry.js or mf-manifest.json

string
exposeKey

The key from the remote's ModuleFederationPlugin exposes config (e.g. './App'). Defaults to '.' (root export)

string
fallback

Content rendered when the federated component fails to load

Node
Last updated on