Reference

RTVI events

Full catalog of events emitted by a Breeze Buddy Daily session, plus client → server messages like tts-speak.

Breeze Buddy streams the entire session — transport state, user speech, bot speech, LLM tokens, function calls, pipeline metrics — to the browser over the Pipecat RTVI protocol. The backend uses Pipecat’s RTVIObserver + RTVIProcessor, so every standard RTVI event reaches the client, and Breeze Buddy adds four custom server messages on top.

Enabling events

RTVI events are gated by a single environment flag:

.env
bash
ENABLE_BREEZE_BUDDY_DAILY_EVENTS=true

Events dispatch automatically for every Daily session (both DAILY agent mode and DAILY_STREAM stream mode). Register listeners via the callbacks object on PipecatClient — no per-event opt-in is required.

setup.ts
typescript
import { PipecatClient } from '@pipecat-ai/client-js';
import { DailyTransport } from '@pipecat-ai/daily-transport';
//
const client = new PipecatClient({
  transport: new DailyTransport(),
  callbacks: { onBotConnected: () => console.log('bot connected') }
});
await client.initDevices();
await client.startBotAndConnect({
  endpoint: `${baseUrl}/agent/voice/breeze-buddy/connect`,
  headers: new Headers({ Authorization: `Bearer ${apiToken}` }),
  requestData: { lead_id: leadId }
});

Event binding styles

The examples below use the client.on(event, handler) emitter style for brevity. In production code you’ll usually register all callbacks via the callbacks: { onXxx: ... } object on the constructor (see the Web SDK setup guide). Both styles work and can be mixed.

Mode availability at a glance

Two execution modes serve a Daily session; not every event fires in both.

Event familyAgent mode (DAILY / DAILY_TEST)Stream mode (DAILY_STREAM)
Connection, transport
User speech + transcription
Bot TTS (onBotTts*, onBotStartedSpeaking)
Bot LLM (onBotLlm*)— (no LLM in stream mode)
Function calls (onLLMFunctionCall*)
Custom server messages (bot-ready, conversation-start/end, pipeline-error)

Connection & transport

EventWhen it firesPayload
onConnectedClient connects to the Daily room.
onDisconnectedTransport tears down — clean disconnect or network drop.
onTransportStateChangedTransport moves through authenticating → connecting → connected → disconnected.{ state: TransportState }
onErrorAny unrecoverable RTVI-level error.{ type: string, message: string }
onMessageErrorServer rejected a client message.{ type: string, data?: any }

Participants

EventWhen it firesPayload
onParticipantJoinedA participant (user or bot) joins the room.{ id, name, local }
onParticipantLeftA participant leaves the room.{ id, reason? }

Bot lifecycle

EventWhen it firesPayload
onBotStartedBackend bot subprocess launched.
onBotConnectedBot has joined the Daily room.{ id }
onBotReadyPipeline is fully assembled and ready to process audio. This is the “start talking” signal.{ version, about }
onBotDisconnectedBot left the room (call ended, idle timeout, or crash).{ id, reason? }

What to gate on `onBotReady`

Wait for onBotReady before showing the “speak now” UI. Microphone capture starts on connect, but transcription won’t route to an active pipeline until ready.

User speech

EventWhen it firesPayload
onUserStartedSpeakingVAD detects the user started speaking.
onUserStoppedSpeakingVAD detects the user stopped.
onUserTranscriptSTT emits interim (partial) or final transcription. Check data.final.{ text: string, final: boolean, timestamp: number, user_id: string }
onUserMuteStartedUser muted their microphone client-side.
onUserMuteStoppedUser unmuted.
transcription.ts
typescript
let interim = '';
client.on('userTranscript', (d) => {
  if (d.final) {
    appendToTranscript('user', d.text);
    interim = '';
  } else {
    interim = d.text;
    renderInterim(interim);
  }
});

Bot speech (TTS)

EventWhen it firesPayload
onBotStartedSpeakingBot audio begins playing in the room.
onBotStoppedSpeakingBot audio ended.
onBotTtsStartedTTS engine began synthesizing a new utterance.
onBotTtsTextTTS text chunk — what the bot is actually saying. Emitted as text streams through TTS.{ text: string }
onBotTtsStoppedTTS finished synthesizing this utterance.
onBotOutputAggregated bot text for an entire utterance, with spoken flag.{ text: string, spoken: boolean }
bot-speech.ts
typescript
client.on('botTtsText', (d) => appendToStream('bot', d.text));
client.on('botStoppedSpeaking', () => finalizeStream('bot'));

Bot LLM (agent mode only)

EventWhen it firesPayload
onBotLlmStartedLLM request sent.
onBotLlmTextLLM streamed a token.{ text: string }
onBotLlmStoppedLLM finished this response.
onBotLlmSearchResponseLLM tool-call response (grounded search).{ response: any }

Function calls (agent mode only)

EventWhen it firesPayload
onLLMFunctionCallStartedLLM decided to call a template function.{ function_name: string }
onLLMFunctionCallInProgressArguments being extracted.{ function_name: string, arguments: object }
onLLMFunctionCallStoppedFunction call complete; transition (if any) applied.{ function_name: string, arguments: object, transition_to: string \| null }
function-calls.ts
typescript
client.on('llmFunctionCallStopped', (data) => {
  if (data.function_name === 'appointment_confirmed') {
    showConfirmation(data.arguments.date, data.arguments.time);
  }
});

