> ## Documentation Index
> Fetch the complete documentation index at: https://docs.poly.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Tutorial: Tool return values

> PolyAcademy Level 2 – What your tools (Python functions) can return and how each return type affects agent behavior.

export const LessonMeta = ({level, difficulty, time}) => {
  const levelConfig = {
    1: {
      badge: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
      label: 'Level 1'
    },
    2: {
      badge: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
      label: 'Level 2'
    },
    3: {
      badge: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
      label: 'Level 3'
    }
  };
  const difficultyConfig = {
    Beginner: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
    Intermediate: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
    Advanced: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
  };
  const lvl = levelConfig[level] || levelConfig[1];
  const diffColor = difficultyConfig[difficulty] || difficultyConfig['Beginner'];
  return <div className="flex flex-wrap items-center gap-2 my-4 not-prose">
      <span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${lvl.badge}`}>
        {lvl.label}
      </span>
      <span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${diffColor}`}>
        {difficulty}
      </span>
      {time && <span className="inline-flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400">
          <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
          </svg>
          {time}
        </span>}
    </div>;
};

export const Challenge = ({scenario, hints = [], solution}) => {
  const [revealed, setRevealed] = useState(0);
  const [showSolution, setShowSolution] = useState(false);
  return <div className="my-6 rounded-xl border-2 border-violet-200 bg-violet-50 dark:border-violet-800 dark:bg-violet-950 overflow-hidden">
      <div className="flex items-center gap-2 px-5 pt-4 pb-0">
        <svg className="w-4 h-4 shrink-0 text-violet-500 dark:text-violet-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
          <path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
        </svg>
        <span className="text-xs font-bold uppercase tracking-widest text-violet-500 dark:text-violet-400">
          Challenge
        </span>
      </div>

      <p className="px-5 pt-3 pb-4 mt-0 text-sm leading-relaxed text-violet-900 dark:text-violet-100">
        {scenario}
      </p>

      {hints.length > 0 && <div className="border-t border-violet-200 dark:border-violet-800 px-5 py-3">
          {revealed > 0 && <div className="mb-3 space-y-2">
              {hints.slice(0, revealed).map((h, i) => <div key={i} className="flex gap-2 text-sm">
                  <span className="font-semibold shrink-0 text-violet-400 dark:text-violet-500">
                    Hint {i + 1}:
                  </span>
                  <span className="text-violet-800 dark:text-violet-200">{h}</span>
                </div>)}
            </div>}
          {revealed < hints.length && <button type="button" onClick={() => setRevealed(r => r + 1)} className="text-xs font-medium text-violet-600 dark:text-violet-400 hover:text-violet-800 dark:hover:text-violet-200 underline underline-offset-2 cursor-pointer transition-colors duration-150">
              {revealed === 0 ? 'Show a hint' : 'Show next hint'}
            </button>}
        </div>}

      {solution && <div className="border-t border-violet-200 dark:border-violet-800 px-5 py-3">
          {!showSolution ? <button type="button" onClick={() => setShowSolution(true)} className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 underline underline-offset-2 cursor-pointer transition-colors duration-150">
              Show solution
            </button> : <div>
              <div className="flex justify-between items-center mb-2">
                <span className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
                  Solution
                </span>
                <button type="button" onClick={() => setShowSolution(false)} className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 underline underline-offset-2 cursor-pointer transition-colors duration-150">
                  Hide
                </button>
              </div>
              <p className="mt-0 text-sm leading-relaxed text-gray-800 dark:text-gray-200">
                {solution}
              </p>
            </div>}
        </div>}
    </div>;
};

export const ProgressTracker = ({lessonNum, totalLessons, level}) => {
  const [checked, setChecked] = useState(false);
  return <div onClick={() => setChecked(prev => !prev)} className={checked ? 'flex items-center gap-3 p-4 rounded-lg border-2 border-green-600 bg-green-50 dark:bg-green-950 cursor-pointer select-none transition-all' : 'flex items-center gap-3 p-4 rounded-lg border-2 border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 cursor-pointer select-none transition-all'}>
      <div className={checked ? 'w-5 h-5 rounded border-2 border-green-600 bg-green-600 flex items-center justify-center shrink-0 transition-all' : 'w-5 h-5 rounded border-2 border-gray-400 dark:border-gray-500 bg-white dark:bg-gray-800 flex items-center justify-center shrink-0 transition-all'}>
        {checked ? <svg width="10" height="8" viewBox="0 0 10 8" fill="none">
            <path d="M1 4L3.5 6.5L9 1" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
          </svg> : null}
      </div>
      <div>
        <div className={checked ? 'font-semibold text-sm text-green-700 dark:text-green-300' : 'font-semibold text-sm text-gray-700 dark:text-gray-200'}>
          {checked ? 'Lesson complete' : 'Mark lesson complete'}
        </div>
        {lessonNum && totalLessons ? <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
            {level ? level + ' - ' : ''}Lesson {lessonNum} of {totalLessons}
          </div> : null}
      </div>
    </div>;
};

