> ## Documentation Index
> Fetch the complete documentation index at: https://unmute.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Unmute compiles to exactly three targets. Pipecat and LiveKit are code targets: compile writes a Python project you run. SLNG is a hosted target: compile writes a deployment body and SLNG runs the agent, so it has no `unmute dev`. Those three are the only values `provider` accepts in `targets.yaml`. Deepgram and ElevenLabs appear in these docs as model vendors, which is not the same thing as a target, and `slng` is both.
> The Go structs in `internal/spec` and `internal/ir` are the schema truth. Check a field against them, or run `unmute validate`, rather than against what you remember.

# Variables reference

> Types, sources, where a variable can be used, and how it reaches a tool.

A variable is a named value that lives for one call. This is the complete field
reference. For an introduction, start with [Variables](/build/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`.

```yaml agent.yaml theme={null}
variables:
  caller_name:
    type: string
    source: call_start
    default: there
    description: Caller's first name, used in the greeting and the prompt.
```

<ParamField path="type" type="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](/reference/variables#the-type-grammar) for composition
  rules and target limits.
</ParamField>

<ParamField path="description" type="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.
</ParamField>

<ParamField path="default" type="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.
</ParamField>

<ParamField path="source" type="string">
  Accepts `call_start`, `conversation` (SLNG only), or a [system call
  fact](/reference/variables#sources). 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`.
</ParamField>

<ParamField path="confirm" type="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.
</ParamField>

## 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

| Type        | Accepted value                                     |
| ----------- | -------------------------------------------------- |
| `str`       | Text                                               |
| `int`       | A whole number                                     |
| `float`     | A number that may have a decimal part              |
| `bool`      | `true` or `false`                                  |
| `Phone`     | Phone number text with the format below            |
| `Date`      | Date text with the format below                    |
| `Time`      | Time text with the format below                    |
| `Id`        | Identifier text with the format below              |
| `EmailStr`  | Email address text, checked as described below     |
| `NameEmail` | An object with a `name` field and an `email` field |

Aliases: `string` means `str`, `integer` means `int`, `number` means `float`,
and `boolean` means `bool`.

### Custom types and combinations

| Form                          | Accepted value                                                      |
| ----------------------------- | ------------------------------------------------------------------- |
| `Literal["value1", "value2"]` | One of the distinct, double-quoted strings you list                 |
| Your shape's name             | An object matching the fields declared under that name in `shapes:` |
| `list[T]`                     | An array whose items have type `T`                                  |
| `T \| None`                   | A value of type `T` or `null`; also written `None \| T`             |

`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:`](#shapes-groups-fields-into-a-named-type).

`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`:

```json theme={null}
{
  "description": "a phone number in E.164, one leading plus and 7 to 15 digits, like +34600111222",
  "type": "string"
}
```

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:

| Family                                    | What the model receives                                               | Where it is checked                                                       |
| ----------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `Phone`, `Date`, `Time`, `Id`, `EmailStr` | `type: string` plus the format sentence                               | When the value is saved                                                   |
| `Literal[...]`                            | An `enum` holding your exact values                                   | The provider limits what it can generate, and it is checked again on save |
| A shape, `NameEmail`, `list[...]`         | The real object or array, each field carrying its own format sentence | On save, field by field                                                   |

### 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

| Type       | Format checked when saving                                                                                        |
| ---------- | ----------------------------------------------------------------------------------------------------------------- |
| `Phone`    | One leading `+`, followed by 7 to 15 digits; the first digit is nonzero                                           |
| `Date`     | Four digits, a hyphen, two digits, a hyphen, two digits (`YYYY-MM-DD`)                                            |
| `Time`     | Hours `00` to `23`, a colon, and minutes `00` to `59`                                                             |
| `Id`       | 1 to 64 characters; starts with an ASCII letter or digit; subsequent characters may also be `.`, `-`, `_`, or `:` |
| `EmailStr` | A valid email address. The saved value is the normalized form                                                     |

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

| Target              | Supported variable types                                                                                                                                                                                                                                                                                          |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| LiveKit and Pipecat | All types and task assignments on this page                                                                                                                                                                                                                                                                       |
| SLNG                | Basic scalars: `str`, `int`, `float`, `bool`, and their aliases. A text type with a checked format, a shape and `NameEmail` are all refused, because SLNG runs no code from your package and has nowhere to check one. Also the only target that takes `source: conversation`, a value the model records mid-call |

## `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.

```yaml agent.yaml theme={null}
shapes:
  - name: Appointment
    description: One thing being booked, moved or cancelled.
    fields:
      - scheduled_date: Date
      - scheduled_time: Time
      - name: appointment_type
        type: Literal["haircut", "haircolor", "dry_cut"]
        description: The service the caller asked for, in the salon's own words.
```

<ParamField path="name" type="string" required>
  A unique `CapWords` type name, used by `type:`. Built-in type names are reserved. There
  is no inferred name.
</ParamField>

<ParamField path="description" type="string">
  Description of the shape, used as its class docstring. Omit it for no extra description.
</ParamField>

<ParamField path="fields" type="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.
</ParamField>

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

| Source         | Who supplies it                                                                               | Availability                                                                                                                                                                                                                                                                                                                        |
| -------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `call_start`   | the dispatch payload, or `--var` locally                                                      | every channel, before the first word                                                                                                                                                                                                                                                                                                |
| omitted        | the dispatch payload if it carries the name, or `--var` locally; otherwise a step's `assign:` | never guaranteed, so write the prompt to read whole while it is still empty, or give it a `default:`                                                                                                                                                                                                                                |
| `conversation` | the model, during the call, once the caller has given and confirmed the value                 | `slng` only. It becomes one of the agent's runtime variables, filled through SLNG's own `set_runtime_variables` tool and returned on the call record under `memory_variables`. It takes no `default:` and needs a `description:`, which the model reads. On `livekit` and `pipecat` a step's `assign:` records such a value instead |
| `session_id`   | the phone adapter                                                                             | LiveKit `sip` or `connector`                                                                                                                                                                                                                                                                                                        |
| `call_id`      | the phone adapter                                                                             | LiveKit `sip` or `connector`, and both Pipecat Twilio routes                                                                                                                                                                                                                                                                        |
| `stream_id`    | the phone adapter                                                                             | LiveKit `connector`, and Pipecat `cloud-websocket`                                                                                                                                                                                                                                                                                  |
| `direction`    | the phone adapter                                                                             | LiveKit `sip` or `connector`, and both Pipecat Twilio routes                                                                                                                                                                                                                                                                        |
| `from_number`  | the phone adapter                                                                             | LiveKit `sip` or `connector`, both directions; both Pipecat Twilio routes, inbound calls only                                                                                                                                                                                                                                       |
| `to_number`    | the phone adapter                                                                             | LiveKit `sip` or `connector`, both directions; Pipecat `cloud-websocket`, outbound calls only                                                                                                                                                                                                                                       |
| `carrier`      | the phone adapter                                                                             | LiveKit `sip` or `connector`                                                                                                                                                                                                                                                                                                        |
| `connection`   | the phone adapter                                                                             | LiveKit `sip` or `connector`                                                                                                                                                                                                                                                                                                        |

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](/build/prefetch#where-it-works) 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.**

```yaml agent.yaml theme={null}
prefetch:
  - name: today
    clock: now
    timezone: Europe/Madrid
    assign:
      - booking_date: result.date
      - booking_weekday: result.day_of_week

  - name: caller
    source: from_number
    assign:
      - customer_phone: result.value

  - name: profile
    tool: look_up_account
    writes: false
    args:
      - phone: "{{customer_phone}}"
    assign:
      - account_name: result.name
      - account_on_file: result.status
```

`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.

| Key              | Reads                                                            | Result fields                                                                                           |
| ---------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `clock: now`     | the clock, in the entry's own `timezone:`                        | `result.date`, `result.time`, `result.datetime`, `result.day_of_week`, `result.year`, `result.timezone` |
| `source: <name>` | one of the eight facts a call carries                            | `result.value`                                                                                          |
| `tool: <name>`   | one already-declared tool, with `writes:` declared on this entry | `result.<field>`, from the tool's `output:`                                                             |

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:`.

<Note>
  `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.
</Note>

### Prefetch fields

<ParamField path="name" type="string" required>
  A unique entry name used in errors and logs. Use lower snake case. No name is inferred.
</ParamField>

<ParamField path="clock" type="string">
  Only `now` is accepted. Choose exactly one of `clock`, `source`, and `tool`; omitting
  all three is refused.
</ParamField>

<ParamField path="timezone" type="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.
</ParamField>

<ParamField path="source" type="string">
  One of the eight [system call facts](/reference/variables#sources). Choose exactly one
  of `clock`, `source`, and `tool`. An unavailable fact skips the entry and leaves its
  destination values unchanged.
</ParamField>

<ParamField path="tool" type="string">
  A declared local or webhook tool. Choose exactly one of `clock`, `source`, and `tool`.
  No tool runs when this key is absent.
</ParamField>

<ParamField path="writes" type="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`.
</ParamField>

<ParamField path="args" type="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.
</ParamField>

<ParamField path="assign" type="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.
</ParamField>

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](/build/prefetch#where-it-works) for the full route table
and [Prefetch](/build/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.

```yaml agent.yaml theme={null}
variables:
  customer_phone:
    type: string
    default: ""
    confirm: verify_customer   # names the task itself
```

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](/build/prefetch#always-check-a-value-with-the-caller).

## 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.

| Site                         | When the value is read                                 |
| ---------------------------- | ------------------------------------------------------ |
| `conversation.greeting.text` | Once, at session start                                 |
| An agent's instructions      | On entry and when its tasks save values                |
| A task's instructions        | When the task starts                                   |
| A tool's `inject:` value     | On every tool call                                     |
| A webhook tool's `path`      | On every tool call; substituted values are URL encoded |

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.

| Referenced value                                              | Prompt text                                 |
| ------------------------------------------------------------- | ------------------------------------------- |
| Text, including `Phone`, `Date`, `Time`, `Id`, and `EmailStr` | The text itself                             |
| Number or boolean                                             | JSON text, such as `2` or `true`            |
| Shape, `NameEmail`, or list                                   | Compact JSON; an empty list renders as `[]` |
| Unset, `null`, or empty string                                | `none recorded yet.`                        |
| A field inside an absent shape                                | `none recorded yet.`                        |

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:`](#confirm-marks-a-value-the-caller-has-to-agree-to) 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](/best-practices/context-scope) to choose exactly what crosses a boundary.

## Seeding values locally

```sh theme={null}
unmute dev ./agent --var caller_name=Ada --var customer_id=cus_2002
```

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:

```text wrap theme={null}
unmute: dev my-agent: --var from_number=+15551234567: "from_number" has source from_number, so the runtime supplies it, not you
```

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:

```sh theme={null}
unmute dev ./agent --source from_number=<a number in E.164>
```

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.

<Warning>
  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.
</Warning>

## Passing a value into a tool

```yaml tools/book_appointment.yaml theme={null}
description: Book the selected appointment for the current customer.

input:
  type: object
  properties:
    slot_id:
      type: string
  required:
    - slot_id

inject:
  - customer_id: "{{customer_id}}"
  - service: "{{requested_service}}"

local:
  handler: tools/book_appointment.py
```

`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](/build/tools/prebuilt)). See
[Hosted tools](/build/tools/hosted) 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:

```text theme={null}
cannot call book_appointment yet: requested_service not set. Ask the caller first.
```

## Assigning a task result to a variable

```yaml agent.yaml theme={null}
agents:
  appointment_desk:
    tasks:
      - name: check_customer
        when: Identify the caller before booking.
        instructions: tasks/check-customer.md
        assign:
          - customer_id: result.customer_id
```

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:

```yaml agent.yaml theme={null}
        assign:
          - appointments+: result.appointment
```

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:

```yaml agent.yaml theme={null}
agents:
  appointment_desk:
    tasks:
      - name: manage_booking
        when: Save one appointment.
        instructions: tasks/booking.md
        assign:
          - appointments+: result.appointment
          - last_booking_day: result.appointment.scheduled_date
```

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](#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.

```markdown agents/appointment-manager.md theme={null}
You are handling appointment {{appointment_id}}.
```

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

<Columns cols={2}>
  <Card title="Variables, explained" icon="braces" href="/build/variables">
    The same surface as a walkthrough.
  </Card>

  <Card title="Secrets" icon="key" href="/reference/secrets">
    Why a secret is never a variable.
  </Card>
</Columns>
