Skip to content

Agent-Generated Workout Tracker UI

View the complete sample on GitHub

Related documentation: Custom tools, Streaming, Session management

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, FastAPI

Architecture

sequenceDiagram
participant B as Browser (web/app.js)
participant S as FastAPI (server.py)
participant A as Strands agent (agent.py)
B->>S: POST /api/chat { chat, message }
S->>A: agent(message)
A->>A: list_workouts / summarize_workouts
A->>A: render_ui(components) - validated against catalog.py
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. catalog.py defines every component as a Pydantic model. Those models are 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 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

  • Python 3.10+
  • AWS credentials configured, with model access to the Strands default model on Amazon Bedrock (Claude Sonnet)

Setup

Terminal window
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Usage

Terminal window
python server.py

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 (catalog.py). Each component is a Pydantic model with a type literal, combined into a discriminated union. Field descriptions are written for the model, because they end up in the tool schema.

class Stat(BaseModel):
type: Literal["stat"]
label: str
value: str = Field(description="The headline number, formatted for display, e.g. '42 km'.")
Component = Annotated[Text | Stat | Table | BarChart | Form | Button | Row, Field(discriminator="type")]

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

@tool
def render_ui(self, components: list[Component]) -> str:
"""Show UI components to the user, in order, below your reply."""
self.ui.extend(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 catalog.py 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. A React version is the same lookup table with a component per 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(...) to agent.stream_async(...) 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
catalog.pyComponent catalog (Pydantic models)
agent.pySystem prompt, render_ui tool, and Chat wrapper
tools.pyWorkout data tools
server.pyFastAPI app: POST /api/chat and static files
web/Browser client and renderer
data/workouts.jsonSample workouts, with dates relative to today

A TypeScript version of this sample lives in typescript/03-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.