content/docs/04-ai-sdk-ui/20-streaming-data.mdx
It is often useful to send additional data alongside the model's response. For example, you may want to send status information, the message ids after storing them, or references to content that the language model is referring to.
The AI SDK provides several helpers that allows you to stream additional data to the client
and attach it to the UIMessage parts array:
createUIMessageStream: creates a data streamcreateUIMessageStreamResponse: creates a response object that streams datapipeUIMessageStreamToResponse: pipes a data stream to a server response objectThe data is streamed as part of the response stream using Server-Sent Events.
First, define your custom message type with data part schemas for type safety:
import { UIMessage } from 'ai';
// Define your custom message type with data part schemas
export type MyUIMessage = UIMessage<
never, // metadata type
{
weather: {
city: string;
weather?: string;
status: 'loading' | 'success';
};
notification: {
message: string;
level: 'info' | 'warning' | 'error';
};
} // data parts type
>;
In your server-side route handler, you can create a UIMessageStream and then pass it to createUIMessageStreamResponse:
import { openai } from '@ai-sdk/openai';
import {
createUIMessageStream,
createUIMessageStreamResponse,
streamText,
convertToModelMessages,
} from 'ai';
__PROVIDER_IMPORT__;
import type { MyUIMessage } from '@/ai/types';
export async function POST(req: Request) {
const { messages } = await req.json();
const stream = createUIMessageStream<MyUIMessage>({
execute: ({ writer }) => {
// 1. Send initial status (transient - won't be added to message history)
writer.write({
type: 'data-notification',
data: { message: 'Processing your request...', level: 'info' },
transient: true, // This part won't be added to message history
});
// 2. Send sources (useful for RAG use cases)
writer.write({
type: 'source',
value: {
type: 'source',
sourceType: 'url',
id: 'source-1',
url: 'https://weather.com',
title: 'Weather Data Source',
},
});
// 3. Send data parts with loading state
writer.write({
type: 'data-weather',
id: 'weather-1',
data: { city: 'San Francisco', status: 'loading' },
});
const result = streamText({
model: __MODEL__,
messages: await convertToModelMessages(messages),
onFinish() {
// 4. Update the same data part (reconciliation)
writer.write({
type: 'data-weather',
id: 'weather-1', // Same ID = update existing part
data: {
city: 'San Francisco',
weather: 'sunny',
status: 'success',
},
});
// 5. Send completion notification (transient)
writer.write({
type: 'data-notification',
data: { message: 'Request completed', level: 'info' },
transient: true, // Won't be added to message history
});
},
});
writer.merge(result.toUIMessageStream());
},
});
return createUIMessageStreamResponse({ stream });
}
Regular data parts are added to the message history and appear in message.parts:
writer.write({
type: 'data-weather',
id: 'weather-1', // Optional: enables reconciliation
data: { city: 'San Francisco', status: 'loading' },
});
Sources are useful for RAG implementations where you want to show which documents or URLs were referenced:
writer.write({
type: 'source',
value: {
type: 'source',
sourceType: 'url',
id: 'source-1',
url: 'https://example.com',
title: 'Example Source',
},
});
Transient parts are sent to the client but not added to the message history. They are only accessible via the onData useChat handler:
// server
writer.write({
type: 'data-notification',
data: { message: 'Processing...', level: 'info' },
transient: true, // Won't be added to message history
});
// client
const [notification, setNotification] = useState();
const { messages } = useChat({
onData: ({ data, type }) => {
if (type === 'data-notification') {
setNotification({ message: data.message, level: data.level });
}
},
});
When you write to a data part with the same ID, the client automatically reconciles and updates that part. This enables powerful dynamic experiences like:
The reconciliation happens automatically - simply use the same id when writing to the stream.
The onData callback is essential for handling streaming data, especially transient parts:
import { useChat } from '@ai-sdk/react';
import type { MyUIMessage } from '@/ai/types';
const { messages } = useChat<MyUIMessage>({
api: '/api/chat',
onData: dataPart => {
// Handle all data parts as they arrive (including transient parts)
console.log('Received data part:', dataPart);
// Handle different data part types
if (dataPart.type === 'data-weather') {
console.log('Weather update:', dataPart.data);
}
// Handle transient notifications (ONLY available here, not in message.parts)
if (dataPart.type === 'data-notification') {
showToast(dataPart.data.message, dataPart.data.level);
}
},
});
Important: Transient data parts are only available through the onData callback. They will not appear in the message.parts array since they're not added to message history.
You can filter and render data parts from the message parts array:
const result = (
<>
{messages?.map(message => (
<div key={message.id}>
{message.parts
.filter(part => part.type === 'data-weather')
.map((part, index) => (
<div key={index} className="weather-widget">
{part.data.status === 'loading' ? (
<>Getting weather for {part.data.city}...</>
) : (
<>
Weather in {part.data.city}: {part.data.weather}
</>
)}
</div>
))}
{message.parts
.filter(part => part.type === 'text')
.map((part, index) => (
<div key={index}>{part.text}</div>
))}
{message.parts
.filter(part => part.type === 'source')
.map((part, index) => (
<div key={index} className="source">
Source: <a href={part.url}>{part.title}</a>
</div>
))}
</div>
))}
</>
);
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
import type { MyUIMessage } from '@/ai/types';
export default function Chat() {
const [input, setInput] = useState('');
const { messages, sendMessage } = useChat<MyUIMessage>({
api: '/api/chat',
onData: dataPart => {
// Handle transient notifications
if (dataPart.type === 'data-notification') {
console.log('Notification:', dataPart.data.message);
}
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
sendMessage({ text: input });
setInput('');
};
return (
<>
{messages?.map(message => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts
.filter(part => part.type === 'data-weather')
.map((part, index) => (
<span key={index} className="weather-update">
{part.data.status === 'loading' ? (
<>Getting weather for {part.data.city}...</>
) : (
<>
Weather in {part.data.city}: {part.data.weather}
</>
)}
</span>
))}
{message.parts
.filter(part => part.type === 'text')
.map((part, index) => (
<div key={index}>{part.text}</div>
))}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Ask about the weather..."
/>
<button type="submit">Send</button>
</form>
</>
);
}
Both message metadata and data parts allow you to send additional information alongside messages, but they serve different purposes:
Message metadata is best for message-level information that describes the message as a whole:
message.metadatamessageMetadata callback in toUIMessageStreamResponse// Server: Send metadata about the message
return result.toUIMessageStreamResponse({
messageMetadata: ({ part }) => {
if (part.type === 'finish') {
return {
model: part.response.modelId,
totalTokens: part.totalUsage.totalTokens,
createdAt: Date.now(),
};
}
},
});
Data parts are best for streaming dynamic arbitrary data:
message.partscreateUIMessageStream and writer.write()// Server: Stream data as part of message content
writer.write({
type: 'data-weather',
id: 'weather-1',
data: { city: 'San Francisco', status: 'loading' },
});
For more details on message metadata, see the Message Metadata documentation.