export const FillBlank = ({prompt, answer, hint, explanation}) => {
  const [value, setValue] = useState('');
  const [submitted, setSubmitted] = useState(false);
  const normalize = s => s.trim().toLowerCase().replace(/[^a-z0-9_]/g, '');
  const answers = Array.isArray(answer) ? answer : [answer];
  const isCorrect = answers.some(a => normalize(value) === normalize(a));
  const handleSubmit = e => {
    e.preventDefault();
    if (value.trim()) setSubmitted(true);
  };
  const handleReset = () => {
    setValue('');
    setSubmitted(false);
  };
  return <div className="my-6">
      <p className="mt-0 mb-3 text-sm font-semibold leading-relaxed text-gray-900 dark:text-gray-100">
        {prompt}
      </p>
      <form onSubmit={handleSubmit} className="flex flex-col gap-2.5">
        <div className="flex gap-2">
          <input type="text" value={value} onChange={e => {
    setValue(e.target.value);
    setSubmitted(false);
  }} placeholder={hint || "Type your answer…"} className="flex-1 rounded-xl border py-2.5 px-4 text-sm font-mono border-gray-200 bg-white text-gray-900 placeholder-gray-400 outline-none focus:border-gray-400 focus:ring-2 focus:ring-gray-200 transition-all duration-150 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-600 dark:focus:border-gray-500 dark:focus:ring-gray-700" />
          <button type="submit" className="rounded-xl border py-2.5 px-5 text-sm font-medium transition-all duration-150 border-gray-800 bg-gray-800 text-white hover:bg-gray-700 hover:border-gray-700 dark:border-gray-200 dark:bg-gray-200 dark:text-gray-900 dark:hover:bg-white">
            Check
          </button>
        </div>
        {submitted ? <div className={`py-3 pl-4 pr-3.5 rounded-r-xl text-sm leading-relaxed border-l-4 ${isCorrect ? 'border-green-500 bg-green-50 dark:bg-green-900 dark:border-green-500' : 'border-red-500 bg-red-50 dark:bg-red-900 dark:border-red-500'}`}>
            {isCorrect ? <>
                <span className={`font-semibold !text-green-800 dark:!text-green-200`}>Correct.</span>{' '}
                <span className="!text-gray-700 dark:!text-gray-300">{explanation}</span>
              </> : <>
                <span className="font-semibold !text-red-800 dark:!text-red-200">Not quite.</span>{' '}
                <span className="!text-gray-700 dark:!text-gray-300">The answer is <code className="!text-gray-800 dark:!text-gray-200">{answers[0]}</code>. {explanation}</span>
              </>}
          </div> : null}
      </form>
      {submitted ? <button type="button" onClick={handleReset} className="mt-2 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 underline underline-offset-2 cursor-pointer transition-colors duration-150">
          Try again
        </button> : null}
    </div>;
};

