Active-Call supports two template syntaxes that don't conflict and work at different stages for different purposes.
| Feature | ${VAR} |
{{var}} |
|---|---|---|
| Purpose | Environment variable substitution | Runtime variable substitution |
| Timing | Playbook load time (static) | During conversation (dynamic) |
| Scope | YAML configuration section | Prompt text section |
| Source | System environment variables | SIP Headers, set_var, state |
| Engine | Rust regex | MiniJinja template engine |
| Typical Use | API Keys, config parameters | Customer info, conversation state |
┌─────────────────────────────────────────────────────────┐
│ 1. Load Playbook File │
│ *.md file from disk │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. Jinja2 Rendering (if variables parameter passed) │
│ {{var}} → Runtime variable replacement │
│ Used to dynamically generate playbook content (opt.) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. Split YAML and Prompt │
│ Split by "---" │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 4. Environment Variable Expansion (in YAML section) │
│ ${VAR} → environment variable value │
│ ⚠️ Only affects YAML configuration │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 5. YAML Parsing │
│ serde_yaml::from_str() │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 6. Prompt Section Preserved │
│ {{var}} kept in prompt, waiting for runtime replace │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 7. Call Starts - Runtime Variable Injection │
│ - Extract SIP Headers → {{X-CID}} │
│ - LLM set_var → {{user_name}} │
│ - Replace {{var}} when rendering prompt │
└─────────────────────────────────────────────────────────┘
Timing: When playbook loads, before YAML parsing
Location: Only in YAML configuration section (between ---)
Source: System environment variables
---
llm:
apiKey: "${OPENAI_API_KEY}" # ← Use ${VAR} here
baseUrl: "${OPENAI_BASE_URL}"
model: "${OPENAI_MODEL}"
tts:
speed: ${TTS_SPEED} # Numeric values work too
---
# Scene: main
Your prompt hereProcessing Flow:
- Read environment variable
OPENAI_API_KEY - Replace
"${OPENAI_API_KEY}"→"sk-actual-key" - YAML parser sees:
apiKey: "sk-actual-key"
Timing: During conversation, each time prompt is generated
Location: Mainly in Prompt text section (after ---)
Source: SIP Headers, LLM set_var commands, state variables
---
sip:
extract_headers:
- "X-CID"
- "X-Name"
llm:
provider: "openai"
---
# Scene: main
Hello, {{X-Name}}!
Your customer ID is: {{X-CID}}
Conversation turn: {{turn_count}}Processing Flow:
- Call starts, extract SIP Header:
X-Name=John - Generate prompt, replace
{{X-Name}}→John - Different value for each call
---
# Configuration: Use ${VAR} from environment variables
asr:
provider: "${ASR_PROVIDER}" # Env var: sensevoice
language: "${ASR_LANGUAGE}" # Env var: zh
tts:
provider: "${TTS_PROVIDER}" # Env var: supertonic
speaker: "${TTS_SPEAKER}" # Env var: F1
llm:
provider: "${LLM_PROVIDER}" # Env var: openai
apiKey: "${OPENAI_API_KEY}" # Env var: sk-xxx
model: "${LLM_MODEL}" # Env var: gpt-4o
sip:
extract_headers:
- "X-Customer-ID"
- "X-Customer-Name"
- "X-VIP-Level"
---
# Prompt: Use {{var}} for runtime replacement
# Scene: greeting
Hello, {{X-Customer-Name}}!
Your customer ID is: {{X-Customer-ID}}
You are our {{X-VIP-Level}} customer.
How can I help you today?Execution Result:
-
At load time (${VAR} replacement):
asr: provider: "sensevoice" language: "zh" tts: provider: "supertonic" speaker: "F1" llm: provider: "openai" apiKey: "sk-actual-key-here" model: "gpt-4o"
-
At runtime ({{var}} replacement):
Hello, John! Your customer ID is: C12345 You are our Gold customer.
---
llm:
apiKey: "{{api_key}}" # ❌ Not recommended! This will fail
---Why it fails?
{{api_key}}exists when YAML is parsed- YAML parser treats it as literal string
"{{api_key}}" - LLM config gets invalid API key
Correct approach:
---
llm:
apiKey: "${OPENAI_API_KEY}" # ✅ Use environment variable
------
llm:
provider: "openai"
---
# Scene: main
Hello, I'm using model: ${LLM_MODEL}Result:
- If
LLM_MODELenv var exists:"Hello, I'm using model: gpt-4o" - If not exists:
"Hello, I'm using model: ${LLM_MODEL}"(kept as-is)
# From environment variables
export OPENAI_API_KEY=sk-xxx
export TTS_SPEED=1.2
export ASR_LANGUAGE=zh
# In Docker
docker run -e OPENAI_API_KEY=sk-xxx ...
# Or .env file
OPENAI_API_KEY=sk-xxx
TTS_SPEED=1.2# 1. SIP Headers
sip:
extract_headers:
- "X-CID" # → {{X-CID}}
# 2. LLM set_var command
<set_var key="user_name" value="John" />
# → {{user_name}}
# 3. HTTP response
<http url="..." method="GET" set_var="api_response" />
# → {{api_response}}
# 4. System built-ins
{{turn_count}} # Conversation turn count
{{summary}} # Conversation summary
{{session_id}} # Unique session identifier
{{call_type}} # Call type (sip/websocket/webrtc/b2bua)
{{caller}} # Caller SIP URI (SIP only)
{{callee}} # Callee SIP URI (SIP only)
{{start_time}} # Call start time (RFC 3339)A: Technically yes, but not recommended.
---
# Pass variables parameter before parse()
llm:
model: "{{model_name}}" # Works, but confusing
---This gets replaced during Jinja2 rendering stage (step 2), before environment variable expansion. But this makes config complex - use ${VAR} instead.
A: Yes, but it gets replaced at playbook load time to a fixed value.
---
llm:
provider: "openai"
---
# Scene: main
Hello! API Endpoint: ${OPENAI_BASE_URL}This ${OPENAI_BASE_URL} gets replaced at load time, all calls see the same value.
If you want dynamic values, use {{var}}.
A: No direct nesting, but can be combined.
# ❌ Not supported
apiKey: "${OPENAI_${ENV}_KEY}" # Won't expand
# ✅ Correct approach
export OPENAI_DEV_KEY=sk-dev
export API_KEY_VAR=OPENAI_DEV_KEY
# Then in shell:
export FINAL_KEY=$(eval echo \$$API_KEY_VAR)Execution order determines precedence:
- Jinja2
{{var}}- if variables passed in parse() - Environment vars
${VAR}- before YAML parsing - Runtime
{{var}}- during conversation
---
# Config: Static values use ${VAR}
asr:
provider: "${ASR_PROVIDER}"
language: "${ASR_LANGUAGE}"
tts:
provider: "${TTS_PROVIDER}"
speaker: "${TTS_SPEAKER}"
llm:
apiKey: "${OPENAI_API_KEY}"
model: "${LLM_MODEL}"
sip:
extract_headers:
- "X-Customer-ID"
- "X-Customer-Name"
---
# Prompt: Dynamic values use {{var}}
# Scene: main
Hello, {{X-Customer-Name}}!
Customer ID: {{X-Customer-ID}}
<set_var key="greeting_done" value="true" />---
# ❌ Runtime variables in YAML
llm:
apiKey: "{{runtime_api_key}}" # Will fail
# ❌ Mixing syntaxes
tts:
speaker: "${TTS_SPEAKER}}" # Extra }
---
# ❌ Expecting dynamic config in Prompt
# Scene: main
Current model: ${LLM_MODEL} # Static, fixed at load time| Scenario | Use | Reason |
|---|---|---|
| API Keys | ${OPENAI_API_KEY} |
Sensitive data from env vars |
| Model config | ${LLM_MODEL} |
Config param, set at deploy time |
| TTS params | ${TTS_SPEED} |
Static config, determined at load |
| Customer info | {{X-Customer-Name}} |
Different per call |
| Conversation state | {{user_confirmed}} |
Changes at runtime |
| HTTP response | {{api_response}} |
Retrieved at runtime |
| Session info | {{session_id}} |
Built-in auto-injected |
| Call type | {{call_type}} |
Built-in auto-injected |
Remember:
${VAR}= Configuration time (static) = Environment variables{{var}}= Runtime (dynamic) = Conversation state
They complement each other without conflict! 🎯
- Playbook Advanced Features - Complete feature guide
- Environment Variables Example - Detailed env var usage
- Configuration Guide - Full configuration reference
- README - Getting started guide