Skip to main content
Your server answers Retell’s requests with a hardcoded sentence after setting up the WebSocket. Now generate those answers with an LLM. Two parts need care: stream the output so the agent starts speaking sooner, and stop generating responses Retell has already discarded so you don’t pay for tokens nobody hears.

Why time to first sentence matters

Retell starts speaking as soon as it has your first complete sentence. So the caller’s wait is your time to first sentence (time to first token plus however long the model takes to finish that sentence), not the time to generate the whole response.

Build the prompt from the transcript

Retell sends the full conversation so far in transcript, as a list of utterances with role set to agent or user. Map agent to your provider’s assistant role and everything else to user. A voice prompt needs a few things a chat prompt doesn’t. Tell the model to write speech (short sentences, no markdown, no lists), because anything else gets read aloud literally. Tell it the transcript comes from speech recognition and may contain errors, so it infers meaning instead of asking the caller to repeat themselves. And give it the current date and time in the appropriate timezone. When interaction_type is reminder_required, the caller has gone quiet. Append a short instruction so the model nudges rather than answering a question nobody asked.

Stream the response

Handle discarded responses

Retell asks for a response as soon as the caller sounds finished, which keeps the silence short by starting your generation before a response is expected. The cost is that some requests get discarded: when the caller was only pausing mid-thought and keeps going, Retell drops the pending response and asks again with a higher response_id. Retell accepts content only for the response_id it most recently requested, and only until you mark that response complete. Content sent under an older response_id is dropped without an error, so a discarded response can never reach the caller. Stopping a discarded generation early is still worth doing, because it costs tokens and provider capacity for audio nobody hears. Track the newest response_id per connection and stop as soon as yours is superseded.

Use call context

Set call_details: true in your config event and Retell sends the whole call object as soon as the socket opens. It has what you need to personalize the first turn without an API round trip:
  • from_number, to_number, direction — who’s calling and which way. Phone calls only; these fields are absent on web calls.
  • retell_llm_dynamic_variables — the dynamic variables passed when the call was created, such as a customer name pulled from your CRM
  • metadata — anything you attached at call creation
  • call_id, agent_id, call_type — for your own logging and for branching on web versus phone
A common pattern: hold the begin message until call_details arrives, then greet by name. Drop the begin message from your connection setup and add this case to the message handler above.
Node.js
Because llm_websocket_url itself supports dynamic variables, you can also route by call. A URL of wss://your-domain.com/llm-websocket?tenant={{tenant_id}} reaches your server with the tenant already resolved.

Try it

Restart your server and start a call from the agent’s Test Audio panel. The agent should hold a real conversation, start speaking within a beat of you finishing, and stop cleanly when you interrupt. Check the call in Call History afterward. The transcript and the latency breakdown tell you whether your time to first sentence is where it needs to be. The llm field covers your generation including the WebSocket round trip, and llm_websocket_network_rtt isolates that round trip, so the difference is your own generation time.

FAQ

Yes, unless a newer one has already arrived. Retell waits for content until you send content_complete: true, so an ignored request leaves the agent silent. Answering with an empty string plus content_complete: true is a valid way to say nothing.
Nothing on Retell’s side; it just keeps waiting. Always send a final event with content_complete: true in a finally block, as the code above does. For provider outages, consider a fallback model or a spoken apology so the call degrades instead of going dead.
Yes, and it’s often better. Retell sends the full transcript every time, but nothing stops you from keeping your own per-call state keyed on the call ID from the URL: retrieved documents, tool results, a state machine position. Use the transcript as the source of truth for what was said, and your own state for everything else.
That’s turn-taking, which Retell controls, not your LLM. Tune responsiveness and interruption_sensitivity in interaction settings. You can also change them mid-call by sending an update_agent event, which is useful for making the agent less eager while it waits on a slow lookup.

Next step

Add function calling so the agent can book appointments, transfer to a human, and end the call.