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

CallbackTriggerAvailable In
onCallStartWhen a call connectsserve()
onCallEndWhen a call disconnectsserve()
onTranscriptEach time a transcript segment arrivesserve()
onMessageUser transcript ready for response (pipeline mode)serve()
onMetricsAfter each conversational turn completesserve()
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.
AttributeFires
onUserSpeechStartedRaw VAD positive edge (caller begins speaking).
onUserSpeechEndedRaw VAD trailing edge (caller stops speaking).
onUserSpeechEosCommitted end-of-utterance — anchor TTFT here.
onAgentSpeechStartedFirst wire-time agent audio chunk — turn-start marker for the caller.
onAgentSpeechEndedLast agent audio chunk. Payload includes interrupted flag for barge-in.
onLlmTokenFirst LLM token of the turn — TTFT marker.
onAudioOutFirst TTS audio bytes produced — TTS warmup signal.
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:
KeyTypeNotes
role"tool"Always "tool" for tool events.
tool_namestringThe tool that was dispatched.
tool_argsRecord<string, unknown>Arguments emitted by the LLM.
tool_resultstring | nullResult returned by the tool handler (truncated for log readability).
call_idstringThe active call ID.
textstringPre-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

TierSync / AsyncLatency budgetWhen to useReturn semantics
onChunksync~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.
onSentenceasync50–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.
onResponseasync500 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:
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

EventFires onSignal
onUserSpeechStartedVAD positive edge of inbound audioRaw VAD start — not end-of-utterance. Use for cross-talk detection.
onUserSpeechEndedVAD trailing edgeRaw VAD stop — not committed EOU. Use for talk-ratio.
onUserSpeechEosCommitted end-of-utteranceCanonical “user finished” signal. Anchor eos_to_first_token_ms here.
onAgentSpeechStartedFirst wire-time chunk of the agent turnWhat the user actually hears (distinct from TTS warmup). Anchor barge-in latency here.
onAgentSpeechEndedLast wire chunk of the agent turnPayload includes interrupted: boolean. true = barge-in cancelled the turn.
onLlmTokenFirst LLM token of the turnTTFT marker. Idempotent — fires once per turn.
onAudioOutFirst TTS audio chunk producedTTS warmup arrival (distinct from wire-time). Idempotent — fires once per turn.

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:
SideStatesInitialSet by
userlistening · speaking · thinking · awaylisteningonUserSpeechStartedspeaking, onUserSpeechEnded / onUserSpeechEoslistening
agentinitializing · idle · listening · thinking · speakinginitializingcall accepted → idle, EOU committed → thinking, onAgentSpeechStartedspeaking, onAgentSpeechEndedidle
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

ExportTypeUse
SpeechEventsclassThe dispatcher. One instance per Patter (auto-created).
SpeechEventCallbacktype(payload: Readonly<Record<string, unknown>>) => void | Promise<void>.
ConversationStateSnapshotinterface{ readonly user: UserState; readonly agent: AgentState }.
UserStatetype"listening" | "speaking" | "thinking" | "away".
AgentStatetype"initializing" | "idle" | "listening" | "thinking" | "speaking".
EouTriggertype"vad_silence" | "semantic_turn_detector" | "manual_commit".

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.
CallbackSpan event nameSelected attributes
onUserSpeechStartedpatter.event.user_speech_startedpatter.audio.offset_ms, patter.vad.confidence
onUserSpeechEndedpatter.event.user_speech_endedpatter.speech.duration_ms
onUserSpeechEospatter.event.user_speech_eospatter.eos.trigger, patter.eos.trailing_silence_ms
onAgentSpeechStartedpatter.event.agent_speech_startedpatter.turn.idx, patter.tts.provider, patter.engine
onAgentSpeechEndedpatter.event.agent_speech_endedpatter.turn.idx, patter.speech.duration_ms, patter.turn.interrupted
onLlmTokenpatter.event.llm_first_tokengen_ai.request.model, gen_ai.provider.name (per OTel GenAI semconv), patter.turn.idx
onAudioOutpatter.event.tts_first_audiopatter.turn.idx, patter.tts.provider
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.