With a custom LLM, Tried does not execute tools for you. The Functions section of the agent builder is hidden for custom LLM agents, and tools defined on an LLM or flow are not sent to your server. Your server decides when to call a tool, calls it, and decides what the agent says.
Tried gives you three things to work with:
- Built-in call actions on a
responseoragent_interruptframe:end_call,transfer_number(withshow_transferee_as_caller) anddigit_to_press. - Tool call events (
tool_call_invocationandtool_call_result) that record your server's tool calls in the call transcript. agent_interruptto speak when a slow tool finishes, without waiting for the caller.
Built-in call actions#
These replace the end_call, transfer_call and press_digit tools that dashboard agents use.
| Action | Frame field | What the worker does |
|---|---|---|
| Hang up | end_call: true |
Speaks the response, then ends the call with reason agent_hangup. |
| Transfer | transfer_number: "+61290000000" |
Speaks the response, then performs a cold transfer by SIP INVITE to the number. |
| Show caller ID to the target | show_transferee_as_caller: true |
With transfer_number, the transfer target sees the caller's number. |
| Press keypad digits | digit_to_press: "2" |
Waits for the speech to finish, then sends the digits as DTMF. |
Example: confirm and transfer in one response.
{
"response_type": "response",
"response_id": 7,
"content": "Let me put you through to accounts.",
"content_complete": true,
"transfer_number": "+61290000000"
}Only cold transfers by number are available from a custom LLM. Warm transfers, transfer agents, SMS and the other dashboard tool types are not reachable through the protocol. If you need them, run that logic in your own telephony integration or use a dashboard agent.
Transfers and keypad digits need a real phone call. A web call has no SIP leg to transfer.
Running your own tools#
A typical turn with a tool looks like this:
Receive the request#
response_required arrives with response_id: 4 and the transcript.
Say a filler line (optional)#
Stream a short response chunk with content_complete: false, such as "One moment while I check." The caller hears it while your tool runs.
Record the invocation#
Send tool_call_invocation with an id, the tool name and the JSON-encoded arguments.
Call the tool#
Call your API, database or model with tools.
Record the result#
Send tool_call_result with the same tool_call_id.
Finish the response#
Stream the rest of the answer for response_id: 4 and set content_complete: true.
{ "response_type": "response", "response_id": 4, "content": "One moment while I check.", "content_complete": false }
{ "response_type": "tool_call_invocation", "tool_call_id": "call_lookup_1", "name": "lookup_booking", "arguments": "{\"phone\":\"+61411111111\"}" }
{ "response_type": "tool_call_result", "tool_call_id": "call_lookup_1", "content": "{\"date\":\"Tuesday\",\"time\":\"14:00\"}" }
{ "response_type": "response", "response_id": 4, "content": " You're booked for Tuesday at 2pm.", "content_complete": true }Keep the gap between response frames under 30 seconds. If no frame of a requested response arrives for 30 seconds, the call ends with error_llm_websocket_runtime.
What the tool events do#
tool_call_invocation and tool_call_result are recorded as transcript items on the call, in transcript_with_tool_calls:
[
{ "role": "user", "content": "When is my appointment?" },
{ "role": "tool_call_invocation", "tool_call_id": "call_lookup_1", "name": "lookup_booking", "arguments": "{\"phone\":\"+61411111111\"}" },
{ "role": "tool_call_result", "tool_call_id": "call_lookup_1", "content": "{\"date\":\"Tuesday\",\"time\":\"14:00\"}" },
{ "role": "agent", "content": "One moment while I check. You're booked for Tuesday at 2pm." }
]They show in the call's transcript in Call History and are part of the call object returned by the API. They do not change what the agent says, and they are not sent back to your server in later requests (the worker sends transcript only, with agent and user items).
Send them only if you want the record. A server that never sends them works the same way.
Slow tools and agent_interrupt#
If a tool takes longer than a sentence or two, finish the current response with a holding line, then speak the result with agent_interrupt when it is ready:
{ "response_type": "response", "response_id": 9, "content": "I'm checking the warehouse system, this can take a moment.", "content_complete": true }Later:
{
"response_type": "agent_interrupt",
"interrupt_id": 1,
"content": "Thanks for waiting. Your order left the warehouse this morning.",
"content_complete": true
}agent_interrupt text is spoken as soon as it is complete, and its end_call, transfer_number and digit_to_press flags are applied after it. Use a new interrupt_id for each interrupt.
If the caller speaks while you wait, a new response_required arrives. Decide whether to answer it, or to answer it and still send the interrupt later.
Using call details in tools#
Turn on call_details in your config frame to receive the call object at connect time. Use from_number, to_number, metadata, retell_llm_dynamic_variables and collected_dynamic_variables as tool inputs (for example, look up the caller by from_number). See WebSocket server.
Gotchas#
- No tool definitions are sent to you. Define tools in your own model call.
- Every call action is applied after speech. If you need to hang up without saying anything, send
content: ""withcontent_complete: trueandend_call: true. transfer_numbertakes priority. If a response sets bothtransfer_numberandend_call, the call is transferred, not ended.- Stale ids are dropped. If the caller interrupts while your tool runs, frames for the old
response_idare discarded, including the final answer. Answer the newest request, or useagent_interruptfor results that should be spoken anyway. metadataframes are not stored. Use tool events or your own database to keep per-call data.