Skip to main content
A variable is a named value the call holds onto. You declare what it holds, something fills it during the call, and the prompts that need it name it. On this page:

Keep only the values the call needs

Keep state small. Declare a variable when a value must survive a task or handoff, feed a later tool, or supply a needed call fact to a prompt. Before adding one used by only one prompt, check whether the agent needs the fact at all. Prefer fewer values that each represent one useful fact or result; do not split a timestamp into date and time just because both fields are available. For a clock, replace current_date, current_weekday, and current_time with current_datetime and current_weekday. The timestamp already carries the date and time. Keep the weekday only if the prompt must name it; reading the clock’s answer avoids asking the model to calculate it. If nobody needs the weekday, keep just the timestamp. Merge these entries into an existing package:
agent.yaml
Use a shape for fields that travel together as a task result. Pre-fetch fills plain values, so it cannot save the clock into a shape. Keep separate values when they have different confirmation steps or different readers.

Quickstart

Declare a value, save it when a task finishes, and read it in a later prompt:
agent.yaml
tasks/booking.md
That is the whole loop: declare, fill, read. The rest of this page is each step in full.
The snippets below extend an existing package. Merge them into the matching blocks in agent.yaml.

1. Declare it

A variable needs a name and a type:. Start with plain text:
agent.yaml
That is the whole declaration. requested_service is the name you will use everywhere else, and str means it holds text.
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.
A variable starts unset unless you give it a default:.

2. Fill it

Declaring a variable creates the box. Something still has to put a value in it. There are three ways, and each has its own page: The common one is a task. Add assign: to the task that learns the value:
agent.yaml
tasks/choose-service.md
assign: creates a finish argument named service from the requested_service declaration. The destination already owns the type and the description, so the task does not repeat them. All assigned values are checked before any are saved. If one is invalid, the task stays open so the model can correct its answer, and nothing changes.

3. Read it where it is needed

Saving a value does not put it in any prompt. A prompt reads a value only when it names it:
tasks/booking.md
An unset value renders as none recorded yet., so write the sentence to read whole either way. The instructions above work before and after the earlier task saves the service.
The agent that owns a task needs its own placeholder to remember the result after the task returns. It gets a completion status and its own earlier conversation back, not the private conversation inside the task.

Use one in the greeting

The greeting renders once, before anyone speaks, so it can only name a value that is already settled: one supplied at session start, a default:, or a prefetch assignment.
agent.yaml
Do not greet a caller by a name you have not checked with them. A name looked up from the number that called is a guess: people call from a partner’s phone. Mark it with confirm: and the greeting cannot use it until they say yes.

The types you can declare

The type is what the call checks a value against before saving it. A task’s finish argument is built from it too, so the model is told what shape of answer is wanted.

Built-in types

string, integer, number, and boolean are aliases for str, int, float, and bool. Those ten cover most packages. The three forms below are for when they do not.

A fixed set of choices

agent.yaml
Anything outside the set is refused where it enters, and the refusal lists what was allowed.

A value that may be absent

T | None says null is a valid answer, not a failure. Use it when a lookup can honestly come back with nothing.

Several values of the same type

list[T] holds many. See Keep several records below.
LiveKit and Pipecat support every type on this page. The SLNG target supports the basic scalars only: str, int, float, bool, and their aliases. It runs none of your package’s code, so it has nowhere to check a shape.
The type reference has the exact formats and composition rules. Your tools still check business rules, such as whether a record exists and whether a slot is free. Every type on this page appears once in examples/customer-intake, one agent that collects a caller’s details and hands them to a tool.

Keep several records

Declare type: list[T] when later steps need several values of the same type. A list starts as []. In a task’s assign:, variable+: appends one item and variable: replaces the whole list:
agent.yaml
Appending skips null and skips an identical structured item. See assignment rules, and state design to choose between a current value and a list.

Advanced

Three things you will not need on your first agent.

Group fields into a shape

Skip this until you need it. A shape is worth declaring when several fields always travel together and one tool fills all of them at once. Until then, separate variables are simpler to read and simpler to prompt.
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 shape defines your own reusable object type. Choose a name, then declare each field and its type under shapes:. Set a variable’s type: to that name to store an object with those fields. One shape can be used by several variables. Each variable holds its own value, and a task assignment saves all of the object’s fields together. For example, this package defines an object type called Appointment and uses it for the variable appointment:
agent.yaml
A task assignment saves all of the object’s fields together, and one shape can be used by several variables, each holding its own value. Use the short field form, - field_name: Type, or the long form with name:, type: and description: when a field needs explaining to the model. See the shape reference for both.
Declare a field only when something in the package can fill it. A field no tool returns leaves the model inventing a value or sending an empty one, and it renders empty in every prompt that names it for the rest of the call.

Reading one field

{{appointment}} renders the whole object as compact JSON. {{appointment.date}} reads one field, and a dotted path can follow nested shapes. To read fields from a list item, save that item into its own variable first. NameEmail is an object type too, so {{contact.name}} and {{contact.email}} work without declaring anything under shapes:. It is refused as a shape name, because it already means something. For deciding which fields belong together, separating a requested change from a saved result, and choosing one record or a list, see Designing declared state.

Hand a value to a tool

Sometimes the tool needs a value the model does not need to read or type. Use inject: for that argument:
tools/check_slots.yaml
The model supplies date. The handler receives both date and the current service. Keep injected arguments out of input.properties and input.required. If the value is missing or unconfirmed, the tool stops before it runs. The error names the task that supplies or confirms the value when one is declared, and otherwise asks the model to collect it. Injection is not the same as keeping a value private: tool results and spoken messages can still contain it. See Reduce context sharing step by step.

Try it locally

--var works for a declared variable with source: call_start or no source: at all. Values are parsed against the variable’s type; pass JSON for an object or a list, quoted for your shell. A system source such as from_number comes from the phone route instead, and your machine is not one. To exercise prefetch and confirmation locally, stand in for the network:
Which facts a route supplies, and in which direction, is in the source reference.

Troubleshooting

A prompt renders none recorded yet. on every call

The variable is unset. Either nothing has filled it yet, or the prompt that reads it runs before the step that fills it. Fix: check which step owns the assign:, and write the sentence so it reads whole either way. An unset value is normal early in a call, so the prompt should say what to do about it:

The owning agent forgets a value after its task returns

A task returns a completion status and the ’s own earlier conversation. It does not hand back the private conversation inside the task. Fix: put a placeholder in the owner’s prompt too. Saving is not sharing.

A tool refuses to run, naming a value

The tool reads that value through inject: or a webhook path, and the value is empty or still unconfirmed. Fix: the refusal names the task that supplies or confirms it. Run that step first, or, when nothing supplies the value, have the model ask the caller.

--var is refused for a variable

--var works only for a variable with source: call_start or no source: at all. A system source such as from_number comes from the phone route, and your machine is not one. Fix: use --source instead, which stands in for the network:

Where to go next

Tasks

The usual way a variable gets filled: assign: on the task that learns it.

Pre-fetch

Fill a value before the greeting, and confirm the ones about the caller.

Context scope

Who can read what, and how to share less as the call goes on.

Designing declared state

Which fields belong together, and when a list beats a current value.

Credentials

API keys and tokens, which are not variables.

Variables reference

Every type, field, source and assignment option.