Tutorial: Tool return values
Level 2 – Lesson 3 of 8 – This lesson covers the different values a tool can return and when to use each one. Understanding return values gives you precise control over what the agent says and when.
Choosing a return type
The two valid return types
Functions called by the LLM can only return a string or a dictionary. Any other type – integer, list, boolean – will cause an error. The LLM will retry up to three times before giving up and producing an error response.
Always return a string or dictionary from functions called by the LLM. If your value is a number, wrap it: return str(count) or return f"There are {count} items.".
Returning a string
The simplest and most common pattern. Return a string and the LLM reads it, considers it alongside the conversation history, and formulates its own response.
The string is inserted into conversation history under the function role. The LLM then produces a natural response based on it.
Trade-off: The LLM has flexibility to express the result conversationally – but you do not control the exact phrasing.
Returning a dictionary
Dictionaries give you more specific control. The following keys are supported individually or in combination:
content
Equivalent to returning a string. The value is shown to the LLM to inform its next response.
utterance
A hard-coded response that bypasses the LLM entirely. The text is spoken directly to the user without any further LLM request.
Trade-off: You get full control over phrasing and only one LLM request (lower latency). But you must manually handle all variations – zero stock, one item, multiple items, and so on.
Return string (LLM generates response)
Return utterance (bypasses LLM)
The LLM adds empathy, context, and a follow-up offer.
Check your understanding
When should you use utterance instead of returning a plain string?
When should you use utterance instead of returning a plain string?
- When you want the LLM to have more creative freedom in its response
- When you need exact control over the phrasing, or want to save an LLM request
- When the function result contains sensitive data
- When the user asks a yes/no question
Answer: When you need exact control over the phrasing, or want to save an LLM request
Use utterance when you need to control exactly what the agent says – for example, legal disclaimers, hard-coded confirmations, or latency-critical responses where skipping the second LLM request matters.
content + utterance together
When both keys are returned, the utterance is played immediately to the user, and the content is stored in conversation history to inform the LLM’s response on the next turn.
Use this pattern when you want to:
- play a hard-coded holding phrase while the agent processes something
- give the LLM context for how to handle the follow-up
end_turn: False
By default, returning an utterance ends the turn. Setting end_turn to False plays the utterance but then immediately triggers another LLM request in the same turn.
This is useful for latency optimization – play a filler phrase while the LLM decides on its next action (such as calling another function). It is a power-user pattern and not needed in most flows.
hangup: True
Ends the call after the function executes.
Always include an utterance when hanging up – otherwise the call ends silently, which feels like a dropped call to the user.
handoff: True
Transfers the call to a live agent after the function executes. Like hangup, always pair it with an utterance so the user hears a proper transition message before the transfer.
This is commonly used in flows when a user fails validation repeatedly or requests a live agent. See Flow patterns for examples.
Check your understanding
A user says they have no further questions. Your hangup function runs but the user hears nothing before the call ends. What is missing?
- The function is missing a
contentkey - The function is missing an
utterancekey paired withhangup: True - The LLM needs to be prompted to say goodbye first
- The
end_turnflag needs to be set to False
Answer: The function is missing an utterance key paired with hangup: True
Without an utterance, the hangup executes silently. Always return an utterance like "Thanks for calling. Goodbye." alongside hangup: True so the user has proper closure.
Fill in the blank
You want the agent to say “Goodbye!” and then end the call. Complete the return value: return {"utterance": "Goodbye!", "______": True}
Answer: hangup
Returning an empty dictionary
An empty dictionary {} means the function returns no output. The LLM calls the function, receives nothing, and has no new information to work with. It will typically produce a filler response (“One moment, please”) and then hallucinate the rest of the conversation.
Avoid this in production. Always return something meaningful.
Passing the utterance as a function argument
When using Raven (PolyAI’s in-house LLM), the model returns either a tool call or text – not both in the same response. This means you cannot rely on the LLM to generate a goodbye message at the same time as calling a hangup function.
A useful pattern for this situation: pass the utterance as a parameter of the function.
Parameter: utterance – The goodbye message to say to the user before ending the call
The LLM generates the utterance value and passes it as an argument, so the response is contextually appropriate. The function then plays it as a hard-coded utterance.
The LLM generates the phrasing; the function controls execution.
This pattern also works for handoff functions, where you want the LLM to generate a context-appropriate transfer message rather than using a fixed phrase.
Scenario: You’re building a function that retrieves a caller’s account balance. If the balance is above £500, you must say the exact phrase ‘Your balance is above the threshold’ (compliance requirement). If it’s below £500, you want the LLM to generate a natural, empathetic response. How do you structure the return value for each case?
Hints:
- Think about which return type gives you precise control over the exact words spoken.
- For the low-balance case, the LLM needs to see the data so it can generate a response — not hear a hard-coded phrase.
- One case needs
utterance; the other needs a plain string.
Solution: High balance: return {"utterance": "Your balance is above the threshold"} — this plays the exact phrase without any LLM involvement. Low balance: return f"The caller's balance is £{balance}." — the LLM reads this and generates a natural, empathetic response. Using utterance for the low-balance case would mean you have to write every possible phrasing yourself, which removes the LLM’s main advantage.
Quick reference
Check your understanding
Your function returns both an "utterance" and a "content" key. What happens with the content?
- The content is spoken aloud immediately after the utterance
- The content is stored in conversation history so the LLM can use it on the next turn, but is not spoken
- The content replaces the utterance if it contains more detail
- The content is logged but the LLM cannot access it
Answer: The content is stored in conversation history so the LLM can use it on the next turn, but is not spoken
When you return both keys, the utterance is played immediately (buying time while data loads), and the content is stored in history for the LLM to reference on the next turn. This lets you say something natural now while giving the LLM the structured data it needs for the next turn.

