Agent Server Changes Per-Contact Dynamic Variables
Overview
The Node.js backend now pre-merges all dynamic variable sources (agent defaults, campaign defaults, per-contact CSV values) into a single flat dictionary called dynamic_values before dispatching to LiveKit. The Agent server just needs to read this field and use it for prompt substitution.
What Changed in Dispatch Metadata
One new optional field is included in the dispatch metadata JSON:
{
"mode": "call",
"name": "John Doe",
"phone_number": "+919876543210",
"agent_id": "abc123",
"agent_config": { ... },
"dynamic_values": {
"company_name": "Acme Corp",
"product_name": "Widget Pro",
"price": "₹999",
"offer_code": "SAVE20"
},
...
}dynamic_values
- A flat
{key: value}dictionary, already merged with the correct priority:- Per-contact values from CSV extra columns (highest priority)
- Campaign-wide defaults
- Agent-level
DynamicVardefaults (lowest priority)
- Will be
null/absent if no dynamic variables exist at any level - The Node.js backend handles all the merging the Agent just consumes the result
Required Changes
1. Update MetadataSchema (src/schemas/metadata_schema.py)
Add one new optional field:
class MetadataSchema(BaseModel):
# ... existing fields ...
# Pre-merged dynamic variables (contact > campaign > agent defaults)
# Ready for direct prompt substitution
dynamic_values: Optional[dict] = None2. Update VoiceAssistant.__init__() (src/core/voice_assistant.py)
Change the dynamic variable loading logic. Current code:
dynamic_variables = {item.key: item.value for item in agent_meta.DynamicVar or []}
dynamic_variables["name"] = user_name if user_name and user_name.lower() != "unknown" else "User"
dynamic_variables["date"] = datetime.now().strftime("%Y%m%d_%H%M%S")Replace with:
# Start with agent-level defaults (backward compat)
dynamic_variables = {item.key: item.value for item in agent_meta.DynamicVar or []}
# Overlay pre-merged values from dispatch (already has correct priority)
if meta.dynamic_values:
for k, v in meta.dynamic_values.items():
if v is not None and str(v).strip():
dynamic_variables[str(k)] = str(v)
# Built-in variables (always set, cannot be overridden)
dynamic_variables["name"] = user_name if user_name and user_name.lower() != "unknown" else "User"
dynamic_variables["date"] = datetime.now().strftime("%Y%m%d_%H%M%S")3. No Changes Needed in agent_settings.py
The sync_agent_settings function already handles agent_config correctly. The DynamicVar field on the agent document is still passed through as before. The new dynamic_values is on the meta object, not on agent_meta.
How the Merge Works (done by Node.js backend)
The Node.js calling.service.ts builds dynamic_values before dispatch:
Priority 1 (lowest): agent_config.DynamicVar → [{key: "company", value: "Acme"}]
Priority 2: campaign.dynamicVariables → {offer_code: "SUMMER2025"}
Priority 3 (highest): contact.dynamicVariables → {product_name: "Widget Pro", price: "₹999"}Result sent as dynamic_values:
{
"company": "Acme",
"offer_code": "SUMMER2025",
"product_name": "Widget Pro",
"price": "₹999"
}The Agent server doesn't need to know about the priority it's already resolved.
Example Scenario
Agent prompt:
Hi {{name}}, I'm calling from {{company}} about {{product_name}} priced at {{price}}.
We have a special offer: {{offer_code}}.Dispatch metadata for John:
{
"name": "John",
"dynamic_values": {
"company": "Acme Corp",
"product_name": "Widget Pro",
"price": "₹999",
"offer_code": "SUMMER2025"
}
}Final resolved prompt:
Hi John, I'm calling from Acme Corp about Widget Pro priced at ₹999.
We have a special offer: SUMMER2025.Dispatch metadata for Jane (different CSV row):
{
"name": "Jane",
"dynamic_values": {
"company": "Acme Corp",
"product_name": "Widget Lite",
"price": "₹499",
"offer_code": "SUMMER2025"
}
}Test Call Support
Test calls also send dynamic_values when the user fills in variable values in the test call modal. The merge is: agent DynamicVar defaults → test variable values from UI. Same field, same format.
Testing Checklist
- Backward compatibility: Campaigns without extra CSV columns →
dynamic_valuesabsent, agent usesDynamicVaronly - Contact-level override: Two contacts in same campaign get different
dynamic_values - Missing variables:
{{unknown_var}}remains as-is in prompt (no crash) - Empty values: Empty strings don't override existing defaults
- Test calls: Test calls with variable values →
dynamic_valuespresent - Test calls without values:
dynamic_valuesabsent, falls back toDynamicVar
Migration Notes
- Non-breaking change. Existing campaigns and agents continue to work.
dynamic_valuesis optional in MetadataSchema defaults toNone.- No database migration needed on the Agent server side.
- The
DynamicVaronagent_metastill works as the base layer.