Dev & EngARTICLE

Vercel's Chat SDK adds generative UI streaming via json-render

Vercel's Chat SDK adds generative UI streaming via json-render
Image: Carina Ferreira
Article · Dev & Eng
Reading
4 min
Code
1
In short

json-render lets Chat SDK agents generate interface cards directly within the conversation flow, converting the result into Block Kit on Slack or Adaptive Cards on Teams. A typed catalog of components keeps every message from turning into a new adapter to maintain.

Vercel documented, in its official changelog, an integration between Chat SDK (the company's framework for building conversational bots) and a new library called json-render. In practice, it solves a problem well known to anyone who has already built a bot for Slack or Teams: each platform has its own card format (Block Kit on one side, Adaptive Cards on the other), and manually keeping the two in sync is repetitive work.

With json-render, the AI agent doesn't write Block Kit or Adaptive Cards directly. It writes a JSON specification based on a typed catalog of components (tables, charts, buttons, etc.), and the library converts that specification into a Chat SDK card. Each platform adapter takes this intermediate card and translates it into the corresponding native format.

Typed catalog instead of one adapter per message

The central point of the architecture is the catalog: a definition, made just once, of which components exist and which props each one accepts. It's this catalog that becomes context for the model (via catalog.prompt()), telling it exactly what JSON is valid.

This changes the workflow for whoever builds the bot. Instead of writing a message template for every combination of "sales table," "usage chart," or "approval buttons," the team defines the component catalog once and lets the model decide, message by message, which combination to use. The example published by Vercel shows the minimal structure:

ts
import { defineCatalog } from "@json-render/core";
import {
 schema,
 standardComponentDefinitions,
 streamCard,
} from "@json-render/chat";
import { streamText } from "ai";

const catalog = defineCatalog(schema, {
 components: standardComponentDefinitions,
});

bot.onNewMention(async (thread, message) => {
 const result = streamText({
 model: "anthropic/claude-haiku-5.5",
 system: catalog.prompt(),
 prompt: message.text,
 });
 await streamCard(thread, result.textStream);
});

Note that the ai package (Vercel's own AI SDK) still does the work of calling the model with streamText(); json-render comes in afterward, taking that text stream and turning it into UI. It's the same generative UI pattern that Vercel already popularized in the AI SDK for web applications (with streamUI and React Server Components), now applied to chat surfaces instead of a page rendered in the browser.

The card starts incomplete and fills in over time

The streamCard() function doesn't wait for the entire JSON to arrive before posting something in the thread. It publishes the card as soon as the specification's root element has content, and keeps updating the message as new elements arrive in the model's stream.

By default, edits go out at most once every 500 ms, the same streaming interval Chat SDK already uses for plain text. When the stream ends, the message shows the final, validated version of the card, no longer a partial JSON. For anyone coming from the world of front-end generative UI, it's the equivalent of a loading skeleton that gains real components until it settles into the final layout.

Schema validation with a correction loop

The toCard function checks each prop of each element against the schema of the corresponding component in the catalog. If the model generates an invalid specification, toCard throws a CardSpecError with a readable list of issues, one per element.

The detail that matters to anyone building real agents is that this error list can be fed back to the model itself as context, asking for a corrected specification. It's a common pattern in agentic systems (validate, fail, resend the error, let the model correct itself), but here it already comes solved by the library instead of needing to be built by hand for every project.

The limits each platform imposes

Slack and Teams don't accept just anything in a card, and the default behavior of Chat SDK adapters is to degrade content that exceeds those limits rather than reject it. The example cited by Vercel: the Slack adapter turns a second table, beyond the first one allowed, into a code block.

This silent degradation may be the right behavior for an informational dashboard, but it's risky when the card carries an action (an approve button, for example) that the user expects to see clickable and that turns into text with no function. For these cases, json-render allows passing explicit limits to validation, causing the specification to be rejected (and resent to the model) instead of silently simplified.

Who this matters to, and when it isn't worth it

In short: anyone who already maintains a productivity bot on Slack or Teams, with multiple card types (report, alert, approval, status), gains a single typed catalog instead of one template per message format, plus the schema validation they would normally have to write by hand.

The trade-off is the same as with any model-driven generative UI: each card costs one call to the model (in the example, claude-haiku-5.5) and introduces latency and the possibility of a first, invalid specification that needs a second round of correction before appearing on screen. For bots with a small, fixed number of message formats, building Block Kit or Adaptive Card directly, without a model deciding the structure, still tends to be more predictable and cheaper to run.

It's worth it when the message catalog is large, changes frequently, or when the agent itself needs to dynamically decide what type of component the response requires, not when there are two or three stable card formats that already work well written by hand.

Translated from the Brazilian Portuguese original · Read the original