Skip to content

Dynamic UI

View the complete sample on GitHub

An agent that answers with UI, not just text. You ask a question, the agent picks components from a catalog the app defines (stats, charts, tables, forms, buttons), and the browser renders them. When someone submits a form or clicks a button, the interaction goes back to the agent, which acts on it and renders again.

The sample is a small workout log, but the pattern carries over to any app: define your components as a schema, give the agent a tool that takes that schema, and render what it sends.

The agent rendering stats, a weekly chart, a table, and a workout form

Overview

Sample Details

InformationDetails
Agent ArchitectureSingle-agent
Native ToolsNone
Custom Toolsrender_ui, list_workouts, summarize_workouts, log_workout
MCP ServersNone
Use Case VerticalGenerative UI
ComplexityBasic
Model ProviderAmazon Bedrock
SDK UsedStrands Agents SDK (TypeScript), Express, Zod

Architecture

sequenceDiagram
participant B as Browser (web/app.js)
participant S as Express (src/server.ts)
participant A as Strands agent (src/agent.ts)
B->>S: POST /api/chat { chat, message }
S->>A: agent.invoke(message)
A->>A: list_workouts / summarize_workouts
A->>A: render_ui(components) - validated against src/catalog.ts
A-->>S: reply text + rendered components
S-->>B: { text, ui }
B->>B: one renderer per component type
B->>S: form submit / button click as "[ui action] ..." message

Key Features

  • The schema is the contract. src/catalog.ts defines every component with Zod. That schema is the render_ui tool’s input, so the model can only produce components your app supports, and Strands validates them before your code sees them. The same schema gives you the Component TypeScript type.
  • The agent chooses the layout. Different questions get different UI: totals become stat cards, trends become a bar chart, lists become a table, and “I want to log a workout” becomes a form.
  • Interactive round trip. Forms and buttons carry an action name. Interactions come back to the agent as a chat message, so it can log the workout and render an updated view.
  • No frontend build. The client is plain HTML and JavaScript. It renders with DOM APIs and text nodes only, so model output can’t inject markup.

Prerequisites

  • Node.js 20+
  • AWS credentials configured, with model access to the Strands default model on Amazon Bedrock (Claude Sonnet)

Setup

Terminal window
npm install

Usage

Terminal window
npm start

Open http://127.0.0.1:8000. The server only listens on 127.0.0.1 and has no authentication, so it’s for local use. Set PORT to change the port.

Example Queries

  • “How has my training been this month?” (stats, a weekly bar chart and a table)
  • “Show my runs from the last two weeks” (a table)
  • “I want to log a workout” (a form; submit it and the agent logs the workout and shows the updated totals)

How It Works

1. Define the catalog (src/catalog.ts). Each component is a Zod object with a type literal, combined into a discriminated union. Descriptions are written for the model, because they end up in the tool schema.

const stat = z.object({
type: z.literal("stat"),
label: z.string(),
value: z.string().describe("The headline number, formatted for display, e.g. '42 km'."),
});
export const component = z.discriminatedUnion("type", [text, stat, table, barChart, form, button, row]);

2. Give the agent a render_ui tool (src/agent.ts). The tool’s only job is to collect validated components for the current turn.

tool({
name: "render_ui",
description: "Show UI components to the user, in order, below your reply.",
inputSchema: z.object({ components: z.array(component) }),
callback: ({ components }) => {
this.ui.push(...components);
return "Shown to the user. Now write your short reply.";
},
});

3. Return the components with the reply. Chat.send() runs the agent and returns { text, ui }.

4. Render by type (web/app.js). A lookup table maps each type to a function that builds DOM. Unknown types are skipped.

5. Send interactions back. A form submit becomes a message like [ui action] log_workout {"activity": "swim", "minutes": "40"}, and the system prompt tells the agent how to handle it.

Using This in Your Own App

  • Replace src/catalog.ts with the components your design system already has, and keep the props small and display-ready (formatted strings rather than raw values the client has to format).
  • Write the renderer in your own frontend framework. In React it’s the same lookup table with a component per type, typed with the exported Component type.
  • Do calculations in tools rather than letting the model do arithmetic. Here, summarize_workouts computes every total and the prompt tells the agent to use it.
  • To render while the model is still writing, switch from agent.invoke(...) to agent.stream(...) and forward the tool input as it streams.
  • Each chat keeps its agent in server memory and the workout log isn’t persisted, which keeps the sample short. A real app would add a session manager and a database.

Project Structure

FilePurpose
src/catalog.tsComponent catalog (Zod schema)
src/agent.tsSystem prompt, render_ui tool, and Chat wrapper
src/tools.tsWorkout data tools
src/server.tsExpress app: POST /api/chat and static files
web/Browser client and renderer
data/workouts.jsonSample workouts, with dates relative to today

A Python version of this sample lives in python/07-ux-demos/dynamic-ui and uses the same web client.

Additional Resources

Disclaimer

This sample is provided for educational and demonstration purposes only. It is not intended for production use without further development, testing, and hardening.