Voice Agents JS Example (VIP & Repeat Caller Priority Routing)
Voice Agents JS Example - VIP & Repeat Caller Priority Routing
This example script demonstrates Vodia's JavaScript Voice Agents functionality for intelligent priority-based call routing. On every inbound call, the script evaluates two conditions and routes qualifying callers to a designated priority queue — bypassing the standard routing path.
Scenario
- Extract the caller's number from the SIP URI
fromheader. - Check the tenant address book for a contact with
category = vip. - If not VIP, query the CDR table for how many times this number has called within the past hour.
- If either condition is met, transfer the call to a priority queue.
- Otherwise, transfer to the standard destination.
Priority Conditions (OR Logic)
A caller is routed to the priority queue if either of the following is true:
- VIP contact — the caller's number exists in the tenant address book with
categoryset tovip(case-insensitive). - Repeat caller — the caller has made at least
REPEAT_THRESHOLDcalls within the lastREPEAT_WINDOW_SECseconds.
Both conditions are evaluated independently. If condition 1 is met, condition 2 is skipped entirely for efficiency.
'use strict';
// ─── Configuration ────────────────────────────────────────────────────────────
var PRIORITY_QUEUE = '400'; // Transfer destination for VIP / repeat callers
var DEFAULT_DEST = '501'; // Transfer destination for everyone else
var REPEAT_THRESHOLD = 5; // Number of calls within the window to qualify
var REPEAT_WINDOW_SEC = 3600; // Lookback window in seconds (1 hour)
// ─────────────────────────────────────────────────────────────────────────────
function extractPhoneNumber(sipUri) {
if (!sipUri) return '';
var match = sipUri.match(/sip:(\+?\d+)@/);
if (match && match[1]) return match[1];
return sipUri;
}
var fromRaw = tables['cobjs'].get(call.callid, 'from');
var toRaw = tables['cobjs'].get(call.callid, 'to');
var from = extractPhoneNumber(fromRaw);
var to = extractPhoneNumber(toRaw);
console.log('=== VIP IVR START ===');
console.log('From: ' + from + ' To: ' + to);
// ─── Step 1: Check address book for VIP category ─────────────────────────────
function checkVip(number) {
var fields = ['number', 'mobile', 'display_number'];
for (var i = 0; i < fields.length; i++) {
var ids = tables['adrbook'].search(fields[i], number);
if (ids && ids.length > 0) {
var cat = tables['adrbook'].get(ids[0], 'category');
console.log('adrbook match on field "' + fields[i] + '", category: ' + cat);
if (cat && cat.toLowerCase() === 'vip') {
return true;
}
return false;
}
}
return false;
}
// ─── Step 2: Check CDR for repeat calls within the window ────────────────────
// CDR table uses single-char compressed keys:
// f = from (full SIP URI string)
// d = domain (numeric string e.g. "6")
// t0 = start time (Unix float string e.g. "1774842260.850966")
// No searchable index on caller number — scope by domain 'd', filter in JS.
function checkRepeatCaller(number, callback) {
var nowSec = Math.floor(Date.now() / 1000);
var windowStart = nowSec - REPEAT_WINDOW_SEC;
var toRaw = tables['cobjs'].get(call.callid, 'to');
var domainMatch = toRaw && toRaw.match(/@([^>;\s]+)/);
var domainName = domainMatch ? domainMatch[1] : null;
var domainId = domainName ? String(Domain.get(domainName, '*')) : null;
console.log('CDR search: domain=' + domainId + ' number=' + number +
' windowStart=' + windowStart);
if (!domainId) {
console.log('CDR: could not resolve domain ID, skipping repeat check');
callback(false);
return;
}
tables['cdr'].search('d', domainId, function(ids) {
if (!ids || ids.length === 0) {
console.log('CDR: no records found for domain ' + domainId);
callback(false);
return;
}
console.log('CDR: ' + ids.length + ' total records in domain, scanning...');
var recentCount = 0;
for (var i = 0; i < ids.length; i++) {
var startSec = parseFloat(tables['cdr'].get(ids[i], 't0'));
if (startSec < windowStart) continue;
var fromField = tables['cdr'].get(ids[i], 'f');
var numMatch = fromField && fromField.match(/sip:(\+?\d+)@/);
if (numMatch && numMatch[1] === number) {
recentCount++;
console.log('CDR: match id=' + ids[i] + ' t0=' + startSec);
}
}
console.log('CDR: ' + recentCount + ' recent call(s) from ' + number +
' in last ' + REPEAT_WINDOW_SEC + 's (threshold: ' + REPEAT_THRESHOLD + ')');
callback(recentCount >= REPEAT_THRESHOLD);
});
}
// ─── Step 3: Route the call ───────────────────────────────────────────────────
function routeCall(isPriority, reason) {
if (isPriority) {
console.log('PRIORITY QUEUE — reason: ' + reason);
call.transfer(PRIORITY_QUEUE);
} else {
console.log('STANDARD ROUTING');
call.transfer(DEFAULT_DEST);
}
}
// ─── Main flow (OR logic) ─────────────────────────────────────────────────────
// Condition 1 OR Condition 2 → priority queue
// 1. Caller has category='vip' in address book
// 2. Caller has called >= REPEAT_THRESHOLD times in the last REPEAT_WINDOW_SEC
var isVip = checkVip(from);
console.log('VIP check result: ' + isVip);
if (isVip) {
routeCall(true, 'vip-address-book');
} else {
checkRepeatCaller(from, function(isRepeat) {
routeCall(isRepeat, 'repeat-caller');
});
}
Configuration
Adjust the four constants at the top of the script to match your deployment:
| Variable | Default | Description |
|---|---|---|
PRIORITY_QUEUE | 400 | Extension or queue to transfer priority callers to |
DEFAULT_DEST | 501 | Extension or queue for standard callers |
REPEAT_THRESHOLD | 5 | Minimum call count within the window to qualify |
REPEAT_WINDOW_SEC | 3600 | Lookback window in seconds (3600 = 1 hour) |
Marking a Contact as VIP
To flag a caller as VIP, add or update their entry in the tenant address book and set the Category field to vip. The check is case-insensitive, so VIP, Vip and vip all match.
This can be done via the Vodia web UI under Tenant → Address Book, or programmatically using the AddressBook JavaScript API.
Call Flow
Inbound call
│
▼
Extract caller number from SIP URI
│
▼
Search address book for caller number
│
├── category = 'vip' ──────────────────────────────► Transfer to PRIORITY_QUEUE
│ (reason: vip-address-book)
│
└── not VIP → query CDR table (async)
│
├── calls in window ≥ REPEAT_THRESHOLD ► Transfer to PRIORITY_QUEUE
│ (reason: repeat-caller)
│
└── below threshold ──────────────────► Transfer to DEFAULT_DEST
CDR Table Notes
The Vodia CDR table stores records using single-character compressed field keys. This script uses the following:
| Key | Field | Format |
|---|---|---|
f | From (caller) | Full SIP URI, e.g. "Name" <sip:+61200000000@domain> |
d | Domain | Numeric tenant ID string, e.g. "6" |
t0 | Call start time | Unix timestamp float string, e.g. "1774842260.850966" |
Because the CDR table has no indexed search on caller number, the script scopes the query by domain first using search('d', domainId), then iterates the result set in JavaScript — extracting and comparing the number from the f SIP URI string, and filtering by the t0 timestamp against the lookback window.
On tenants with a large CDR history, the domain-scoped scan processes all records in memory. If call volumes are very high, consider reducing REPEAT_WINDOW_SEC or increasing REPEAT_THRESHOLD to keep the qualifying set small and routing decisions fast.
The VIP address book check is synchronous and completes before the CDR query is initiated. If a caller is marked VIP, the CDR table is never queried, keeping latency minimal for known VIP callers.
The repeat caller count includes all calls from that number to the tenant within the window — not just calls to this specific IVR node. This reflects total contact frequency across the tenant.
Ensure the IVR node this script is assigned to is reachable from your inbound trunk routing. The script calls call.transfer() immediately upon a routing decision — there is no hold music or queue announcement built in. Add a call.say() before the transfer calls if an announcement is required.
For more information on Vodia's JavaScript capabilities, refer to: Vodia Backend JavaScript Documentation