Call Context
State management and variable resolution during voice calls
Call Context
The Call Context is the state container for a voice call session. It holds all data that flows between agents, tools, and prompts during a conversation.
Overview
┌─────────────────────────────────────────────────────────────────┐
│ CallContext │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ user │ │ workflow │ │ flags │ │
│ │ (immutable) │ │ (shared) │ │ (booleans) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ agents │ │ params │ │ history │ │
│ │ (isolated) │ │ (transient) │ │ (trace) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘Context Namespaces
user.* - User Data
User-specific data provided when the call is dispatched. Immutable during the call.
# Backend dispatches call with user context
await client.post("/api/v1/calls", json={
"workflow_slug": "meal-logging",
"user_context": {
"name": "Rahul",
"language": "hi",
"language_name": "Hindi",
"voice_id": "a167e0f3-df7e-4e3e-a5b4-3b3a6f3b8b3a",
"pending_meals": ["Breakfast", "Lunch"],
"logged_meals": []
}
})Access in prompts:
Hello {{user.name}}! Let's log your {{user.pending_meals[0]}}.workflow.* - Shared State
State that persists across agent handoffs. Modified by tools during the conversation.
{
"workflow": {
"logged_meals": [
{"meal_type": "Breakfast", "items": ["2 idli", "sambar"]}
],
"pending_meals": ["Lunch", "Dinner"],
"current_meal": "Lunch",
"meal_count": 1
}
}Access in prompts:
You've logged {{workflow.meal_count}} meals so far.flags.* - Boolean Flags
Simple boolean flags for conditional logic.
{
"flags": {
"all_meals_logged": false,
"skip_feedback": true,
"user_requested_end": false
}
}Used for:
- Tool conditions (
"conditions": ["all_meals_logged"]) - Handoff triggers
- Workflow branching
agents.* - Per-Agent State
Isolated state for each agent. Not shared across handoffs.
{
"agents": {
"meal-agent": {
"last_meal_logged": "Breakfast",
"questions_asked": 3
},
"feedback-agent": {
"feedback_given": false
}
}
}params.* - Tool Parameters
Transient namespace containing current tool call parameters. Reset on each tool invocation.
{
"params": {
"meal_type": "Breakfast",
"ingredients": "2-piece-idli, 1-bowl-sambar",
"meal_time": "2024-01-15T08:30:00"
}
}Path Syntax
Context values are accessed using dot-notation paths.
Basic Paths
user.name → "Rahul"
workflow.meal_count → 3
flags.all_meals_logged → trueNested Paths
user.preferences.language → "hi"
workflow.current_meal.type → "Breakfast"Array Access
user.pending_meals[0] → "Breakfast"
workflow.logged_meals[1] → {...}Array Append
Special [+] suffix appends to an array:
workflow.logged_meals[+] → Appends new itemTemplate Resolution
Templates use {{path}} syntax to inject context values into strings.
In Prompts
{
"prompt": {
"system": "You are helping {{user.name}} log meals in {{user.language_name}}.",
"greeting": "Hi {{user.name}}! What did you have for {{workflow.pending_meals[0]}}?"
}
}In Tool Actions
{
"actions": [
{
"type": "webhook",
"url": "https://api.example.com/meals/{{params.meal_id}}",
"body": {
"user_id": "{{user.id}}",
"meal_type": "{{params.meal_type}}"
}
}
]
}In Tool Responses
{
"on_success": {
"response": "Great! I've logged your {{params.meal_type}}. {{workflow.pending_meals_count}} meals left."
}
}Context Flow
1. Call Dispatch
Backend provides initial user_context:
response = await client.post("/api/v1/calls", json={
"workflow_slug": "meal-logging",
"user_context": {
"name": "Priya",
"language": "ta",
"pending_meals": ["Breakfast", "Lunch", "Dinner"],
"logged_meals": [],
"voice_id": "tamil-female-voice"
}
})2. Context Initialization
Worker initializes CallContext:
context = CallContext(
call_id=call_id,
tenant_id=tenant_id,
workflow_id=workflow_id,
user=user_context, # Immutable
workflow=workflow.shared_context, # From workflow config
)3. Prompt Resolution
Before speaking, prompts are resolved:
# Template
greeting = "Hi {{user.name}}! Let's log {{user.pending_meals[0]}}."
# Resolved
greeting = "Hi Priya! Let's log Breakfast."4. Tool Execution
Tools modify context via actions:
{
"actions": [
{
"type": "context.set",
"path": "workflow.logged_meals[+]",
"value": {
"meal_type": "{{params.meal_type}}",
"items": "{{params.ingredients}}"
}
},
{
"type": "context.set",
"path": "workflow.pending_meals",
"value": ["Lunch", "Dinner"]
}
]
}5. Agent Handoffs
On handoff, context is preserved:
# Before handoff
context.workflow = {"meal_count": 1, "logged_meals": [...]}
# After handoff to feedback-agent
# Same context.workflow is availableContext Actions
Tools modify context using the context.set action type.
Set Value
{
"type": "context.set",
"path": "workflow.current_meal",
"value": "Lunch"
}Append to Array
{
"type": "context.set",
"path": "workflow.logged_meals[+]",
"value": {
"meal_type": "{{params.meal_type}}",
"logged_at": "{{params.meal_time}}"
}
}Set Flag
{
"type": "context.set",
"path": "flags.all_meals_logged",
"value": true
}Computed Values
For dynamic values, use backend-computed fields in user_context:
# Backend computes before dispatch
user_context = {
"name": user.name,
"pending_meals": get_pending_meals(user.id),
"pending_meals_display": ", ".join(get_pending_meals(user.id)),
"meal_count": len(get_logged_meals(user.id)),
"greeting": generate_greeting(user.language, user.name)
}Multi-Tenant Isolation
Each tenant's calls have completely isolated contexts:
┌─────────────────────┐ ┌─────────────────────┐
│ Tenant A Call │ │ Tenant B Call │
├─────────────────────┤ ├─────────────────────┤
│ call_id: abc-123 │ │ call_id: xyz-789 │
│ tenant_id: tenant-a │ │ tenant_id: tenant-b │
│ user: {...} │ │ user: {...} │
│ workflow: {...} │ │ workflow: {...} │
└─────────────────────┘ └─────────────────────┘- No data leakage between tenants
- Each call has its own
CallContextinstance - Context is never persisted across calls (stateless)
Best Practices
1. Use user.* for Input Data
# Good: Backend provides all user data
user_context = {
"name": user.name,
"language": user.language,
"pending_meals": get_pending_meals(user.id)
}2. Use workflow.* for Shared State
{
"path": "workflow.logged_meals[+]",
"value": {"meal_type": "Breakfast"}
}3. Use flags.* for Conditions
{
"handoffs": [
{
"target_agent": "feedback-agent",
"conditions": ["all_meals_logged"]
}
]
}4. Provide Defaults in Variables
{
"variables": [
{ "name": "user.name", "default": "there" },
{ "name": "user.language_name", "default": "Hindi" }
]
}5. Pre-compute Display Strings
# Backend computes formatted strings
user_context = {
"pending_meals": ["Breakfast", "Lunch"],
"pending_meals_display": "Breakfast and Lunch", # Pre-formatted
"logged_meals_display": "None yet"
}API Reference
CallContext Methods
| Method | Description |
|---|---|
get(path, default) | Get value by path with optional default |
set(path, value) | Set value by path (supports [+] append) |
delete(path) | Delete value at path |
resolve(template) | Resolve {{path}} placeholders |
resolve_dict(data) | Recursively resolve templates in dict |
get_agent_state(agent) | Get agent-specific state |
set_agent_state(key, value) | Set agent-specific state |
switch_agent(name) | Switch current agent |
trigger_handoff(target) | Trigger handoff to agent |
to_dict() | Export context to dictionary |
from_dict(data) | Create context from dictionary |
Path Examples
# Get values
ctx.get("user.name") # → "Rahul"
ctx.get("workflow.meals[0].type") # → "Breakfast"
ctx.get("flags.done", False) # → False (default)
# Set values
ctx.set("workflow.current_meal", "Lunch")
ctx.set("workflow.logged_meals[+]", {"type": "Breakfast"})
ctx.set("flags.all_done", True)
# Resolve templates
ctx.resolve("Hi {{user.name}}!") # → "Hi Rahul!"