state.py, something fills it during the call, and the prompts that need it
name it.
On this page:
- Quickstart - declare, fill and read one value
- Declare it - the
Stateclass, and thevariables:list - Fill it - a task, a pre-fetch, or the call itself
- Read it - placeholders, and the greeting
- The types you can declare - Pydantic types, plus lists and your own models
- Advanced - models, injection, and seeding a value locally
- Troubleshooting - the four that come up most
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, replacecurrent_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
Quickstart
Declare a value, save it when a task finishes, and read it in a later prompt:state.py
agent.yaml
tasks/booking.md
The snippets below extend an existing package. Merge them into the matching
blocks in
state.py and agent.yaml.1. Declare it
Write aState class in state.py, next to agent.yaml. Each field is one
variable. Start with plain text:
state.py
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.
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
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}}.
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
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
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
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.examples/customer-intake,
one agent that collects a caller’s details and hands them to a tool.
Keep several records
Declarelist[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
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.
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
Field(description=...) when it needs explaining to the
model. Its description is sent with the field in the finish argument.
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. Useinject: for that argument:
tools/check_slots.yaml
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:
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 throughinject: 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.