export const Quiz = ({questions = []}) => {
  const [selected, setSelected] = useState({});
  const [resetCount, setResetCount] = useState(0);
  const letters = ['A', 'B', 'C', 'D'];
  const handleSelect = (qIdx, optIdx) => {
    if (selected[qIdx] !== undefined) return;
    setSelected(prev => ({
      ...prev,
      [qIdx]: optIdx
    }));
  };
  const handleReset = () => {
    setSelected({});
    setResetCount(c => c + 1);
  };
  if (!questions?.length) return null;
  const getOptionClasses = ({hasAnswered, isThisCorrect, isThisSelected}) => {
    if (!hasAnswered) {
      return {
        btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-pointer border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50 hover:shadow-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:border-gray-500 dark:hover:bg-gray-700',
        badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-300',
        icon: null
      };
    }
    if (isThisCorrect) {
      return {
        btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-default border-green-400 bg-green-50 text-green-900 font-medium dark:border-green-500 dark:bg-green-950 dark:text-green-100',
        badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-green-500 text-white dark:bg-green-500',
        icon: <svg className="shrink-0 w-4 h-4 text-green-500 dark:text-green-400 ml-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
          </svg>
      };
    }
    if (isThisSelected) {
      return {
        btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-default border-red-400 bg-red-50 text-red-900 dark:border-red-500 dark:bg-red-950 dark:text-red-100',
        badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-red-500 text-white dark:bg-red-500',
        icon: <svg className="shrink-0 w-4 h-4 text-red-400 dark:text-red-400 ml-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
          </svg>
      };
    }
    return {
      btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-default border-gray-100 bg-white text-gray-400 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-500',
      badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-500',
      icon: null
    };
  };
  return <div key={resetCount} className="my-6">
      {questions.map((q, qIdx) => {
    const answer = selected[qIdx];
    const hasAnswered = answer !== undefined;
    const isCorrect = answer === q.correct;
    return <div key={String(qIdx)} className="mb-8">
            <p className="flex items-start gap-2.5 font-semibold text-sm mb-3 mt-0 leading-relaxed text-gray-900 dark:text-gray-100">
              <span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-800 dark:bg-gray-200 text-white dark:text-gray-900 text-xs font-bold shrink-0 mt-px leading-none">
                {qIdx + 1}
              </span>
              {q.q}
            </p>

            <div className="flex flex-col gap-2">
              {q.options.map((opt, i) => {
      const isThisCorrect = i === q.correct;
      const isThisSelected = i === answer;
      const {btn, badge, icon} = getOptionClasses({
        hasAnswered,
        isThisCorrect,
        isThisSelected
      });
      return <button key={String(i)} type="button" onClick={() => handleSelect(qIdx, i)} className={btn}>
                    <span className={badge}>{letters[i]}</span>
                    <span className="flex-1">{opt}</span>
                    {icon}
                  </button>;
    })}
            </div>

            {hasAnswered ? <div className={`mt-3 py-3 pl-4 pr-3.5 rounded-r-xl text-sm leading-relaxed border-l-4 ${isCorrect ? 'border-green-500 bg-green-50 dark:bg-green-950 dark:border-green-500' : 'border-red-500 bg-red-50 dark:bg-red-950 dark:border-red-500'}`}>
                <span className={`font-semibold ${isCorrect ? '!text-green-800 dark:!text-green-200' : '!text-red-800 dark:!text-red-200'}`}>
                  {isCorrect ? 'Correct.' : 'Not quite.'}
                </span>{' '}
                <span className="!text-gray-700 dark:!text-gray-300">{q.explanation}</span>
              </div> : null}
          </div>;
  })}

      <button type="button" onClick={handleReset} className="mt-1 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 underline underline-offset-2 cursor-pointer transition-colors duration-150">
        Reset quiz
      </button>
    </div>;
};

<Info>
  **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](/tools/return-values) gives you precise control over what the agent says and when.
</Info>

<LessonMeta level={2} difficulty="Intermediate" time="15 min" />

## Choosing a return type

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart TD
    A[Function needs to return something] --> B{Need exact\nphrasing control?}
    B -->|No – LLM can word it| C[Return a string\nLLM generates the response]
    B -->|Yes – exact words matter| D{Also end\nthe interaction?}
    D -->|End call| E["Return {utterance, hangup: True}"]
    D -->|Transfer caller| F["Return {utterance, handoff: True}"]
    D -->|Context for next turn| G["Return {utterance, content}"]
    D -->|Neither| H["Return {utterance: '...'}"]
```

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

<Warning>
  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."`.
</Warning>

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

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def check_banana_stock(conv) -> str:
    return "There are 3 bananas left."
```

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.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {"content": "There are 3 bananas left."}
```

### `utterance`