Audio levels & metrics

EventWhen it firesPayload
onLocalAudioLevelLocal mic volume sample.{ level: number } (0–1)
onRemoteAudioLevelRemote audio volume sample.{ participant_id, level }
onMetricsPipeline-level metrics after each turn.{ stt_latency_ms, llm_ttfb_ms, tts_ttfb_ms, total_latency_ms, llm_tokens? }

Device management

Standard Daily device events reach the client unchanged: onAvailableMicsUpdated, onAvailableSpeakersUpdated, onAvailableCamsUpdated, onMicUpdated, onSpeakerUpdated, onCamUpdated, onDeviceError, onTrackStarted, onTrackStopped. Use them to build device pickers and mic-level visualizers; see the Daily SDK docs for payloads.

Custom server messages

Breeze Buddy sends four application-level messages on top of RTVI. They arrive as serverMessage events with a typed type field.

Message typeWhen it firesPayload
bot-readyPipeline assembly finished and client-ready acknowledged. (Parallel to onBotReady; useful if you want to read extra metadata.){ version?, about? }
conversation-startClient connected and the conversation context is active.{}
conversation-endSession concluded.{ reason: "client_disconnected" \| "idle_timeout" }
pipeline-errorA pipeline processor failed irrecoverably.{ processor: string, error: string }
server-messages.ts
typescript
client.on('serverMessage', (msg) => {
  switch (msg.type) {
    case 'conversation-end':
      toast(`Call ended: ${msg.data.reason}`);
      break;
    case 'pipeline-error':
      reportError(msg.data.processor, msg.data.error);
      break;
  }
});

Client-to-server messages

The client can push messages back to the bot via client.sendClientMessage(type, data). Today one type is wired end-to-end: tts-speak, available in DAILY_STREAM mode.

tts-speak — client-driven bot utterance

In stream mode (no LLM, no template flow), the client decides what the bot says. Send a tts-speak message and the backend queues a TTSSpeakFrame into the pipeline; it plays through the normal TTS → transport path and fires onBotTts* / onBotStartedSpeaking events.

tts-speak.ts
typescript
// Stream mode only — requires execution_mode: "DAILY_STREAM" on the lead
await client.sendClientMessage('tts-speak', {
  text: 'Hello! How can I help you today?'
});
FieldTypeRequiredNotes
typestringYesMust be exactly "tts-speak".
data.textstringYesUtterance to speak. Empty or non-string values are silently ignored.

Behaviour

  • Mode gate: only handled when the lead’s execution_mode is DAILY_STREAM. Full agent modes do not expose this path.
  • Queued, not interrupting: the utterance appends to the FIFO frame queue — if the bot is already speaking, the new text plays after the current utterance finishes. For barge-in style interruption, interrupt the bot via the user speaking (VAD will handle it).
  • Max length: text over 2000 characters is silently truncated.
  • Auth: no per-message auth — any client connected to the room can trigger tts-speak. Treat room access as the security boundary.
  • Observability: each utterance fires the normal onBotTtsStarted, onBotTtsText, onBotTtsStopped, onBotStartedSpeaking, onBotStoppedSpeaking, and onBotOutput events — you can reflect spoken text in your UI from the same listeners you already have.

When to reach for stream mode

Use DAILY_STREAM when your app already decides what to say (e.g. a custom agent, a scripted demo, a human-in-the-loop console) and you just need Breeze Buddy to run STT + TTS + transcription capture. For LLM-driven conversations, use DAILY with a template.

Error handling

Pipeline and transport errors arrive on two channels — choose based on which you want to surface.

Source eventWhat triggers itRecommended action
onErrorRTVI-level error (usually transport).Show retry UI; call client.disconnect() and reconnect.
onMessageErrorServer rejected a client message.Log; no user-facing action unless you sent the message.
serverMessage { type: 'pipeline-error' }A processor inside the pipeline failed.Report and reconnect.
errors.ts
typescript
client.on('error', (e) => {
  console.error('[rtvi]', e.type, e.message);
  if (e.type === 'connection-failed') showRetry();
});
client.on('serverMessage', (m) => {
  if (m.type === 'pipeline-error') {
    reportError(m.data.processor, m.data.error);
  }
});

Patterns

state-machine.ts
typescript
type State = 'idle' | 'listening' | 'thinking' | 'speaking';
let state: State = 'idle';
//
client.on('userStartedSpeaking', () => (state = 'listening'));
client.on('userTranscript', (d) => { if (d.final) state = 'thinking'; });
client.on('botStartedSpeaking', () => (state = 'speaking'));
client.on('botStoppedSpeaking', () => (state = 'idle'));
//
client.on('metrics', (m) => trackLatency(m.total_latency_ms));

Clean up listeners

Always call client.off(name, handler) (or client.disconnect(), which tears down all listeners) on component unmount. Leaks here turn into phantom updates on stale components.

Was this helpful?