Skip to main content
A variable is a named value that lives for one call. This is the complete field reference. For an introduction, start with Variables.

How a value moves

Values are supplied at session start, filled by prefetch:, 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

The type: 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 a Phone:
There is no 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 for EmailStr, 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 | None applies to scalar types and shapes. Use it when null is 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.
Any variable may be unset before it is filled. Successful assignments must match its declared type, including | None when the result is null.

Defaults

Scalar variables may have a default: 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.
Fields use either the short form, - 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.
A tool entry reads fields from the tool’s declared 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.
unmute treats all three as 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 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
Until that step has heard the caller agree, the value:
  • 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.
The mark clears when the named confirming task saves a non-empty value. Its prompt must ask for agreement; the runtime does not judge the spoken answer. Confirmation is inherited: a value looked up from an unconfirmed value carries the same confirming step, because a name found from a number nobody agreed to is exactly as unconfirmed as the number was. That is what stops an agent greeting a stranger by the account holder’s name. Write the confirming task’s prompt to read the value back when present and ask from scratch when absent, then save it through 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

Repeatable. Each value is parsed against the declared type, so an 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:
An undeclared name is refused rather than accepted and dropped.

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:
Repeatable, and only the eight facts a call carries are accepted. On a real call the carrier’s own value wins: a seed fills only what the route supplied nothing for.
Do not seed the variable with --var instead. That writes the value directly, skips the pre-fetch, marks nothing as awaiting confirmation, and lets a local run act on a number it never read back. The run would pass a path a real call fails.

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
The variable declaration supplies the finish field’s type and description. The task validates all assigned values before saving any of them. A failed validation keeps the task open so the model can correct the result. Finishing with a non-empty unserved_request saves no assignments. A + on the key appends one entry instead of replacing the value:
agent.yaml
Use append with a 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 an assign: 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
with 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_date is 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.
If a field on the way is optional, 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
With 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.