Skip to main content

Events

Patter emits events at key moments during a call. Register callbacks in serve() to react to call starts, ends, transcripts, and messages.

Callback Overview

For fine-grained pipeline observability see the Speech-edge events and Tool events via onTranscript sections below — they complement the lifecycle callbacks rather than replacing them. For mutating prompts and responses (RAG augmentation, output validation, PII redaction) use PipelineHooks — they sit inside the LLM step rather than firing alongside it.

onCallStart

Fires when a new call connects. Use it for logging, CRM lookups, or initializing per-call state.

Payload

custom_params contains key-value pairs passed via TwiML <Parameter> elements. This is only present for Twilio calls that include custom parameters. For Telnyx calls, this field is omitted.

onCallEnd

Fires when a call disconnects. Includes the full conversation transcript.

Payload

onCallEnd is guaranteed to fire exactly once per call, even if the WebSocket disconnects unexpectedly.

onTranscript

Fires each time a transcript segment is generated during the call. Useful for real-time dashboards, live monitoring, or logging.

Payload

onMessage (Pipeline Mode)

In pipeline mode, onMessage is the core callback. It receives the user’s transcript and conversation history, and must return the text to be spoken by the TTS engine.

Payload

Return Value

The function must return a string that will be converted to speech. If you return an empty string, nothing is spoken.

onMetrics

Fires after each conversational turn completes. Use it for real-time latency monitoring, cost tracking, or per-turn analytics.

Payload

Speech-edge events

For turn-taking, TTFT measurement, and barge-in / interrupt observability, set the speech-edge callbacks directly on the Patter instance. They proxy to a per-process SpeechEvents dispatcher and fire from any in-flight call.
Callbacks are async. Throwing inside a callback logs the error but does not interrupt the call.

Tool events via onTranscript

Tool invocations (including the built-in transfer_call and end_call) surface through the same onTranscript callback you pass to phone.serve(...). Filter on role === "tool" to handle them:
The event payload for tool calls carries:

Pipeline Hooks (afterLlm)

PipelineHooks lets you intercept LLM output before it reaches the TTS engine. Pass it via phone.agent({ hooks: { afterLlm: ... } }). The new 3-tier API exposes three callbacks tuned to different latency budgets — pick the one that matches your work.

Tier table

HookContext carries callId, caller, callee, and history. Hooks run in pipeline mode only — engines (OpenAIRealtime, ElevenLabsConvAI) bundle the LLM step internally. PipelineHooks also exposes beforeStt / afterStt and beforeTts / afterTts for audio-stage interception.

Migration from the legacy callable

The legacy single-callable form is deprecated and will be removed in v0.7.0:
Behavior of the legacy form during the deprecation window:
  • A bare callable is internally mapped to onResponse — it still works, with the same blocking semantics.
  • A console.warn is emitted once per process the first time a legacy callable is registered.
  • Migrate by moving your function into the onResponse slot; if your transform is per-chunk or per-sentence, switch to onChunk / onSentence for a latency win.

Type Signatures

Combining Callbacks

You can use all callbacks together:

Speech-Edge Events (Turn-Taking)

The callbacks above describe the transcript-level lifecycle of a call. For turn-taking instrumentation — barge-in, end-of-utterance, time-to-first-token, TTS warmup vs. wire-time — Patter exposes seven additional async callbacks plus a read-only conversationState snapshot directly on the Patter instance. These events expose the canonical voice-agent metric set (user/agent state transitions, turn boundaries, TTFT, audio first-byte) and align with OpenAI Realtime (input_audio_buffer.speech_started/_stopped/_committed) so downstream metrics work without translation.
Every callback defaults to null. Existing code that does not register any speech-edge callback sees exactly the previous behaviour and zero overhead. The state machine is updated regardless of whether callbacks are registered, so conversationState is always usable.

The seven events

Payload signature matrix

Payload field names use snake_case for parity with the Python SDK. Cast at the call site as needed.
Compute end-to-end latency by anchoring eos_to_first_token_ms to onUserSpeechEos. It marks the moment the SDK has committed that the user is done speaking — VAD trailing edge plus trailing silence (and optionally a semantic turn-detector agreement). Anchoring to onUserSpeechEnded instead would over-count by the silence window and double-fire on mid-utterance VAD blips. Hamming AI thresholds: <800 ms good, >1500 ms critical.

State machine

conversationState returns a snapshot { user, agent } you can read at any time: A monotonic turnIdx counter (also exposed on the dispatcher) increments on every committed EOU. The agentSpeech*, llmToken, and audioOut payloads all carry the current turn_idx so a per-turn metric can correlate them.

Sequence for a normal turn

Sequence for a barged-in turn

Full example — wire all seven callbacks

Barge-in detection

The cleanest way to detect a barge-in is to inspect onAgentSpeechEnded.interrupted:
For barge-in latency (how fast the agent stopped after the user started talking), pair onUserSpeechStarted with the next onAgentSpeechEnded({ interrupted: true }):

Wiring

The realtime stream handler fires userSpeechStarted/Ended/Eos and agentSpeechStarted/Ended automatically on the OpenAI Realtime + Twilio/Telnyx path — no extra setup required. onLlmToken and onAudioOut are exposed on the dispatcher (phone.speechEvents) so custom adapters and pipeline-mode integrations can call them. If you are building a custom provider, call phone.speechEvents.fireLlmFirstToken({...}) on your first streamed chunk and phone.speechEvents.fireAudioOut({...}) on your first synthesized audio buffer; both are idempotent within a turn.

Public exports

OpenTelemetry attach contract

Every speech-edge event also records a span event on the active call span when PATTER_OTEL_ENABLED=1 and the optional @opentelemetry/api peer dep is installed. When OTel is missing or disabled, the OTel branch is a zero-cost no-op — there is no overhead and no failure. See Tracing for the OTel installation and exporter setup.

Callback safety

Observer exceptions are caught and logged, never propagated to the live call. A misbehaving callback cannot crash the call or break audio. Errors are logged at WARN level via the SDK logger with the offending span event name for easy correlation.

Design notes

  • onUserSpeechEnded vs. onUserSpeechEos: surfaced as separate events because they are two different signals. silence_gap_ms_max wants the EOU; cross_talk_pct wants the raw VAD edge.
  • onAgentSpeechStarted vs. onAudioOut: onAudioOut is when TTS bytes arrive in the buffer (warmup metric). onAgentSpeechStarted is when those bytes hit the carrier wire — what the user actually hears. Subtract the two to measure carrier-side jitter.
  • Idempotency: onLlmToken and onAudioOut fire at most once per turn. The guard is reset on onUserSpeechEos so the next turn re-arms cleanly.