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}}
PurposeEnvironment variable substitutionRuntime variable substitution
TimingPlaybook load time (static)During conversation (dynamic)
ScopeYAML configuration sectionPrompt text section
SourceSystem environment variablesSIP Headers, set_var, state
EngineRust regexMiniJinja template engine
Typical UseAPI Keys, config parametersCustomer 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:

  1. Read environment variable OPENAI_API_KEY
  2. Replace "${OPENAI_API_KEY}""sk-actual-key"
  3. 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:

  1. Call starts, extract SIP Header: X-Name=John
  2. Generate prompt, replace {{X-Name}}John
  3. 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:

  1. 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"
    
  2. 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
---

Example 3: Using ${VAR} in Prompt - Will Be Kept

---
llm:
  provider: "openai"
---
# Scene: main

Hello, I'm using model: ${LLM_MODEL}

Result:

  • If LLM_MODEL env 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:

  1. Jinja2 {{var}} - if variables passed in parse()
  2. Environment vars ${VAR} - before YAML parsing
  3. Runtime {{var}} - during conversation

Best Practices

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

ScenarioUseReason
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