Skip to main content
Callers want to book something, reach a person, or hear an answer from your database. Function calling is how the model asks your code to do that work. Keep two layers separate:
  • 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 response event.
Everything else, like querying your database or hitting your booking API, is just code you run.

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 a response 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_call, transfer_number, and digit_to_press are mutually exclusive. Retell runs at most one per response_id, and end_call takes precedence, then transfer_number. Set only the one you want.

End the call

The simplest useful tool. Define a function with a message 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.
Set 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
Your booking function should return a failure as data rather than throwing. The model can offer another slot if it’s told the slot was taken; it can’t do anything with an exception.
llm.ts
Then handle the tool call next to the end_call branch in draftResponse:
llm.ts
While a response is open, Retell waits rather than filling the gap itself. It won’t ask for a reminder or a new response until you send content_complete: true, so the only thing that can cut your work short is the caller speaking.

Transfer and press digits

Both are response fields, so they follow the same pattern as end_call: the agent finishes speaking, then Retell acts.
Transfers initiated this way are cold transfers: the call is handed off and your agent drops out. For warm transfers or transfer logic on your own telephony, run it from your server.

Record tool calls in the transcript

Retell doesn’t see your tool calls, so by default they’re invisible in the call record. Send tool_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_calls in 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: true in your config event, Retell includes the woven transcript in every update_only and response_required event. Retell then keeps that history for you, though you still map it into your provider’s message format yourself.
Send the invocation as the tool starts and the result when it returns, using the same 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

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.
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.
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.
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