Events
Patter emits events at key moments during a call. Register callbacks inserve() 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 astring 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 thePatter 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:
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:- A bare callable is internally mapped to
onResponse— it still works, with the same blocking semantics. - A
console.warnis emitted once per process the first time a legacy callable is registered. - Migrate by moving your function into the
onResponseslot; if your transform is per-chunk or per-sentence, switch toonChunk/onSentencefor 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-onlyconversationState 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 usesnake_case for parity with the Python SDK. Cast at the call site as needed.
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 inspectonAgentSpeechEnded.interrupted:
onUserSpeechStarted with the next onAgentSpeechEnded({ interrupted: true }):
Wiring
The realtime stream handler firesuserSpeechStarted/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 whenPATTER_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
onUserSpeechEndedvs.onUserSpeechEos: surfaced as separate events because they are two different signals.silence_gap_ms_maxwants the EOU;cross_talk_pctwants the raw VAD edge.onAgentSpeechStartedvs.onAudioOut:onAudioOutis when TTS bytes arrive in the buffer (warmup metric).onAgentSpeechStartedis when those bytes hit the carrier wire — what the user actually hears. Subtract the two to measure carrier-side jitter.- Idempotency:
onLlmTokenandonAudioOutfire at most once per turn. The guard is reset ononUserSpeechEosso the next turn re-arms cleanly.