A hard-coded response that **bypasses the LLM entirely**. The text is spoken directly to the user without any further LLM request.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {"utterance": "There are 3 bananas left. Would you like to buy some?"}
```

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

<Tabs>
  <Tab title="Return string (LLM generates response)">
    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    # Function returns:
    return "There are 0 bananas left."

    # LLM produces:
    # "I'm sorry, we don't have any bananas available right now.
    #  Would you like to check back later?"
    ```

    The LLM adds empathy, context, and a follow-up offer.
  </Tab>

  <Tab title="Return utterance (bypasses LLM)">
    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    # Function returns:
    return {"utterance": "There are 0 bananas left."}

    # User hears exactly:
    # "There are 0 bananas left."
    ```

    Grammatically correct but blunt – no conversational framing.

    To improve it, you must write the logic yourself:

    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    if bananas == 0:
        return {"utterance": "I'm sorry, we're out of bananas right now."}
    elif bananas == 1:
        return {"utterance": "Good news – we have one banana left. Would you like it?"}
    else:
        return {"utterance": f"Good news – we have {bananas} bananas left."}
    ```
  </Tab>
</Tabs>

<Quiz
  questions={[
{
q: "When should you use `utterance` instead of returning a plain string?",
options: [
  "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",
],
correct: 1,
explanation: "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**.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
    "utterance": "Let me check that for you – just one moment.",
    "content": "The user asked about banana stock. There are 3 available."
}
```

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.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
    "utterance": "Just one second while I look that up.",
    "end_turn": False
}
```

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.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
    "utterance": "Thanks for calling. Have a great day. Goodbye.",
    "hangup": True
}
```

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.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
    "utterance": "Let me connect you with someone who can help with that.",
    "handoff": True
}
```

This is commonly used in flows when a user fails validation repeatedly or requests a live agent. See [Flow patterns](/learn/guides/expert/flow-patterns) for examples.

<Quiz
  questions={[
{
q: "A user says they have no further questions. Your hangup function runs but the user hears nothing before the call ends. What is missing?",
options: [
  "The function is missing a `content` key",
  "The function is missing an `utterance` key paired with `hangup: True`",
  "The LLM needs to be prompted to say goodbye first",
  "The `end_turn` flag needs to be set to False",
],
correct: 1,
explanation: "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

<FillBlank prompt="You want the agent to say &#x22;Goodbye!&#x22; and then end the call. Complete the return value: return {&#x22;utterance&#x22;: &#x22;Goodbye!&#x22;, &#x22;______&#x22;: True}" answer={["hangup"]} hint="Which key ends the call?" explanation="The hangup key ends the call after the utterance is played. Always pair it with an utterance so the user hears something before the call ends." />

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

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Function signature includes an utterance argument:
def hang_up(conv, utterance: str) -> dict:
    return {"utterance": utterance, "hangup": True}
```

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.

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

<Challenge
  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 {&#x22;utterance&#x22;: &#x22;Your balance is above the threshold&#x22;} — this plays the exact phrase without any LLM involvement. Low balance: return f&#x22;The caller's balance is £{balance}.&#x22; — 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

| Return value                              | LLM reads it?      | User hears it directly?     | LLM requests             |
| ----------------------------------------- | ------------------ | --------------------------- | ------------------------ |
| `"string"`                                | Yes                | No – LLM generates response | 2                        |
| `{"content": "..."}`                      | Yes                | No – LLM generates response | 2                        |
| `{"utterance": "..."}`                    | No                 | Yes                         | 1                        |
| `{"content": "...", "utterance": "..."}`  | Yes (on next turn) | Yes (immediately)           | 1 + next turn            |
| `{"utterance": "...", "end_turn": False}` | No                 | Yes, then LLM continues     | 1 + immediate follow-up  |
| `{"utterance": "...", "hangup": True}`    | No                 | Yes, then call ends         | 1                        |
| `{"utterance": "...", "handoff": True}`   | No                 | Yes, then call transfers    | 1                        |
| `{}`                                      | No                 | No                          | – (LLM gets no feedback) |

## Check your understanding

<Quiz
  questions={[
{
q: "Your function returns `{\"utterance\": \"Checking now...\", \"content\": \"Order status: delayed by 2 days.\"}`. What happens with the content?",
options: [
  "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",
],
correct: 1,
explanation: "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.",
}
]}
/>

<CardGroup cols={2}>
  <Card title="← Previous: Using tools" icon="arrow-left" href="/learn/guides/advanced/using-tools">
    Lesson 2 of 8
  </Card>

  <Card title="Next: Response Control →" icon="arrow-right" href="/learn/guides/advanced/response-control">
    Lesson 4 of 8
  </Card>
</CardGroup>

<ProgressTracker lessonKey="l2-3-return-values" lessonNum={3} totalLessons={8} level="Level 2" />
