Warm Transfer (Announce and Transfer)
Description
This scenario is based upon issuing two identical API requests, named fork connected call. The first fork request is issued
to initiate the transfer, while the second fork request is issued after the transfer destination answers. In both cases,
the dialing and connecting to the caller is performed via simple CXML documents.
Workflow Outline
- An inbound call is accepted at Cloudonix for the phone number
+12127773456. - The call is routed to ReTell using the Register Phone Call API request.
- The voice agent initiated a warm transfer workflow on the n8n server:
3.1. The voice agent issues aswitch voice applicationrequest to Cloudonix, using the agent destination number and a well-formatted CXML document.
3.2. Cloudonix transfers the inbound caller to a pre-defined conference room, waiting for the transfer destination to be connected.
3.3. the workflow initiates an outbound call from Cloudonix to the human agent, using theStart outbound callAPI request. - Cloudonix removes the AI agent completely from the existing call.
- The human agent answers the outbound call, listens to a pre-amble message and connects to the previously created conference room.
Workflow Implementation
To implement this workflow, you would need to:
- Create a Container Application to process the inbound call to Cloudonix and respond with a static CXML document.
- Create a Voice Application to execute the Container Application mentioned in step 1, based upon the inbound call phone number.
- Create a ReTell function, to initiate the transfer, based upon a static CXML application that will be included in the function call.
- Create a n8n workflow to implement the custom function, described in the above workflow outline, in items 3.1 to 3.3.
Step 1: Inbound Calls Processing Container Application
Use the script in the following example, to route calls from the world to your Retell voice agent, via Cloudonix.
Retellai: Pre-Call Webhook Routing
We will assume that your container application is named smartTransfers and that the relevant container application block is named staticRoutingToReTell.
Sample warmTransfers container application code
const httpClient = require('https');
/* ReTell API Config */
const retell_token = '{{ put_your_retell_api_token_here }}';
let retell_agent = '{{ put_your_default_retell_agent_id_here }}';
/* This function provides a simple way to manage multiple inbound phone numbers routing */
function selectAgentIdByNumber(ev) {
switch (ev.parameters.To) {
case "+12127773456":
case "12127773456":
retell_agent='your_agent_id_here';
break;
default:
retell_agent='you_default_agent_id_here';
break;
}
}
/* Handle HTTP outbound requests to Retell and other APIs */
async function httpsRequest(url, method = 'GET', headers = {}, body = null, requestTimeout = 5000) {
const start = Date.now();
let response;
if (method === 'POST') {
response = await httpClient.post(url, {headers, body});
} else if (method === 'PUT') {
response = await httpClient.put(url, {headers, body});
} else if (method === 'DELETE') {
response = await httpClient.delete(url, {headers});
} else if (method === 'GET') {
response = await httpClient.get(url, {headers});
} else {
throw new Error(`Unsupported method: ${method}`);
}
const elapsed = Date.now() - start;
if (elapsed > requestTimeout) {
console.log(`[httpsRequest] HTTP Request ${method} to ${url} timed out after ${elapsed} mSec`);
return false;
}
const status = response.status || response.statusCode || 0;
const ok = status >= 200 && status < 300;
const responseBody = typeof response.body === 'string'
? response.body : JSON.stringify(response.body || {});
return {
status,
ok,
body: responseBody,
headers: response.headers || {}
};
}
/* Register a new phone call on Retell */
async function getRetellCallId(ev) {
const url = 'https://api.retellai.com/v2/register-phone-call';
// SIP headers from Cloudonix
const inboundSipHeaders = JSON.parse(ev.body).SessionData.profile['trunk-sip-headers'];
/* Register the Cloudonix LLM variables, we'll need these later on */
const retellDynamicVariables = {
cloudonix_callerid: ev.parameters.From,
cloudonix_destination: ev.parameters.To,
cloudonix_token: ev.parameters.Session,
cloudonix_domain: ev.parameters.Domain
};
/* Document the inbound SIP headers, these come in handy for debugging purposes */
for (const key in inboundSipHeaders) {
retellDynamicVariables[key] = inboundSipHeaders[key];
}
const payload = {
agent_id: retell_agent,
from_number: ev.parameters.From,
to_number: ev.parameters.To,
retell_llm_dynamic_variables: retellDynamicVariables
};
console.log(`[getRetellCallId] Payload: ${JSON.stringify(payload)}`);
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + retell_token
};
try {
const response = await httpsRequest(url, 'POST', headers, JSON.stringify(payload));
if (!response.ok) {
throw new Error(`[getRetellCallId] ReTell call registration failed: ${response.status} - ${response.body}`);
}
const data = JSON.parse(response.body);
console.log(`[getRetellCallId] Success: ${data.call_id}`);
return data.call_id;
} catch (error) {
console.error(`[getRetellCallId] Error:`, error.message || error);
await deleteSession(ev.parameters.Domain, ev.parameters.Session);
return false;
}
}
/* Build a simple Retell dialing CXML document */
function dialToRetell(ev, retellCallId) {
let result = `<Dial callerId="${ev.parameters.From}">\n`;
result += ` <Service provider="retell">${retellCallId}</Service>\n`;
result += `</Dial>`;
return result;
}
/* Build the formatted CXML response */
function response(content) {
let result = `<?xml version="1.0"?>`;
result += `<Response>${content}</Response>`;
return result;
}
/* Container Application Request Handler */
exports.handler = async (ev) => {
selectAgentIdByNumber(ev);
const retellCallId = await getRetellCallId(ev);
if (!retellCallId) {
console.log(`[handler] ReTell call registration failed`);
return response("<Reject reason='rejected' />");
}
const retellCxml = dialToRetell(ev, retellCallId);
return response(retellCxml);
};
Step 2: Voice Application Attachment and Call Routing
Now, let's attach our Container Application to a new Voice Application and define the inbound phone number. From the Cloudonix dashboard, click the Applications menu option, and create a a new Voice Application, using the following parameters:
| Parameter | Value |
|---|---|
Application Name | eg. inbound_call_to_retell |
Application Resource Type | Container Application Resource |
Container Application Resource | Select the previously created warmTransfers container application |
Entry Block | Select the previous created main block of the warmTransfers container application |
Once completed, a new line will be added to your voice applications view, containing the following information:
| Name | Application Resource ID | DNID |
|---|---|---|
inbound_call_to_retell | warmTransfers::main | - |
Now, click the cog icon on the right side, to edit the voice application settings. Once in the next screen, click the plus button, to create a new DNID (Phone number) to assign to your voice application:

