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 - the smallest entry that works
- What you can fetch - a clock, a call fact, or a tool
- Every key an entry takes - the full shape
- Always check a value with the caller -
confirm:, and why identity needs it - Where it works - which routes supply which facts
- Troubleshooting - what stops the build, and how to fix it
Quickstart
Read the clock before the greeting, and save the date:agent.yaml
tasks/booking.md
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 “callget_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
One clock reading, six fields
One clock reading, six fields
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.Which tools can run before the greeting
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.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 withconfirm:. It names the step that has to hear the caller
say yes:
agent.yaml
- 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.
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
agent.yaml
tasks/verify-customer.md
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.
The number an outbound call carries
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: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 The number goes into that request twice, once as the number Twilio dials and
once as this parameter. Twilio fills
agent.yaml
pipecat cloud-websocket the number has to be put into the request that
places the call, alongside the parameter naming the agent:{{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 caller's number is best effort
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.
+ 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 likePhone, 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: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.