Custom Data Parts Streaming
Overview
Section titled “Overview”Custom Data Parts let you send structured data alongside the LLM’s text stream. Instead of embedding status updates or metadata in the text, you write typed objects into the same stream. The frontend receives them as distinct parts it can render independently.
There are two kinds: Persistent Data Parts are saved in message.parts and survive across re-renders. Transient Data Parts are delivered only via the onData callback and are not stored in message history. Use persistent parts for content the user should see (progress, sources), and transient parts for ephemeral signals (loading indicators, analytics events).
On the server side, createUIMessageStreamResponse gives you a writer that accepts both standard stream events and custom data. You write data parts with writer.write() and merge the LLM stream with result.mergeIntoDataStream(writer).
Server-Side: Writing Data Parts
Section titled “Server-Side: Writing Data Parts”import { createUIMessageStreamResponse, streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
export async function POST(req: Request) { const { messages } = await req.json();
return createUIMessageStreamResponse({ execute(writer) { // Persistent data part -- saved in message.parts writer.write({ type: 'data', data: { type: 'weather', city: 'Berlin', status: 'loading' }, });
const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), messages, onFinish() { writer.write({ type: 'data', data: { type: 'weather', city: 'Berlin', status: 'done', temp: 22 }, }); }, });
result.mergeIntoDataStream(writer); }, });}Writer Methods
Section titled “Writer Methods”| Method / Field | Type | Description |
|---|---|---|
writer.write({ type: 'data', data }) | Persistent | Saved in message.parts as a DataUIPart |
writer.write({ type: 'data', data, id }) | Persistent + ID | Enables ID reconciliation (updates existing part) |
result.mergeIntoDataStream(writer) | — | Pipes the LLM stream into the writer |
Transient Data Parts
Section titled “Transient Data Parts”Transient parts are not stored in message history. Send them for ephemeral signals:
writer.write({ type: 'data', data: { type: 'analytics', event: 'stream-started' },});The frontend receives them only in the onData callback of useChat, not in message.parts.
See also
Section titled “See also”- Custom Data Parts in the Frontend — consuming data parts with
useChat - Custom Data Parts: ID Reconciliation — progressive updates
- Challenge 7.1: Custom Data Parts — hands-on exercise
- Challenge 8.2: Streaming to Frontend — workflow streaming