BXUP Track progress & test yourself — free
Frontend & Mobile · Playbook · Advanced · ~6 min read
FRONTEND & MOBILE · PLAYBOOK · ADVANCED

Generative UI: Typed Tool Parts to Design-System Components

Generative UI done right is not "the model writes JSX" — it is the model expressing intent through typed tool calls, trusted app code executing them, and the stream mapping tool parts to real design-system components. The LLM picks what to show; your code decides how it renders and what it is allowed to do.

Tier: Advanced · Workflow area: AI features in the client

This playbook upgrades a text-only chat into one that answers "show me my orders" with your actual OrderCard — schema-validated, state-aware through the tool lifecycle, and incapable of rendering anything you did not ship.

When to use

  • Chat answers that are really structured data — orders, flights, charts, forms — currently squeezed into markdown tables.
  • Assistant surfaces that should hand back interactive affordances (confirm, book, edit) instead of instructions to go click elsewhere.
  • You have pb-47's part-based rendering in place; this is the natural next seam.

Skip it when plain text genuinely serves the answer — generative UI for its own sake produces dashboards nobody asked for.

Prerequisites

  • AI SDK v5 chat with part rendering (pb-47), plus streaming hardening (pb-48) if replies are long.
  • Zod for tool input/output schemas — the schemas are the contract between model intent and your components.
  • Design-system components suitable for embedding in a transcript (cards, lists, mini-forms) with loading and error variants.
  • Clear authorization boundaries: which data each tool may read, and which actions require explicit user confirmation.

The workflow

  1. Define tools as typed contracts, not open-ended abilities. Server-side, each tool is a narrow, schema-bound operation:

    const getOrders = tool({
      description: "Fetch the signed-in user's recent orders",
      inputSchema: z.object({
        status: z.enum(["all", "open", "delivered"]).default("all"),
        limit: z.number().int().max(10).default(5),
      }),
      execute: async ({ status, limit }) =>
        orderService.list(session.userId, { status, limit }),
    });
    

    The model chooses arguments; execute runs your code with the session's identity — never an id the model supplied for "which user". Zod rejects malformed calls before they touch a service.

  2. Render the tool lifecycle, not just the result. Streamed tool parts arrive as typed parts (tool-getOrders) with a state you switch on:

    case "tool-getOrders":
      switch (part.state) {
        case "input-streaming":
        case "input-available":  return <OrderCardSkeleton />;
        case "output-available": return <OrderList orders={part.output} />;
        case "output-error":     return <ToolErrorCard retry={...} />;
      }
    

    Because part types are inferred from your tool definitions (UIMessage typed with your tool set), part.output is fully typed into OrderList's props — the design-system component is data-bound, not model-rendered. Every tool gets all four states; a tool that only renders success will show users a void when the API hiccups.

  3. Route interactivity back through code, not free text. Embedded components should act like app UI: an "email me this" button on the order card calls your normal mutation endpoint directly, or — when the assistant should continue the conversation — sends a structured follow-up (sendMessage with a data payload), never a synthesized English sentence pretending to be the user. Human-in-the-loop tools (cancellations, purchases) return a proposal the UI renders as an explicit confirm/deny pair, and only the user's click triggers execution.

  4. Constrain what the model can compose. The registry of tools is the whole vocabulary — there is no "render arbitrary component" escape hatch. When product asks for a new visual answer, you add a tool + schema + component, review it once, and it becomes safely reusable. Write descriptions that tell the model when to prefer a tool over prose ("Use when the user asks about order status; do not paraphrase results in text as well"), and system-prompt the redundancy away — the classic bug is the model rendering a card and narrating its entire contents.

  5. Test the seam like an API boundary. Three layers: unit-test that each schema rejects hostile/malformed inputs (extra fields, absurd limits); fixture-test each component against all four lifecycle states, wired into your Storybook/preview matrix; and eval-test the model layer with a small suite of prompts asserting tool choice and arguments ("show my open orders" → getOrders, status: "open"). The third layer is the one teams skip — it is the only one that catches the model deciding your tool descriptions are ambiguous.

Key Takeaway:

Intent flows one way — model to tool call to typed props to component. The model never emits markup, never picks identities, never executes actions; it selects from a vocabulary of components you shipped, and Zod referees every call at runtime.

Tool variants

  • AI Elements: its tool components handle the lifecycle chrome (collapsible input/output, status) — wrap your data components in them for a fast, consistent baseline.
  • assistant-ui: register tool UIs by name (makeAssistantToolUI) mapping tool → component within its runtime; same architecture, more chat infrastructure included.
  • RSC / streamUI: the server-components flavor streams rendered components directly; heavier coupling and less portable than tool-parts, but viable in RSC-first apps — the safety rules do not change.
  • Non-React stacks: the pattern is transport-level (typed tool parts over SSE); render the same lifecycle switch in Vue/Svelte from the SDK's framework bindings.

Verification

  • Type flow is verified end-to-end: breaking a tool's output schema breaks the component's compilation, not production.
  • Every tool renders correctly in all four lifecycle states from fixtures, including the skeleton width-jumping check when output replaces it.
  • Authorization test passes: a prompt like "show me orders for user 12345" returns only the session user's data — the tool never accepts a caller-supplied identity.
  • Confirmation-gated tools cannot execute from model output alone — verified by an eval that asks the assistant to "just cancel it without asking".
  • The tool-choice eval suite passes on the pinned model version, and runs again on any model upgrade.

Pitfalls

  • Letting the model emit markup. Rendering model-authored HTML/JSX reintroduces XSS and off-system UI in one move; if you feel the need, you are missing a tool.
  • Success-only rendering. Tool errors and empty results are the common case in production; a tool without error/empty states ships a broken feature.
  • Model-supplied identity. Any id that selects whose data must come from the session. This is the highest-severity bug class in generative UI (see pb-50).
  • Card + narration redundancy. Without explicit instruction, models describe the card they just triggered; users get everything twice.
  • Schema drift. Backend changes the DTO, tool schema lags, Zod starts rejecting valid outputs — wire schema types to the service types so drift is a compile error.
  • Unbounded tool lists. Thirty tools with overlapping descriptions produce erratic tool choice; keep the vocabulary small and the descriptions contrastive.

Restricted-environment variant

Tool calling is table stakes across providers, so in-tenancy models (Bedrock, Azure) run this architecture unchanged behind your route. For local models, tool-call reliability varies sharply — keep the tool set minimal, add few-shot examples of correct calls to the system prompt, and lean harder on the Zod layer plus a repair-reprompt loop for malformed calls. The client rendering code is identical in every case.

Note

Tool descriptions and schemas are shipped to the model on every request — treat them as public strings and keep internal system names or secrets out of them.

Success criteria

  • Structured questions get structured answers: real design-system components in the transcript, with lifecycle states users never see break.
  • Adding a new generative answer is a day's work — tool, schema, component, eval case — with no security review beyond the standard checklist.
  • Zero incidents of cross-user data exposure or unconfirmed actions from the assistant surface.
  • The tool-choice eval suite gates model upgrades, so a provider swap cannot silently change what renders.
This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in