Let's review the available options for the DNID:
| Option | Description |
|---|---|
DNID | A string representing the DNID to attach to the application, or a pattern of. |
Prefix Match | The DNID entered is formatted as a Prefix Match showing the start of the DNID only. |
Glob Wildcard Expression | The DNID entered is formatted as a Global Wildcard Expression, eg: 1212777*. |
Asterisk Expression | The DNID entered is foramatted as an Asterisk dialplan expression, eg: 1212777XXXX. |
Regular Expression | The DNID entered is formatted as a Regular Expression. |
To learn more about container applications, voice applications and phone numbers (DNIDs), click any of the links below:
Step 3: ReTell Function for Transfer Initiation
Unlike ReTell's normal transfer calling function, we need to implement a new transfer function, using a Custom Fucntion. Below is what our customer function will look like:
- Function Name:
warm_transfer - Description:
Execute all required API requests and workflows to implement the warm transfer functionality - Endpoint:
https://your.favorite.nocode.platform/warm_transfer - Parameters:
{
"type": "object",
"properties": {
"transfer_destination": {
"type": "string",
"description": "Where should the call be transferred to"
},
"transfer_message": {
"type": "string",
"description": "The message to playback to the human agent"
},
"cloudonix_conference": {
"type": "string", /* Will consist of the first 10 characters of the llm_variable `cloudonix_token` */
"description": "The Cloudonix conference ID to connect to"
}
},
"required": [
"transfer_destination",
"transfer_message",
"cloudonix_conference"
]
}
Once your custom function is invoked, it should produce the following HTTP Requests to Cloudonix:
Step 3.1: Put inbound caller into conference room
- Endpoint URL:
https://api.cloudonix.io/calls/{cloudonix_domain}/sessions/{cloudonix_token}/application - Endpoint Method:
POST - Request Body:
{
"cxml": "<Response><Dial beep='false' holdMusic='true' endConferenceOnExit='true'>{cloudonix_conference}</Dial></Response>",
"method": "POST"
}
Step 3.2: Generate an outbound call to the human agent
- Endpoint URL:
https://api.cloudonix.io/calls/{cloudonix_domain}/application - Endpoint Method:
POST - Request Body:
{
"destination": "{transfer_destination}",
"caller-id": "{cloudonix_callerid}",
"execute": "connected",
"timeout": 120,
"trunk": "your-outbound-trunk-name",
"cxml": "<Response><Say>{transfer_message}</Say><Pause length='1' /><Dial beep='false' holdMusic='true' endConferenceOnExit='true'>{cloudonix_conference}</Dial></Response>",
"method": "POST"
}
The above {cloudonix_domain}, {cloudonix_token}, are {cloudonix_callerid} are llm_variables, these should be pre-populated by your no-code scenario (or server side).