Skip to main content
A variable is a named value the call holds onto. You declare what it holds in state.py, 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:
state.py
agent.yaml
Use a model for fields that travel together as a task result. Pre-fetch fills plain values, so it cannot save the clock into a model. 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:
state.py
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 state.py and agent.yaml.

1. Declare it

Write a State class in state.py, next to agent.yaml. Each field is one variable. Start with plain text:
state.py
That is the whole declaration. requested_service is the name you will use everywhere else, and str means it holds text. The default is what the value starts as. Every field needs one: = "" for text, = None with | None, and = [] for a list. Field(description=...) is what the model reads when a task asks it for this value. Write it for the model, short, and so it reads whole when the value is empty. The reasoning you want to keep for the next author goes in a # comment, which reaches nothing. agent.yaml lists a value under variables: only when it needs a source: or a confirm:. It is a list, with one entry per value:
agent.yaml
string
required
The field on State this entry configures. An entry naming no field is refused.
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 Field(description=...) and an = None default in state.py. 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 at its default. An unset value renders as none recorded yet. in a prompt.
unmute validate and unmute compile read state.py by running it through uv, so uv must be on your PATH for a package that has one. The first run needs network. A package without a state.py needs no uv.

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 field. The field already owns the type and the description, so the task does not repeat them. A step must return every whole value it assigns. Mark one it may leave out with a ? after the result field, as - callback_time: result.callback_time?. 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, as {{state.name}}:
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. A bare {{requested_service}} is refused, with the fix: write {{state.requested_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.
state.py
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. state.py is ordinary Pydantic, so the types are Pydantic’s. Those cover most packages. The forms below are for when they do not.

A phone number

pydantic_extra_types.phone_numbers has a PhoneNumber, which on its own saves tel:+34-600-111-222. Wrap it so the saved value is E.164, +34600111222:
state.py
Besides phone_numbers, two more pydantic_extra_types modules are open to you: currency_code and language_code. Any other is refused. The reference lists the types in each.

A fixed set of choices

state.py
Anything outside the set is refused where it enters, and the refusal lists what was allowed. The default is None, because an empty string is not one of the three words.

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. On the SLNG target, a value a task saves, or one with source: conversation, can use most of them. Tasks and task groups lists which. A value the dispatch fills stays plain str, because SLNG holds it as text. The Twilio target refuses a state.py.
The reference has the full list, what is refused, and how a type is enforced. Your tools still check business rules, such as whether a record exists and whether a slot is free. Every kind of 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 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:
state.py
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 model

Skip this until you need it. A model 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 model 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 model is a Pydantic class in state.py. Declare its fields, then use the class as a field’s type on State. One model can be used by several fields. Each field 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:
state.py
Give a model field a Field(description=...) when it needs explaining to the model. Its description is sent with the field in the finish argument.
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

{{state.appointment}} renders the whole object as compact JSON. {{state.appointment.date}} reads one field, and a dotted path can follow nested models. To read fields from a list item, save that item into its own variable first. For a name and an email address, use a two-field model such as Contact(name: str, email: EmailStr), so {{state.contact.name}} and {{state.contact.email}} work. Pydantic’s own NameEmail is one string, so a prompt cannot read .name from it. 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 field’s type; pass JSON for a model or a list, quoted for your shell. A value that does not fit its type stops the call start, and the error names the field. 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.