- Your LLM’s function calling decides when to act. This is your provider’s tool-use API: you define the tools, the model picks one. Retell isn’t involved.
- Retell’s response fields carry out the actions Retell owns: ending the call, transferring it, pressing digits. You attach these to a
responseevent.
A concrete example
Bright Smile Dental runs an appointment line. The agent needs to book into the practice management system while the caller waits, hand off to the front desk when someone’s upset, and hang up cleanly when the caller says goodbye. That’s three tools:book_appointment, transfer_to_front_desk, and end_call.
Actions Retell performs for you
Set these fields on aresponse event. Retell runs the action after the content in that response has been fully spoken, so the field pairs naturally with a closing line.
End the call
The simplest useful tool. Define a function with amessage parameter so the model supplies a goodbye line. Most providers return either a tool call or text, not both, so without that parameter the agent hangs up in silence.
This replaces draftResponse from connecting your LLM. It keeps that version’s two guarantees, and both matter more now than they did before: isStale stops work on a response Retell has already discarded, and the finally block closes the response even when the model errors. Your OpenAI client and prompt constants don’t change.
temperature: 0 when the model has tools available. Lower temperature meaningfully improves how reliably the model picks the right function and formats its arguments.
Do work while the agent talks
Ending a call is easy because nothing happens afterward. Booking an appointment is the harder, more common shape: say something so the caller isn’t listening to silence, do the work, then say what happened.content_complete is what makes this work. Send the holding line with content_complete: false. The agent speaks it and Retell keeps the response open, waiting for more. Do your work, then feed the result back to the model and stream the real answer under the same response_id.
The rest of this section is Node.js, but the sequence of events is the same in any language.
Declare the tool alongside end_call. Give it a message parameter for the holding line, the same way end_call carries the goodbye:
llm.ts
llm.ts
end_call branch in draftResponse:
llm.ts
content_complete: true, so the only thing that can cut your work short is the caller speaking.
Transfer and press digits
Both areresponse fields, so they follow the same pattern as end_call: the agent finishes speaking, then Retell acts.
Record tool calls in the transcript
Retell doesn’t see your tool calls, so by default they’re invisible in the call record. Sendtool_call_invocation and tool_call_result events and Retell weaves them into the transcript at the exact word where they happened.
That pays off in two places:
- After the call,
transcript_with_tool_callsin the Get Call API response shows what the agent did and when, not just what it said. This is what makes a custom LLM call debuggable. - During the call, if you also set
transcript_with_tool_calls: truein yourconfigevent, Retell includes the woven transcript in everyupdate_onlyandresponse_requiredevent. Retell then keeps that history for you, though you still map it into your provider’s message format yourself.
tool_call_id for both:
arguments is a stringified JSON object, and content is a plain string, so stringify structured results yourself. successful is optional and marks the outcome in the transcript. Leaving it out doesn’t mean the call failed, it means you didn’t say, so set it explicitly when you care about telling a failed tool call from a successful one after the fact.
Going to production
The examples above are deliberately minimal. A few things they don’t handle that a live agent needs:- Don’t double-book. This happens on ordinary calls: Retell asks for a response, discards it when the caller keeps talking, and asks again, so your tool runs twice for a single request. An idempotency key is the only real fix. The
isStale()check above helps only when the newer request has already arrived, and a fast tool commits its write a second or two before that happens. Derive the key from the call ID plus the tool arguments, which are usually identical across the duplicate calls, and make the repeat a no-op. - Track state, don’t rely on the prompt. Once past two or three tools, a state machine that controls which tools and prompt are active at each step beats hoping the model reads the transcript correctly. See best practices.
- Report failures out loud. When a tool errors, send the failure back to the model as a tool result, and mark it with
successful: false, so the agent offers an alternative instead of going quiet.
FAQ
Why does the agent hang up without saying goodbye?
Why does the agent hang up without saying goodbye?
You sent
end_call: true with empty content, or the model returned a tool call with no message parameter to speak. Retell ends the call as soon as the content in that response finishes, and empty content finishes immediately. Give the tool a message parameter and put it in content.Why was my end_call or transfer ignored?
Why was my end_call or transfer ignored?
Most likely the caller interrupted before the agent finished speaking, which discards the pending action. Otherwise, check that you didn’t set both
end_call and transfer_number on the same response; only end_call runs. Setting no_interruption_allowed: true on the closing line prevents the interruption case.My tool ran twice for one caller request. Why?
My tool ran twice for one caller request. Why?
Retell asked for a response, discarded it when the caller kept talking, and asked again with a new
response_id. Your code ran the tool on both, usually with identical arguments. This is the most common way a custom LLM agent double-books, and no setting turns it off. Make the write idempotent, keyed on the call ID plus the arguments. Checking response_id first helps, but only when the newer request has already arrived.Do I have to send tool_call_invocation and tool_call_result?
Do I have to send tool_call_invocation and tool_call_result?
No, they’re optional bookkeeping. Skip them and your tools still run; they just won’t appear in the call transcript, which makes debugging a failed call much harder. Send them.
Next steps
- Best practices for keeping latency low once tools are in the loop
- Troubleshooting when the agent goes silent or the call drops
- LLM WebSocket reference for every field on every event

