Events
Patter emits events at key moments during a call. Register callbacks inserve() to react to call starts, ends, transcripts, and messages.
Callback Overview
| Callback | Trigger | Available In |
|---|---|---|
onCallStart | When a call connects | serve() |
onCallEnd | When a call disconnects | serve() |
onTranscript | Each time a transcript segment arrives | serve() |
onMessage | User transcript ready for response (pipeline mode) | serve() |
onMetrics | After each conversational turn completes | serve() |
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.
| Attribute | Fires |
|---|---|
onUserSpeechStarted | Raw VAD positive edge (caller begins speaking). |
onUserSpeechEnded | Raw VAD trailing edge (caller stops speaking). |
onUserSpeechEos | Committed end-of-utterance — anchor TTFT here. |
onAgentSpeechStarted | First wire-time agent audio chunk — turn-start marker for the caller. |
onAgentSpeechEnded | Last agent audio chunk. Payload includes interrupted flag for barge-in. |
onLlmToken | First LLM token of the turn — TTFT marker. |
onAudioOut | First TTS audio bytes produced — TTS warmup signal. |
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:
| Key | Type | Notes |
|---|---|---|
role | "tool" | Always "tool" for tool events. |
tool_name | string | The tool that was dispatched. |
tool_args | Record<string, unknown> | Arguments emitted by the LLM. |
tool_result | string | null | Result returned by the tool handler (truncated for log readability). |
call_id | string | The active call ID. |
text | string | Pre-formatted tool_name(args) → result string. |
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
| Tier | Sync / Async | Latency budget | When to use | Return semantics |
|---|---|---|---|---|
onChunk | sync | ~0 ms (per token chunk) | Fast text rewrites: filter filler words, normalize whitespace. Runs on every streaming chunk before it is appended to the buffer. | Returns the rewritten chunk. Streaming continues immediately. |
onSentence | async | 50–300 ms (per sentence) | Per-sentence transformations that need a small amount of I/O: PII redaction, profanity replacement, lightweight enrichment. Runs once per detected sentence boundary. | Returns the rewritten sentence. Sentence is held until the promise resolves; subsequent sentences continue to stream. |
onResponse | async | 500 ms – 2 s (per full response) | Whole-response validation that must complete before audio plays: JSON schema checks, full-response moderation, summary substitution. Blocks streaming — TTS cannot start until this resolves. | Returns the rewritten full response (or rejects to abort). |
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
| Event | Fires on | Signal |
|---|---|---|
onUserSpeechStarted | VAD positive edge of inbound audio | Raw VAD start — not end-of-utterance. Use for cross-talk detection. |
onUserSpeechEnded | VAD trailing edge | Raw VAD stop — not committed EOU. Use for talk-ratio. |
onUserSpeechEos | Committed end-of-utterance | Canonical “user finished” signal. Anchor eos_to_first_token_ms here. |
onAgentSpeechStarted | First wire-time chunk of the agent turn | What the user actually hears (distinct from TTS warmup). Anchor barge-in latency here. |
onAgentSpeechEnded | Last wire chunk of the agent turn | Payload includes interrupted: boolean. true = barge-in cancelled the turn. |
onLlmToken | First LLM token of the turn | TTFT marker. Idempotent — fires once per turn. |
onAudioOut | First TTS audio chunk produced | TTS warmup arrival (distinct from wire-time). Idempotent — fires once per turn. |
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:
| Side | States | Initial | Set by |
|---|---|---|---|
user | listening · speaking · thinking · away | listening | onUserSpeechStarted → speaking, onUserSpeechEnded / onUserSpeechEos → listening |
agent | initializing · idle · listening · thinking · speaking | initializing | call accepted → idle, EOU committed → thinking, onAgentSpeechStarted → speaking, onAgentSpeechEnded → idle |
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
| Export | Type | Use |
|---|---|---|
SpeechEvents | class | The dispatcher. One instance per Patter (auto-created). |
SpeechEventCallback | type | (payload: Readonly<Record<string, unknown>>) => void | Promise<void>. |
ConversationStateSnapshot | interface | { readonly user: UserState; readonly agent: AgentState }. |
UserState | type | "listening" | "speaking" | "thinking" | "away". |
AgentState | type | "initializing" | "idle" | "listening" | "thinking" | "speaking". |
EouTrigger | type | "vad_silence" | "semantic_turn_detector" | "manual_commit". |
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.
| Callback | Span event name | Selected attributes |
|---|---|---|
onUserSpeechStarted | patter.event.user_speech_started | patter.audio.offset_ms, patter.vad.confidence |
onUserSpeechEnded | patter.event.user_speech_ended | patter.speech.duration_ms |
onUserSpeechEos | patter.event.user_speech_eos | patter.eos.trigger, patter.eos.trailing_silence_ms |
onAgentSpeechStarted | patter.event.agent_speech_started | patter.turn.idx, patter.tts.provider, patter.engine |
onAgentSpeechEnded | patter.event.agent_speech_ended | patter.turn.idx, patter.speech.duration_ms, patter.turn.interrupted |
onLlmToken | patter.event.llm_first_token | gen_ai.request.model, gen_ai.provider.name (per OTel GenAI semconv), patter.turn.idx |
onAudioOut | patter.event.tts_first_audio | patter.turn.idx, patter.tts.provider |
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.

