Skip to content
EN DE

Streaming Text Parts by Hand

Normally, streamText handles text streaming automatically through mergeIntoDataStream. But sometimes you need manual control — to prepend custom text before the LLM responds, combine outputs from multiple LLM calls, or inject formatted text between stream segments.

Manual text streaming uses writer.write() with type: 'text-delta' and a textDelta property. Each write appends to the current text part in the message. The frontend treats these identically to text chunks from streamText.

This approach gives you full control over what appears in the stream and in what order. You can mix hand-written text with LLM-generated text in a single response.

import { createUIMessageStreamResponse } from 'ai';
export async function POST(req: Request) {
return createUIMessageStreamResponse({
execute(writer) {
writer.write({ type: 'text-delta', textDelta: 'Hello ' });
writer.write({ type: 'text-delta', textDelta: 'World!' });
},
});
}

The frontend sees “Hello World!” as a single text part, streamed incrementally.

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) {
// Manual prefix
writer.write({ type: 'text-delta', textDelta: '**Analysis:**\n\n' });
// LLM-generated content
const result = streamText({
model: anthropic('claude-sonnet-4-5-20250514'),
messages,
});
result.mergeIntoDataStream(writer);
},
});
}
import { createUIMessageStreamResponse, streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
export async function POST(req: Request) {
return createUIMessageStreamResponse({
async execute(writer) {
// First LLM call
writer.write({ type: 'text-delta', textDelta: '## Summary\n\n' });
const summary = streamText({
model: anthropic('claude-sonnet-4-5-20250514'),
prompt: 'Summarize AI SDK streaming in 2 sentences.',
});
summary.mergeIntoDataStream(writer);
await summary.text; // Wait for completion
// Separator
writer.write({ type: 'text-delta', textDelta: '\n\n---\n\n## Details\n\n' });
// Second LLM call
const details = streamText({
model: anthropic('claude-sonnet-4-5-20250514'),
prompt: 'Explain AI SDK streaming in detail.',
});
details.mergeIntoDataStream(writer);
},
});
}
Write ObjectEffect
{ type: 'text-delta', textDelta: '...' }Appends text to the current text part
{ type: 'data', data: {...} }Adds a data part (see Data Parts reference)

Part of AI Learning — free courses from prompt to production. Jan on LinkedIn