Events

Every session event, the wildcard handler with a switch-case example, and when to use specific vs wildcard subscriptions.

All events are kebab-case, matching Daily/Pipecat/Web-API convention. Subscribe via session.on(event, handler) or via the on: option on startSession / joinRoom.

Event categories

Connection lifecycle

EventHandler signatureFires when
'connected'() => voidWebRTC fully established
'disconnected'() => voidSession ended
'error'(message: string) => voidSession-level error
'state-change'(status: ConnectionStatus) => voidAny status transition
'assistant-ready'() => voidBot pipeline reports ready

Conversation lifecycle (server-emitted)

EventHandler signatureFires when
'conversation-start'() => voidUser has joined and conversation is active
'conversation-end'(reason: ConversationEndReason) => voidIdle timeout or client disconnected
'pipeline-error'(details: PipelineErrorDetails) => voidFatal error from a pipeline processor

Media

EventHandler signature
'track-started'(track: MediaStreamTrack, local: boolean) => void
'track-stopped'(track: MediaStreamTrack, local: boolean) => void
'mic-change'(enabled: boolean) => void

Speech activity (VAD — no text)

EventHandler signature
'user-speech-start'() => void
'user-speech-end'() => void
'assistant-speech-start'() => void
'assistant-speech-end'() => void

TTS lifecycle (fires in all modes, including stream)

EventHandler signature
'tts-start'() => void
'tts-chunk'(text: string) => void
'tts-end'() => void

Transcripts & telemetry

EventHandler signature
'transcript'(entry: TranscriptEntry) => void
'metrics'(data: unknown) => void

See Transcripts for the transcript entry shape and streaming semantics.

Driving your UI from state-change

idle → connecting → connected → disconnecting → disconnected
                ↘ error
session.on('state-change', (status) => {
  if (status === 'connected')    showCallUI();
  if (status === 'disconnected') showEndedScreen();
  if (status === 'error')        showErrorScreen();
});

Wildcard — every event in one handler

Subscribe to '*' to receive every event. Useful for logging, analytics, or a single dispatch table.

session.on('*', (event, ...args) => console.log(`[buddy] ${event}`, args));

Full wildcard switch-case reference

import {
  type ConnectionStatus,
  type ConversationEndReason,
  type PipelineErrorDetails,
  type TranscriptEntry
} from '@juspay/breeze-buddy-client-sdk';

session.on('*', (event, ...args) => {
  switch (event) {
    // ---------- Connection lifecycle ----------
    case 'connected':
      showCallUI();
      break;
    case 'disconnected':
      showEndedScreen();
      break;
    case 'error':
      showErrorBanner(args[0] as string);
      break;
    case 'state-change':
      updateStatusDot(args[0] as ConnectionStatus);
      break;
    case 'assistant-ready':
      console.log('assistant pipeline ready');
      break;

    // ---------- Conversation lifecycle (server-emitted) ----------
    case 'conversation-start':
      startCallTimer();
      break;
    case 'conversation-end': {
      const reason = args[0] as ConversationEndReason;
      if (reason === 'idle_timeout') showIdleTimeoutScreen();
      else showDisconnectedScreen();
      break;
    }
    case 'pipeline-error': {
      const details = args[0] as PipelineErrorDetails;
      reportToSentry(`pipeline ${details.processor}: ${details.error}`);
      break;
    }

    // ---------- Media / mic ----------
    case 'track-started': {
      const [track, local] = args as [MediaStreamTrack, boolean];
      if (!local) attachAudioVisualizer(track);
      break;
    }
    case 'track-stopped':
      // args: [track, local]
      break;
    case 'mic-change':
      setMicButtonState(args[0] as boolean);
      break;

    // ---------- Speech activity (VAD, no text) ----------
    case 'user-speech-start':
      showListeningIndicator();
      break;
    case 'user-speech-end':
      hideListeningIndicator();
      break;
    case 'assistant-speech-start':
      showSpeakingIndicator();
      break;
    case 'assistant-speech-end':
      hideSpeakingIndicator();
      break;

    // ---------- TTS lifecycle ----------
    case 'tts-start':
      // bot's TTS pipeline began emitting audio
      break;
    case 'tts-chunk':
      appendWordToBubble(args[0] as string);
      break;
    case 'tts-end':
      finalizeBubble();
      break;

    // ---------- Transcripts (discriminated union on role) ----------
    case 'transcript': {
      const entry = args[0] as TranscriptEntry;
      switch (entry.role) {
        case 'user':
          renderUserBubble(entry.id, entry.text, entry.isComplete);
          break;
        case 'assistant':
          renderAssistantBubble(entry.id, entry.text, entry.isComplete);
          break;
        case 'tool_call':
          renderToolBadge(entry.id, entry.functionName, entry.isComplete);
          break;
      }
      break;
    }

    // ---------- Telemetry ----------
    case 'metrics':
      console.debug('metrics', args[0]);
      break;
  }
});

Type casts — why?

Wildcard handlers are typed (event, ...args: unknown[]) => void — you lose per-event arg typing because one handler receives every event. Casting inside each case is the honest way to do it. If you want full type safety, subscribe to specific events (below) — each is fully typed with no casts needed.

Specific subscriptions — fully typed

For business logic, prefer specific subscriptions. TypeScript narrows the arg types for you:

session.on('transcript', (entry) => {
  // entry is TranscriptEntry — role-discriminated
  if (entry.role === 'user') renderUserBubble(entry.id, entry.text);
});

session.on('state-change', (status) => {
  // status is ConnectionStatus
});

session.on('conversation-end', (reason) => {
  // reason is 'idle_timeout' | 'client_disconnected'
});

session.on('pipeline-error', ({ processor, error }) => {
  // details destructured
});

When to use wildcard vs specific

Use caseRecommended
Debug logging of every eventWildcard — one line
Analytics (track every transition)Wildcard or 'state-change' specific
Feature logic (call UI, mic, etc.)Specific subscriptions — typed, no casts
Transcripts (any role)session.on('transcript', (entry) => ...) — branch on entry.role

Handler safety

Handler exceptions are caught and logged — one broken listener won’t stop others from firing or bring down the session. Errors go to the console with the event name.

Was this helpful?