Template Syntax Comparison: {{var}} vs ${VAR}
March 1, 2026 · View on GitHub
Overview
Active-Call supports two template syntaxes that don't conflict and work at different stages for different purposes.
Comparison Table
| 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 |
Execution Order
┌─────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────┘
Detailed Explanation
${VAR} - Environment Variables (Configuration Time)
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 here
Processing Flow:
- Read environment variable
OPENAI_API_KEY - Replace
"${OPENAI_API_KEY}"→"sk-actual-key" - YAML parser sees:
apiKey: "sk-actual-key"
{{var}} - Runtime Variables (Conversation Time)
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
Real-World Examples
Example 1: Using Both Together
---
# 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.
Example 2: Using {{var}} in YAML - Not Recommended
---
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
---
Example 3: Using ${VAR} in Prompt - Will Be Kept
---
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)
Variable Source Comparison
${VAR} Sources
# 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
{{var}} Sources
# 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)
Common Questions
Q1: Can I use {{var}} in YAML?
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.
Q2: Can I use ${VAR} in Prompt?
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}}.
Q3: Can they be nested?
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)
Q4: What's the precedence?
Execution order determines precedence:
- Jinja2
{{var}}- if variables passed in parse() - Environment vars
${VAR}- before YAML parsing - Runtime
{{var}}- during conversation
Best Practices
✅ Recommended
---
# 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" />
❌ Avoid
---
# ❌ 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
Summary
| 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! 🎯
Additional Resources
- Playbook Advanced Features - Complete feature guide
- Environment Variables Example - Detailed env var usage
- Configuration Guide - Full configuration reference
- README - Getting started guide