Skip to content

Ask the docs

Answers come only from these docs and are written by an AI model (Gemini), so check the linked pages. Your question is not kept. Do not include personal details or keys.

Stream replies

Streaming replies · stream LLM response to WhatsApp · stream OpenAI to Telegram · send ChatGPT reply to iMessage · LLM chat bubbles · typing indicator API

Turn a model stream into complete chat bubbles while showing typing.

Messaging apps have no streaming text box, so a streamed model answer is best sent as a few natural bubbles, each sent as soon as it is complete, with the typing indicator on in between.

TypeScript
import Anthropic from "@anthropic-ai/sdk";
import { FlowMessaging, contentText } from "@flow-engineer/messaging";
const flow = new FlowMessaging();
const anthropic = new Anthropic();
for await (const event of flow.events.stream({ types: ["message.received"] })) {
const stream = anthropic.messages.stream({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: contentText(event.data.message.content) }],
});
const sent = await event.conversation.reply(stream); // typing on, bubbles out, typing off
console.log(`sent ${sent.length} bubbles`);
}

conversation.reply(input) accepts a string, a kind object (basic, buttons, …), a list of them, or a stream. With a stream it:

  1. Shows the typing indicator, and keeps showing it (channels clear it after a few seconds).
  2. Reads text out of the stream as it arrives.
  3. Cuts a bubble at each natural break (below) and sends it while the model is still writing. Bubbles go out in order.
  4. Stops refreshing typing when the stream ends, even if it failed. Typing has no “off”: it clears itself when the next message goes or after a few seconds.

It reads these streams as they are, with no adapter:

SourcePass
OpenAI Chat Completionsopenai.chat.completions.create({ ..., stream: true })
OpenAI Responsesopenai.responses.create({ ..., stream: true })
Anthropic Messagesanthropic.messages.stream(...) or create({ ..., stream: true })
Vercel AI SDKthe streamText(...) result
OpenAI Agents SDKthe streamed run(agent, input, { stream: true }) result
Claude Agent SDKthe query(...) iterator
LangChainmodel.stream(...) or a runnable’s .stream(...)
Mastraagent.stream(...)
Anything elseany AsyncIterable<string> or ReadableStream

Each bubble is a basic message, sent exactly as written: no markdown is rendered or stripped. Ask the model for plain text, or style text yourself in a kind object with a channel block (telegram.parse_mode, imessage.styles).

await event.conversation.reply(stream, {
idempotencyKey: event.id, // bubbles get event.id:0, event.id:1, ... so retrying the reply is safe
split: true, // false: one message, cut only at the channel's length limit
onBubble: (message, i) => console.log(i, message.id),
});

Without the SDK, apply this rule to the model’s text and send each bubble as a basic message (POST /v0/conversations/{conversation_id}/basic with {"text": ...}), one after another, showing typing (POST .../typing with {}) before you start and every 4 seconds until you finish.

  1. A bubble ends at a paragraph break (a blank line), except after a lead-in line that ends with : and between items of one list, unless the bubble is already past the channel’s soft length.
  2. Past the soft length, a bubble ends at the next sentence end (. , ! , ? or … followed by a space).
  3. Never cut inside a fenced code block, unless the bubble would pass the channel’s hard limit; then cut at the last line break or space before it.
ChannelSoft lengthHard limit
Telegram900 characters4096
WhatsApp700 characters4096
iMessage400 characters9999
SMS300 characters670 (10 segments, even with an emoji)

Use the event’s ID plus the bubble’s index as each send’s Idempotency-Key (evt_...:0, evt_...:1), so retrying a failed reply never sends a bubble twice.

For tool calls, retrieval or anything slow before the first word:

const answer = await event.conversation.responding(() => agent.run(question)); // typing stays shown
await event.conversation.reply(answer);

Messages in one conversation go out one at a time, in the order they were accepted, across every endpoint, webhook replies and the stream, so bubbles never arrive out of order even when you send them quickly.