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

> ## Agent Instructions
> Unmute compiles to exactly three targets. Pipecat and LiveKit are code targets: compile writes a Python project you run. SLNG is a hosted target: compile writes a deployment body and SLNG runs the agent, so it has no `unmute dev`. Those three are the only values `provider` accepts in `targets.yaml`. Deepgram and ElevenLabs appear in these docs as model vendors, which is not the same thing as a target, and `slng` is both.
> The Go structs in `internal/spec` and `internal/ir` are the schema truth. Check a field against them, or run `unmute validate`, rather than against what you remember.

# Pre-fetch

> Run a lookup before the call starts, when you already have everything it needs.

`prefetch:` runs a lookup once, before the greeting, and puts the answer in a
[variable](/build/variables). Today's date. Who is calling. What that person
ordered last time.

If you already have what a lookup needs, the model should not spend the call
doing it.

On this page:

* [Quickstart](#quickstart) - the smallest entry that works
* [What you can fetch](#what-you-can-fetch) - a clock, a call fact, or a tool
* [Every key an entry takes](#every-key-an-entry-takes) - the full shape
* [Always check a value with the caller](#always-check-a-value-with-the-caller) - `confirm:`, and why identity needs it
* [Where it works](#where-it-works) - which routes supply which facts
* [Troubleshooting](#troubleshooting) - what stops the build, and how to fix it

## Quickstart

Read the clock before the greeting, and save the date:

```yaml agent.yaml theme={null}
variables:
  booking_date:
    type: Date
    default: ""

prefetch:
  - name: today
    clock: now
    timezone: Europe/Madrid
    assign:
      - booking_date: result.date
```

Name it in the prompt that needs it:

```markdown tasks/booking.md theme={null}
Local date: {{booking_date}}
Use this as today when the caller says tomorrow or names a weekday.
```

```sh theme={null}
unmute validate my-agent
```

That is a whole pre-fetch. Everything below is the rest of what an entry can do.

<Note>
  A fetched value is a guess until the caller agrees to it. The phone network
  says which phone called, not who is holding it. Mark anything about the
  caller's identity with <Tooltip tip="A value stays locked until the caller says yes to it out loud. Locked values stay out of prompts and out of tools.">`confirm:`</Tooltip> and the agent has to hear them say yes
  before it acts: [Always check a value with the
  caller](#always-check-a-value-with-the-caller).
</Note>

## Why bother

When the model calls a tool, it thinks twice: once to decide to call it, once to
use what came back. The caller waits through both, in silence.

Prefetch moves that work before the greeting. It removes a lookup from the
conversation, but its own run time can delay the greeting. Keep it short.

Here is a good sign. If your prompt tells the model "call `get_x` first", `get_x`
is a tool to pre-fetch.

You cannot pre-fetch a tool that needs to know what the caller said. The time
they picked, the service they want, the reason they called. Those still happen
during the call.

## What you can fetch

| Write this            | And you get                                                            |
| --------------------- | ---------------------------------------------------------------------- |
| `clock: now`          | the date, the time, and more, all from one reading of the clock        |
| `source: from_number` | a fact the call carries, like the number it came from                  |
| `tool: my_tool`       | a result from a local or webhook tool; declare whether it changes data |

<Accordion title="One clock reading, six fields">
  `clock: now` reads the clock once. Assign as many of the six fields as you
  want, in one entry:

  | Field                | Example                  |
  | -------------------- | ------------------------ |
  | `result.date`        | `2026-09-04`             |
  | `result.time`        | `14:32`                  |
  | `result.datetime`    | `2026-09-04T14:32+02:00` |
  | `result.day_of_week` | `Friday`                 |
  | `result.year`        | `2026`                   |
  | `result.timezone`    | `Europe/Madrid`          |

  All six fields are text, including `result.year`. Use `Date` for
  `result.date`, `Time` for `result.time`, and `str` for the weekday, year,
  combined datetime, or timezone.

  All six come from the same reading. An entry assigning `result.date` and
  `result.time` cannot straddle a second, and one assigning `result.date` and
  `result.day_of_week` cannot straddle midnight.
</Accordion>

<Accordion title="Which tools can run before the greeting">
  A local or webhook tool can run before the greeting when every input is
  already known: a fixed value, a call fact, or a value an earlier entry saved.
  MCP, knowledge, prebuilt, and hosted tools cannot run as prefetch entries.

  That covers a lot of tools. A customer record, looked up by phone number.
  Today's opening hours, looked up by date. Someone's last order, looked up by
  their customer id. What plan an account is on.

  Declare `writes: false` for a lookup that only reads. `writes: true` is also
  supported, but means the tool changes data on every call with usable inputs,
  including wrong numbers and calls that end before a conversation. It does not
  wait for the caller's agreement.
</Accordion>

## A worked example

Three entries, in order: the clock, the caller's number, then a lookup that
uses it.

```yaml agent.yaml theme={null}
prefetch:
  - name: today
    clock: now
    timezone: Europe/Madrid
    assign:
      - booking_date: result.date
      - booking_weekday: result.day_of_week

  - name: caller
    source: from_number
    assign:
      - customer_phone: result.value

  - name: profile
    tool: look_up_account
    writes: false
    args:
      - phone: "{{customer_phone}}"
    assign:
      - account_name: result.name
      - account_on_file: result.status
```

`profile` reads once and assigns twice. An entry can fill as many variables as
the result has fields, and a second `assign:` line costs no second call.

Declare the receiving variables. A default is optional; here an empty string
means the value is not known yet:

```yaml agent.yaml theme={null}
variables:
  booking_date:
    type: Date
    default: ""
  booking_weekday:
    type: string
    default: ""
  customer_phone:
    type: Phone
    default: ""
  account_name:
    type: string
    default: ""
  account_on_file:
    type: string
    default: ""
```

<Note>
  Do not give these variables a `source:` of their own. The pre-fetch entry is
  what fills them. That is why the same package still works on a phone route
  carrying no caller ID.
</Note>

## Every key an entry takes

<ParamField path="name" type="string" required>
  A unique entry name used in errors and logs. Use lower snake case. No name is inferred.
</ParamField>

<ParamField path="clock" type="string">
  Only `now` is accepted. Choose exactly one of `clock`, `source`, and `tool`; omitting
  all three is refused.
</ParamField>

<ParamField path="timezone" type="string">
  Required with `clock`: an IANA zone such as `Europe/Madrid`. There is no default,
  because the container clock is UTC. Refused on source and tool entries.
</ParamField>

<ParamField path="source" type="string">
  One of the eight [system call facts](/reference/variables#sources). Choose exactly one
  of `clock`, `source`, and `tool`. An unavailable fact skips the entry and leaves its
  destination values unchanged.
</ParamField>

<ParamField path="tool" type="string">
  A declared local or webhook tool. Choose exactly one of `clock`, `source`, and `tool`.
  No tool runs when this key is absent.
</ParamField>

<ParamField path="writes" type="boolean">
  Required with `tool`: `true` if this use changes data, or `false` if it only reads.
  There is no inferred answer. Refused with `clock` or `source`.
</ParamField>

<ParamField path="args" type="list of one-key pairs">
  Arguments for a tool entry, as argument/value pairs. Values may use declared variable
  placeholders. Omit when the tool needs no authored arguments; required inputs must still
  be supplied. Refused on clock and source entries.
</ParamField>

<ParamField path="assign" type="list of one-key pairs" required>
  One or more `variable: result.field` pairs. Every destination must be declared. Values
  are checked together before any are saved. No assignment is inferred.
</ParamField>

## Entries run in order

Top to bottom, in the order you wrote them. Above, `caller` gets the phone
number and then `profile` uses it.

Put them the other way round and the build stops, naming both entries and
telling you which one to move up. An entry can only read what an entry above it
has already assigned.

## Always check a value with the caller

The phone network tells you which phone called. It does not tell you who is
holding it. People call from a partner's phone, or from work.

So mark the value with `confirm:`. It names the step that has to hear the caller
say yes:

```yaml agent.yaml theme={null}
variables:
  customer_phone:
    type: Phone
    default: ""
    confirm: verify_customer
```

Until they say yes, the value is held back:

* no prompt can use it, except that one step's own;
* during the conversation, a tool that reads it through `inject:` or a webhook
  path refuses to run until it is confirmed. A prefetch lookup may still read
  the candidate so it can prepare a record before the greeting.

This carries over. `account_name` was looked up from a number nobody has
confirmed yet, so it is unconfirmed too. That is what stops the agent greeting a
stranger by the account holder's name.

Then write that step to read the value back:

```md tasks/verify-customer.md theme={null}
The call came from {{customer_phone}}. If there is a number there, read it back
and ask if it is the right one for the booking. If it is empty, ask for a number.
```

The task must save the agreed number to clear confirmation. Add the assignment
to the confirming task:

```yaml agent.yaml theme={null}
agents:
  concierge:
    tasks:
      - name: verify_customer
        when: Confirm the customer's number before using their account.
        instructions: tasks/verify-customer.md
        assign:
          - customer_phone: result.phone
```

End its instructions with:

```markdown tasks/verify-customer.md theme={null}
After the caller agrees, finish with the confirmed phone number. If they
correct it, confirm the corrected number before finishing.
```

The prompt asks for agreement; the runtime clears the confirmation mark when
that named task saves the value. It cannot infer agreement from speech alone.
Confirmation is not identity authentication; add the checks your service needs.

If the caller confirms the same number, the prefetched dependent values can
be used. If they change it, values looked up from the old number are cleared.
They are not automatically fetched again: use a task tool to look up the new
number and save its result. Unrelated facts, such as today's date, remain.

To avoid repeating verification in another task or agent, do not track a second
"is this confirmed" variable of your own: `confirm:` already marks it. A
[task group](/build/orchestration/task-groups#skip-a-step-whose-work-is-already-done)
step can name the variable in `skip_when_confirmed:` and the step is skipped
once it is confirmed. The public
[salon example](https://github.com/slng-ai/unmute/tree/main/examples/salon-concierge)
shows this: its booking task group skips the verification step entirely once
`customer_phone` is already confirmed.

## Where it works

`clock:` and `tool:` entries run on every target except `slng`, which refuses
`prefetch:` and `confirm:` outright: it starts the session itself, so there is
no gap before the greeting to fetch anything in.

`source:` depends on the route, and on the direction of the call:

| Fact                 | `livekit sip` | `livekit connector` | `pipecat daily-sip` | `pipecat cloud-websocket` |
| -------------------- | ------------- | ------------------- | ------------------- | ------------------------- |
| `source.session_id`  | in, out       | in, out             |                     |                           |
| `source.carrier`     | in, out       | in, out             |                     |                           |
| `source.connection`  | in, out       | in, out             |                     |                           |
| `source.call_id`     | in, out       | in, out             | in, out             | in, out                   |
| `source.stream_id`   |               | in, out             |                     | in, out                   |
| `source.direction`   | in, out       | in, out             | in, out             | in, out                   |
| `source.from_number` | in, out       | in, out             | in                  | in                        |
| `source.to_number`   | in, out       | in, out             |                     | out                       |

`in` means the fact resolves on an inbound call, `out` means it resolves on an
outbound call. A blank cell means the route supplies nothing there, and an
entry reading that fact is skipped on every call, with a log line saying so.

The two Pipecat rows read a phone number one way only, and that is not an
oversight. A TwiML Bin is attached to one number, so on an inbound call the
number being called is already fixed, and only the caller's number is worth
carrying. An outbound call goes out from the environment's own caller ID, so
there the number worth carrying is the one being dialled. Each direction hands
the agent the one number the carrier knows and unmute does not.

So a `source: from_number` entry gets nothing on an outbound `pipecat
daily-sip` call, and a `source: to_number` entry gets nothing anywhere on
Pipecat except an outbound `cloud-websocket` call. `unmute validate` warns
when a package declares only the direction a route does not grant.

<Accordion title="The number an outbound call carries">
  An outbound call has nobody calling in, so the number worth carrying is the one
  being dialled. It is the same shape of entry, reading the other fact:

  ```yaml agent.yaml theme={null}
  prefetch:
    - name: dialing
      source: to_number
      assign:
        - customer_phone: result.value
  ```

  On both LiveKit routes that is all of it. The worker places the call, so it
  already holds the number, and nothing else has to be set up.

  On `pipecat cloud-websocket` the number has to be put into the request that
  places the call, alongside the parameter naming the agent:

  ```xml theme={null}
  <Parameter name="to_number" value="+15550707444"/>
  ```

  The number goes into that request twice, once as the number Twilio dials and
  once as this parameter. Twilio fills `{{To}}` in for a TwiML Bin but substitutes
  nothing inside an inline `Twiml=`, so the caller of the API supplies it. The
  generated `build/pipecat/README.md` prints the whole request with your own
  values already in it, which is the copy worth using.

  Leave the parameter out and the entry is skipped, exactly as a withheld caller
  ID is skipped, and the run log names the entry and says the call carried no
  `to_number`.
</Accordion>

<Accordion title="A caller's number is best effort">
  A phone number is a less certain fact than a call id, on every route that
  supplies one. A caller can withhold their own number, and a withheld number
  does not arrive as nothing:

  * Twilio's own policy is to set it to the word `anonymous`.
  * Where an upstream carrier sends a word such as ANONYMOUS or RESTRICTED
    instead, Twilio converts it to keypad digits, which look exactly like a real
    number.
  * Some calls simply arrive with the field empty.

  unmute treats all three the same way: absent. A number-valued fact resolves
  only when it looks like a plausible E.164 number, a `+` followed by 8 to 15
  digits, and a short list of known digit placeholders is rejected on top of
  that check. Either way the entry is skipped, and the log names which entry and
  why.

  On LiveKit `sip`, the number is also absent when the dispatch rule sets
  `HidePhoneNumber`. On `pipecat cloud-websocket` it can be missing for a
  quieter reason: the number rides a `<Parameter>` in the TwiML Bin you made, so
  a Bin created before this existed does not carry it. Nothing warns about that
  at compile time, because checking would need carrier credentials the compiler
  never asks for.

  Treat the caller's number as best effort everywhere it resolves, not only on
  Pipecat. A route granting the fact is not a promise that any one call will
  carry it.

  <Note>
    The same fact declared as a variable's own `source:` is stricter: a route
    that cannot supply it refuses the declaration. This depends on the fact and
    route, not simply on whether the target is Pipecat. A prefetch entry can
    instead skip an unavailable fact and leave the variable at its default.
  </Note>
</Accordion>

## Troubleshooting

### The build refuses a clock entry with no `timezone:`

Containers run on UTC time. Without a timezone, a business in Spain taking a
late evening booking for "tomorrow" writes down the wrong day.

**Fix:** put the zone on the entry, not on the package. Two entries may
honestly want two zones.

```yaml agent.yaml theme={null}
prefetch:
  - name: today
    clock: now
    timezone: Europe/Madrid
```

### The build asks a `tool:` entry to declare `writes:`

There is no default. A pre-fetch runs on every single call, whether the inputs
turn out to be right or wrong, so the build makes you say whether that is safe
before it will run the tool at all.

**Fix:** answer it. The build trusts that line; it cannot open your handler and
check.

```yaml agent.yaml theme={null}
prefetch:
  - name: profile
    tool: look_up_account
    writes: false
```

`writes: true` compiles too. It is a declaration, not a request for permission:
the entry is named in `compile-report.json` and in the runbook instead of
printing a warning. Point a pre-fetch at a lookup, and mark anything else
`writes: true` so it stays easy to find later.

### `writes:` is refused on a clock or source entry

Neither runs a tool, so the key means nothing there.

**Fix:** remove it.

### `assign:` on a clock entry names a field that is not one of the six

**Fix:** use one of the six fields above. The refusal lists all six.

### A pre-fetch cannot fill a list or a shape

A pre-fetch resolves before anybody speaks, so what it has is one value: a
formatted clock reading, the number the call carries, one field of a tool
result. Text is fine, and so is shaped text like `Phone`, `Date` or
`EmailStr`, because the shape is checked where the value enters the state. A
`Literal` works when the tool's own field declares the same set.

**Fix:** run the lookup in a [task](/build/orchestration/tasks) and save the
result through task `assign:`. The refusal names the step to assign it from.

### A value arrives empty on every call

The whole prefetch list shares a two-second budget. Entries run in order, once
per call. There is no per-entry retry or automatic refresh during the call.

| Outcome                               | What happens                                       |
| ------------------------------------- | -------------------------------------------------- |
| An input is missing                   | The entry is skipped                               |
| A lookup times out or raises an error | The entry is logged and skipped                    |
| The result fails type validation      | Its assignments are not saved                      |
| An entry saves several fields         | All are validated before any are saved             |
| The shared budget runs out            | Remaining work is skipped so the call can continue |

Skipped entries leave the destination values unchanged, usually at their
initial defaults. A later entry is skipped too if an input it needs is still
missing. If a usable default supplies that input, the later entry may run.

Write prompts to handle missing values. `none recorded yet.` means an unset
value, not proof that no customer or booking exists. Check a lookup's explicit
status before drawing that conclusion. See
[What to fetch before the call](/optimization/prefetch) for choosing useful
lookups and reading their logs.

## Try it

Your machine has no caller ID, so stand in for one:

```sh theme={null}
unmute dev examples/salon-concierge --target livekit --source from_number=+15005550006
```

The command above uses a test number. For your own lookup, use a test account
number it knows. Check four cases: the caller confirms it, corrects it, has
no record, or has no caller ID. Leave the flag off for the last case. The
agent should ask only for the missing information in each case.

<Warning>
  Do not use `--var` for this. It writes the value straight in, skips the
  pre-fetch and marks nothing as needing confirmation. Your local run would then
  act on a value it never read back, and pass where a real call fails.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="Deciding what to pre-fetch" icon="gauge" href="/optimization/prefetch">
    Which lookups are worth moving earlier, and the traps to avoid doing it.
  </Card>

  <Card title="Variables reference" icon="file-code" href="/reference/variables">
    Every source, every rule, every error message.
  </Card>
</CardGroup>
