Context & Dynamic Variables
Most agents aren't a single static script. The same support agent should know the caller's name; the same outbound campaign should reference each lead's appointment date. Bolti supports dynamic variables in your agent's system prompt and first message — {{ placeholders }} that get replaced with real values at the moment the call starts.
The syntax uses common {{ variable }} placeholders with optional default filters, so prompts remain easy to author and reuse across integrations.
How it works
sequenceDiagram
participant Caller as Your client / Dashboard
participant API as Bolti API
participant Worker as Voice agent
Caller->>API: Start call with variable_values
API->>API: Validate every {{var}} has a value or default
API->>Worker: Dispatch with variable_values + customer context
Worker->>API: Fetch resolved config
API-->>Worker: Prompt + first message rendered
Worker->>Worker: Talk to caller using resolved prompt
Substitution happens server-side at session start, not in the LLM call. The LLM only ever sees a fully resolved prompt — it never sees the {{ ... }} syntax itself.
Syntax
The simplest variable is just a name in double braces:
Hello {{ name }}, your appointment is on {{ appointment_date }}.
Add a fallback with the Liquid default filter:
Hi {{ name | default: "there" }}, calling about {{ topic | default: "your account" }}.
The rule is straightforward:
- A variable without
default:is required. Every call must supply a value or the API rejects the request with400 Bad Requestand lists the missing names. - A variable with
default:is optional. If the caller doesn't supply one, the default is used.
Both the System prompt and the First message (greeting) are templated using the same context, so you can personalize the opening line — "Hi {{ name }}, this is Aria from Acme."
Built-in variables
These are populated automatically and never need to be supplied by the caller. They're also excluded from the "required" check, so it's safe to reference them in any prompt.
| Variable | Value | Example |
|---|---|---|
{{ now }} | Current date+time, UTC | Apr 25, 2026 12:34 PM |
{{ date }} | Current date, UTC | Apr 25, 2026 |
{{ time }} | Current time, UTC | 12:34 PM |
{{ month }} | Full month name | April |
{{ day }} | Day of month | 25 |
{{ year }} | Four-digit year | 2026 |
{{ customer.number }} | Destination phone number for outbound, caller's number for inbound | +14155551212 |
{{ transport.conversationType }} | Always "voice" for now | voice |
For custom date formatting use the Liquid date filter:
Today is {{ "now" | date: "%A, %B %d" }}.
Where values come from
| Entry point | How values are passed |
|---|---|
| Dashboard preview | If the prompt has variables, a dialog appears before the call asking for them. |
| Public preview link | Same dialog, shown to whoever opens the share link. |
POST /workspaces/{ws}/agents/{id}/token | Send { "variable_values": { "name": "John" } } in the request body. |
POST /workspaces/{ws}/agents/{id}/outbound-call | Add "variable_values" alongside to_number / agent_phone_number_id. |
| Inbound calls (DID) | No per-call values — the prompt must be fully resolvable from built-ins + defaults. Bolti rejects DID assignment otherwise. |
Example — outbound call with variables
POST /workspaces/{workspace_id}/agents/{agent_id}/outbound-call
Authorization: Bearer <token>
Content-Type: application/json
{
"to_number": "+14155551212",
"agent_phone_number_id": "...",
"variable_values": {
"name": "Priya",
"appointment_date": "April 28"
}
}
If the agent's prompt is "Hi {{ name }}, just confirming your appointment on {{ appointment_date }}.", the LLM receives the fully rendered text — "Hi Priya, just confirming your appointment on April 28." — at session start.
Example — browser preview token
POST /workspaces/{workspace_id}/agents/{agent_id}/token
Authorization: Bearer <token>
Content-Type: application/json
{
"variable_values": {
"name": "Priya",
"topic": "billing question"
}
}
The body is optional — for agents whose prompts have no variables (or only built-ins / defaults), an empty POST works fine.
Discovering an agent's variables
Before you build a UI to collect values, you can ask the API which variables the prompt actually references:
GET /workspaces/{workspace_id}/agents/{agent_id}/variables
Response:
{
"variables": [
{ "name": "name", "default": null, "required": true },
{ "name": "appointment_date", "default": null, "required": true },
{ "name": "topic", "default": "your account", "required": false }
]
}
This is exactly what the dashboard uses to render its variable-input dialog — you can use the same endpoint to dynamically build forms in your own integrations.
Using built-ins for time-aware prompts
Built-ins shine when you want the same agent to behave slightly differently depending on the time. A few patterns:
Good {{ "now" | date: "%p" | downcase | replace: "am", "morning" | replace: "pm", "afternoon" }},
this is Aria calling from Acme.
{% if month == "December" -%}
Wishing you a happy holiday season.
{%- endif %}
Anything Liquid supports — filters, conditionals, loops — works inside the prompt template. See the Liquid documentation for the full filter list.
Validation behavior
When you start a call, Bolti runs the same check on both endpoints:
- Extract every
{{ variable }}from the prompt and first message (built-ins are skipped). - For each one without a
default:, check thatvariable_valuesprovides a value. - If anything is missing, return
400 Bad Requestwith the body:
{
"detail": {
"error": "missing_variables",
"missing": ["name", "appointment_date"]
}
}
The dashboard surfaces this directly in the variable dialog — the Start Conversation button stays disabled until every required field is filled.
Inbound calls
DIDs (the phone numbers that ring inbound) don't have a per-call hook to inject variables. So when you assign a number to an agent on the Phone tab:
- Built-ins like
{{ customer.number }}are fine — the worker injects them from the SIPFromheader. - User-defined variables must have
default:filters. If they don't, the assignment is rejected with a clear error pointing to the offending variables.
In practice, this means inbound prompts should be self-contained, while outbound prompts can lean on variable_values from the API.
Notes & limits
- Extra keys are silently ignored — sending
{"name": "John", "unused": "x"}for a prompt that only references{{ name }}is fine. - User values override built-ins. A user-supplied
yearshadows the auto-populated{{ year }}for that call. Useful for back-dating test scenarios. - Payload size is capped at 4 KB. The serialized
variable_valuesJSON is rejected with413 PAYLOAD_TOO_LARGEif it exceeds that — for big context, fetch it inside a tool call instead. - Substitution is one-shot. Variables are resolved at session start. They don't re-render mid-call; if you need live data during the conversation, use a tool.
- All values are coerced to strings during rendering, so passing numbers or booleans works, but they'll be stringified in the prompt.
Related
- Basic tab — System prompt — where you author the template.
- Calling overview — passing variables through the outbound API.
- Tool calling — for context that can only be fetched mid-call.