Skip to main content
prefetch: runs a lookup once, before the greeting, and puts the answer in a variable. 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

Read the clock before the greeting, and save the date:
agent.yaml
Name it in the prompt that needs it:
tasks/booking.md
That is a whole pre-fetch. Everything below is the rest of what an entry can do.
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 and the agent has to hear them say yes before it acts: Always check a value with the caller.

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

clock: now reads the clock once. Assign as many of the six fields as you want, in one entry: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.
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.

A worked example

Three entries, in order: the clock, the caller’s number, then a lookup that uses it.
agent.yaml
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:
agent.yaml
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.

Every key an entry takes

string
required
A unique entry name used in errors and logs. Use lower snake case. No name is inferred.
string
Only now is accepted. Choose exactly one of clock, source, and tool; omitting all three is refused.
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.
string
One of the eight system call facts. Choose exactly one of clock, source, and tool. An unavailable fact skips the entry and leaves its destination values unchanged.
string
A declared local or webhook tool. Choose exactly one of clock, source, and tool. No tool runs when this key is absent.
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.
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.
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.

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:
agent.yaml
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:
tasks/verify-customer.md
The task must save the agreed number to clear confirmation. Add the assignment to the confirming task:
agent.yaml
End its instructions with:
tasks/verify-customer.md
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 step can name the variable in skip_when_confirmed: and the step is skipped once it is confirmed. The public salon example 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: 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.
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:
agent.yaml
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:
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.
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.
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.

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

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.
agent.yaml
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 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. 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 for choosing useful lookups and reading their logs.

Try it

Your machine has no caller ID, so stand in for one:
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.
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.

Next steps

Deciding what to pre-fetch

Which lookups are worth moving earlier, and the traps to avoid doing it.

Variables reference

Every source, every rule, every error message.