How a value moves
Values are supplied at session start, filled byprefetch:, or saved when
a task finishes through assign:. Prompts and tools read them through
explicit variable references. Conversation sharing is controlled separately
by context.history.
agent.yaml
string
required
A single-line Python type expression. Accepts
str, int, float, bool, Phone,
Date, Time, Id, EmailStr, NameEmail, a declared shape name, Literal[...],
list[T], or T | None. The aliases string, integer, number, and boolean also
work. See the type grammar for composition
rules and target limits.string
A sentence explaining what the value means. Used to describe the task finish argument
derived from this variable. Required for
source: conversation; otherwise omit for no
extra description. The saved value is still shared only through placeholders.matching the declared type
A scalar starting value, checked against
type. Shapes, lists, and NameEmail take no
authored default. If omitted, a scalar or shape starts unset and a list starts as [].
An unset value renders as none recorded yet. in a prompt.string
Accepts
call_start, conversation (SLNG only), or a system call
fact. If omitted, the variable has no automatic call-fact
source; a task, pre-fetch, or session payload can still fill it. Route support is
checked for system sources. conversation requires a description and forbids a default;
inbound code-target calls require a default for call_start.string
The name of the task that must hear the caller agree to a pre-fetched value. Until
confirmed, only that task can read it in a prompt and tools cannot silently use it. If
omitted, the value is usable as soon as it arrives.
The type grammar
Thetype: field accepts a single-line Python type expression. Names are
case-sensitive. Choose a built-in type or compose a type using the forms below.
Built-in types
Aliases:
string means str, integer means int, number means float,
and boolean means bool.
Custom types and combinations
T stands for the element or value type. Replace it with a built-in type,
a Literal expression, or a shape name. A shape name refers to an object type
you define in the package; see shapes:.
NameEmail is a supplied object type. Read one part with a dotted placeholder
or a dotted assignment, the same way you read a field of a shape you declared.
It cannot be declared under shapes:: the name is refused, because a second
declaration would produce a second type of the same name.
How a type is enforced
A declared type does two jobs, in two different places. The model is told, not constrained. Every text type reaches the model as a plain string carrying a sentence about the format. This is the whole schema the model receives for aPhone:
pattern and no format keyword. Either one would travel to the
provider, and one target sends its schema with strict mode on, where both are
rejected. That failure shows up on your first real call and in no local check,
so the format lives in the description instead.
The value is checked where it is saved. The generated project validates
every declared field before anything enters call state. A value that does not
fit is refused, nothing is written, the previous contents stand, and the format
sentence goes back to the model as the tool result. The model reads what was
wrong and corrects itself on the next turn.
The sentence the model reads and the sentence it gets back on a refusal come
from one place, so the two cannot describe different formats.
The three families of type behave differently:
What checking costs
Nothing you can hear. Checking runs inside the process and makes no network request: roughly 0.2 microseconds for the pattern types and 18 microseconds forEmailStr, which calls a library instead of matching a pattern. No model
request is made to check anything.
The one thing that does cost a model request is a refusal. The tool result goes
back, the model rereads the format and tries again, and the caller waits through
that turn. So a type that refuses something a caller can legitimately say is a
type worth changing, not a prompt worth rewording.
Text types and their limits
These checks validate text format. Use tools to check calendar validity,
record existence, phone ownership, and other business rules. Store timezone
separately from date and time. Use
str for external identifiers requiring
other characters, and preserve the exact value returned by the tool.
An address is checked with the email-validator package, which the generated
project declares when your package uses either email type. The check reads the
address only. It does not look up whether the domain accepts mail, because the
check runs while the caller is on the line.
NameEmail accepts the two fields, and also a single string in either of two
forms: Fred Bloggs <fred.bloggs@example.com>, or the address on its own, in
which case name becomes the part before the at sign. The email field is
checked as EmailStr.
Lists and absent values
- Lists hold one element type and start as
[]. Use an empty list to represent no recorded items. T | Noneapplies to scalar types and shapes. Use it whennullis a valid result for a successful task to save.- Shapes may contain other shapes and lists. Nested shapes must form a finite structure, with each path ending in a value or list.
- To group lists inside a list, define a shape with a list field and use that shape as the outer list’s element type.
| None when the result is null.
Defaults
Scalar variables may have adefault: matching their type. Use a default
when an initial value is useful. Otherwise leave the variable unset until
call-start data, prefetch, or a task supplies it.
Shapes start unset and lists start empty, and so does NameEmail. Fill them
through call-start data or task assignments. An empty text value is treated as missing; a successful
assignment must satisfy the declared type’s format.
Target support
shapes: groups fields into a named type
A pre-fetch cannot fill a shape or a list. Use a task to produce grouped fields;
use separate scalar variables for clock and lookup pre-fetch results. A single
pre-fetch may assign several scalar variables from one result.
A top-level list of reusable object types. Each entry defines a name and
typed fields. Use that name in a variable’s type: to store an object with
those fields. The name in this example, Appointment, is chosen by the author.
agent.yaml
string
required
A unique
CapWords type name, used by type:. Built-in type names are reserved. There
is no inferred name.string
Description of the shape, used as its class docstring. Omit it for no extra description.
list of field definitions
required
One or more fields. Each item is a one-key
field_name: Type pair, or an object with
name, type, and optional description. Names and types are required in the long
form; omitted descriptions add no extra model guidance.- field_name: Type, or a block with
name:, type:, and an optional description:, as shown above.
Set confirm: on the variable to apply confirmation to the whole object,
including its fields.
Sources
The last eight are system sources. You declare them, and the selected route
must be able to supply the fact: a route that grants nothing for it refuses the
declaration at validation. A
prefetch: entry reads the same route table as a
variable’s own source:. The same fact hydrates either way, on whichever route
grants it. The full grid has exactly what
each route grants, fact by fact and direction by direction.
For an inbound LiveKit or Pipecat telephony channel, every call_start variable
needs a default. There is no dispatch payload to supply an unsolicited incoming
call before its greeting.
prefetch: resolves a value before the call starts
Some values are knowable before anybody speaks: today’s date, the number the call
came from, whatever a lookup keyed on that number returns. prefetch: resolves
them once per call, before the greeting, so the model never spends a turn
discovering them.
It is an ordered list. Entries resolve top to bottom.
agent.yaml
profile assigns two variables from one lookup. An entry can assign: as
many variables as the result has fields, from the one call.
Every entry carries a name: and exactly one source key.
A
clock: now entry reads the clock once and may assign: as many of the six
fields as you want. All six come from that one reading, so 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.
args: belongs to a tool: entry only, and takes the same value grammar as a
tool’s inject:.
assign: and args: are lists of one-key items, not mappings. Each pair gets
its own - line. An item holding two pairs is a dropped indent and is refused
with its line number.Prefetch fields
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.output: schema. Its
arguments may be fixed scalars or reference values saved by earlier entries.
The receiving variables must already be declared, with no source: of their
own. Prefetch fills scalar values, including Phone, Date, Time, Id,
EmailStr, and a compatible Literal. Shapes, NameEmail, lists, and nullable
bare scalars such as str | None must be saved through a task; for prefetched
text, leave a str variable unset until the value is available.
A reference to a variable assigned by a later prefetch entry is refused.
Entries run in authored order. writes: true entries appear in the compile
report and generated runbook; the declaration does not trigger a confirmation
conversation before execution.
What happens when it cannot resolve
The whole list shares a two-second budget. Missing inputs skip an entry; a failed lookup or invalid result is logged without failing the call. Assignments from one entry are validated together before any are saved. Skipped assignments leave the destination values unchanged. Later entries can run only if their inputs are available, including any usable defaults. Prefetched text is limited to 512 characters per value; longer text is shortened with a log message. Keep long records in your backend and fetch what a task needs during the conversation.unmute validate reports facts unavailable on a target’s route or direction.
On a real call, a fact may still be absent even when the route supports it.
See Where it works for the full route table
and Prefetch for the setup and missing-value workflow.
A caller’s number is best effort
from_number and to_number resolve less often than the other system
sources, on every route that grants them. A caller can withhold their own
number, and withholding 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
configuration reason instead: 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 on every route that grants it, not
only on Pipecat. A route granting the fact is not a promise that any one call
will carry it.
Denied on the slng target: that platform owns session start, so there is no seam
to resolve a fact in.
confirm: marks a value the caller has to agree to
A fact that arrives from the carrier is a proposal, not a settled value. Somebody
may be ringing from a friend’s phone, or may hold a second account.
agent.yaml
- renders in no prompt but that step’s own, refused at compile time everywhere else, including the greeting and every agent prompt;
- makes every tool injecting it refuse itself to the model, naming the value it is waiting for.
assign: after agreement.
A prefetch lookup can use a candidate before confirmation, but its dependent
values inherit the same restriction. When the candidate changes, those old
prefetched values are cleared, not silently reused or automatically fetched
again. A task must look up the corrected value if it is needed.
See the confirmation walkthrough.
How a variable reaches a prompt
A saved value enters a prompt only where you write{{variable}} or
{{variable.field}}. This applies to every type, including shapes and lists.
The greeting may only name a variable with a session-start source, a default,
or a prefetch assignment. A prefetch can still be skipped, so handle a missing
value. Agent and task instructions may name values that will be filled later.
Undeclared variable names are compile errors at every site.
Naming one part of a value
{{variable.field}} reads one field from an object. {{variable}} reads the
whole object. Dotted paths can follow nested shapes. To read a list item’s
fields, first save the selected item in its own variable.
Write a label followed by an instruction for the missing case. These
placeholders do not support conditions, filters, indexing, or function calls.
A wrong field name is a compile error that names the fields you can use.
Rendered values are limited to 4000 characters each and shortened with a
warning. Keep records small or reference just the fields the prompt needs.
confirm: limits where a
candidate value may be referenced. Only its confirming task may name it in a
prompt; a prompt elsewhere is refused at compile time. The tool path uses the
runtime guard described below.
Conversation sharing is a separate choice. With messages, a receiver can
still read a value somebody said aloud. See Reduce context sharing step by
step to choose exactly what crosses a boundary.
Seeding values locally
integer
variable receives a number and not a quoted string. --var is the local stand
in for the dispatch payload production sends, so it accepts the two kinds of
variable that payload fills: source: call_start, and a variable that declares
no source: at all. It refuses a runtime-owned source, because that one
arrives from the carrier, not the dispatch:
Seeding a caller ID
--var stands in for the dispatch payload. To stand in for a caller ID, use
--source, which seeds the fact a prefetch: entry reads:
Passing a value into a tool
tools/book_appointment.yaml
inject values are not part of the model’s schema, so the model can neither
see them nor overwrite them. An inject key that also names an input
property is a compile error, for exactly that reason.
Each authored injected value must be a scalar, such as a string containing
{{variable}}. An exact placeholder passes the value with its native type,
including a referenced object or list. Mixed text such as "Booking {{booking_id}}"
produces a string. An authored map or list is refused.
Keep injected keys out of both input.properties and input.required.
Injection hides an argument from the model; it does not hide data a tool
returns or data already present in the conversation.
inject is legal on webhook, local, and hosted slng tools, and on
builtin: send_sms for its one setting, the sender (see
Prebuilt tools). See
Hosted tools for the hosted attachment rules.
When an injected variable is missing or unconfirmed, the tool refuses before
execution. The error names the task that supplies or confirms it, if there is
one. Otherwise it tells the model to ask the caller:
Assigning a task result to a variable
agent.yaml
unserved_request saves no assignments.
A + on the key appends one entry instead of replacing the value:
agent.yaml
list[T] variable. It accepts one item of type T or
null, which adds nothing. Identical structured items are saved once; plain
values may repeat. A changed object is a new item. Without +, the assignment
replaces the variable’s value.
Picking one part of a structured result
The right side of anassign: pair does not have to be the whole result. It
can be result.<field>, or a dotted path into a declared shape,
result.<field>.<subfield>, as deep as the shape goes:
agent.yaml
appointments declared list[Appointment] and last_booking_day
declared Date | None. The whole-object assignment establishes the
appointment result field as Appointment | None; the second line can then
project its date.
The picked part’s type has to fit the variable it lands in: a Date field
into a Date variable, a Literal into the same Literal, a whole shape
into a variable declared with that shape.
Two paths are refused rather than silently accepted:
- a path through a list. Nothing says which entry to take, so
result.appointments.scheduled_dateis refused. - a path into a field with no declared shape. Once the path reaches a scalar, going one level deeper has nothing left to read.
Appointment | None above, the picked
value may be absent for a visit where the task left it out. Give the variable
the same option, Date | None, so an absent pick is a legal value, or use an
appending assign, name+:, which already skips an absent entry instead of
writing one into the list.
The same path form works in a prompt placeholder too: see Naming one part of
a value.
A value the caller may not give is declared | None
Every field a step assigns goes through its declared type whether the model
sent it or not. A field the model leaves out arrives as None, and so does one
it sends as an explicit null. A type without | None refuses both, which
refuses the whole finish call: nothing is saved, the model is told why and gets
another go, and the caller waits through that round trip.
Declare the option in the type, Time | None, for any value the caller may
simply not give. Do not try to solve it in the description: no wording reliably
stops a model saying “nothing” when there is nothing.
An empty string is still right for a value that is always asked for and may not
be known yet, which is why every text type accepts one. The difference is
whether absence is a legal outcome of the step. If it is, say so in the type.
Absence is already legal in one other place, and needs nothing: an appending
assign, name+:, whose field is optional on its own and drops an absent entry
instead of writing it into the list.
A handoff can read selected declared values
Declared values stay in call state across a handoff. The receiving model sees only the ones its prompt names.agents/appointment-manager.md
history: reset, this explicit reference still resolves but no prior
conversation is inherited. An unsaved detail from the triggering sentence is
gone, so the receiving agent asks for it.
Where to go next
Variables, explained
The same surface as a walkthrough.
Secrets
Why a secret is never a variable.