Agents that render rich, interactive components inline — tables, charts, forms, diffs — not just text. No frontend code required.
Traditional chatbot UIs render everything as text bubbles. But agents produce structured data — query results, validation reports, deployment status, comparison diffs. Rendering all of that as markdown is a poor experience.
Haira's Generative UI lets tools declaratively control how their output appears. A database query renders as a table. A validation result renders as a status card. A deployment pipeline renders as a progress tracker. All inline in the chat, all without writing frontend code.
Tools return UI components using the ui module. The runtime does two things with every result:
Neither path is sacrificed. The user sees a beautiful table, the agent sees structured data.
import "ui"
tool query_database(query: string) -> any {
"""Executes a SQL query and displays results."""
rows, err = postgres.query(db, query)
if err != nil {
return ui.status_card("error", "Query Failed", conv.to_string(err))
}
return ui.table("Query Results", headers, rows)
}Enable UI on an agent with a single line:
agent DataExplorer {
provider: OpenAI
tools: [query_database, search_data]
ui: ui
memory: conversation(max_turns: 30)
}import "ui"
ui.status_card("success", "Deploy Complete", "All 3 services updated")
ui.table("Results", ["Name", "Email"], [["Alice", "a@co"], ["Bob", "b@co"]])
ui.key_value("Server Info", {"Region": "us-east-1", "Status": "healthy"})
ui.chart("bar", "Revenue", ["Q1", "Q2", "Q3", "Q4"], [dataset])
ui.confirm("Delete this record?", "Yes, delete", "Cancel")
ui.group(
ui.status_card("success", "Query Complete", "42 rows"),
ui.table("Results", headers, rows)
)Every Haira workflow automatically gets a web UI. Define a workflow, Haira generates the form:
@webui(title: "File Summarizer", description: "Upload a file and get an AI summary")
@post("/summarize")
workflow Summarize(document: file, context: string) -> { summary: string } {
content, err = io.read_file(document)
if err != nil { return { summary: "Failed to read file." } }
reply, err = Summarizer.ask("Summarize: ${content}")
if err != nil { return { summary: "AI error." } }
return { summary: reply }
}The @webui decorator sets the title and description. file parameters render as upload inputs. Streaming workflows (-> stream) get a full chat interface.
Streaming workflows get the richest experience — real-time token streaming, tool execution cards, and inline UI components:
@webhook("/chat")
workflow Chat(message: string, session_id: string) -> stream {
return Assistant.stream(message, session: session_id)
}
fn main() {
http.Server([Chat]).listen(8080)
}The chat UI communicates via the ARP protocol — handling text deltas, tool lifecycle events, and rich component rendering over WebSocket or SSE.
For domain-specific needs, drop TypeScript Web Components into a components/ directory:
export class HairaGanttChart extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: "open" });
}
setProps(props) {
// Render your custom UI
}
}
export default {
tag: "haira-gantt-chart",
component: HairaGanttChart,
};Custom components inherit Haira's theme via CSS custom properties and can dispatch haira-action events that become chat messages. The compiler discovers, bundles, and embeds them at build time.
ui.* functionsNo separate frontend repo. No API client to maintain. No component library to install. One .haira file, one binary, full UI.