AI Voice Agent Backend
Module 13 · ai · intermediate
starter.json
In n8n, copy this file and paste it onto the canvas, or use Import from File.
{
"name": "13 - AI Voice Agent Backend (starter)",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "voice/tools",
"responseMode": "responseNode",
"options": {}
},
"id": "m13-tools-webhook",
"name": "Voice Tools API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
200
],
"webhookId": "course-13-voice-tools"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "b1",
"name": "business_name",
"value": "Bright Smile Dental",
"type": "string"
},
{
"id": "b2",
"name": "open_hour",
"value": 9,
"type": "number"
},
{
"id": "b3",
"name": "close_hour",
"value": 17,
"type": "number"
},
{
"id": "b4",
"name": "open_days",
"value": "={{ [1, 2, 3, 4, 5] }}",
"type": "array"
},
{
"id": "b5",
"name": "default_service",
"value": "a check-up",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "m13-settings",
"name": "Business Settings",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
220,
200
]
},
{
"parameters": {
"jsCode": "// Turn any voice platform's tool-call payload into one item per tool call:\n// { platform, tool, args, tool_call_id }\nconst out = [];\nfor (const item of $input.all()) {\n const body = item.json.body ?? {};\n\n if (body.message?.toolCallList) {\n // Vapi: { message: { type: 'tool-calls', toolCallList: [{ id, function: { name, arguments } }] } }\n for (const tc of body.message.toolCallList) {\n const fn = tc.function ?? tc;\n let args = fn.arguments ?? fn.parameters ?? {};\n if (typeof args === 'string') {\n try { args = JSON.parse(args); } catch (e) { args = {}; }\n }\n out.push({ json: { platform: 'vapi', tool: fn.name, args, tool_call_id: tc.id } });\n }\n } else if (body.name) {\n // Retell: { name, args, call }\n out.push({ json: { platform: 'retell', tool: body.name, args: body.args ?? {}, tool_call_id: null } });\n } else {\n // Retell \"args only\" mode or a plain test call: the tool name comes from ?tool=\n out.push({ json: { platform: 'plain', tool: item.json.query?.tool ?? 'unknown', args: body, tool_call_id: null } });\n }\n}\nreturn out;\n"
},
"id": "m13-normalize",
"name": "Normalize Tool Calls",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
440,
200
]
},
{
"parameters": {
"jsCode": "// check_availability({ date: 'YYYY-MM-DD' }) → free slots that day\nconst s = $('Business Settings').first().json;\nconst db = $getWorkflowStaticData('global');\ndb.bookings = db.bookings ?? {};\n\nconst speak = (t) => DateTime.fromFormat(t, 'HH:mm').toFormat('h:mm a').replace(':00', '');\nconst speakList = (a) => (a.length <= 1 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a.at(-1));\n\nreturn $input.all().map(({ json }) => {\n const date = String(json.args.date ?? '').trim();\n const day = DateTime.fromISO(date);\n let result;\n\n if (!day.isValid) {\n result = { ok: false, message: 'Which day would you like? Please tell me the date.' };\n } else if (day.startOf('day') < DateTime.now().startOf('day')) {\n result = { ok: false, message: 'That date is in the past. Which upcoming day works for you?' };\n } else if (!s.open_days.includes(day.weekday)) {\n result = { ok: false, date, available_slots: [], message: `We're closed on ${day.toFormat('cccc')}s. We're open Monday to Friday.` };\n } else {\n const all = [];\n for (let h = s.open_hour; h < s.close_hour; h++) all.push(String(h).padStart(2, '0') + ':00');\n const taken = Object.values(db.bookings).filter((b) => b.date === date).map((b) => b.time);\n const free = all.filter((t) => !taken.includes(t));\n result = free.length\n ? { ok: true, date, available_slots: free, message: `On ${day.toFormat('cccc, LLLL d')} we have ${speakList(free.map(speak))} available.` }\n : { ok: true, date, available_slots: [], message: `Sorry, ${day.toFormat('cccc, LLLL d')} is fully booked. Would another day work?` };\n }\n return { json: { ...json, result } };\n});\n"
},
"id": "m13-avail",
"name": "check_availability",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
-40
]
},
{
"parameters": {
"jsCode": "// Shape the reply the way each platform expects.\n// Vapi: { results: [{ toolCallId, result }] } · Retell / plain: the result object itself\nconst items = $input.all().map((i) => i.json);\nif (items[0]?.platform === 'vapi') {\n return [{ json: { results: items.map((i) => ({ toolCallId: i.tool_call_id, result: JSON.stringify(i.result) })) } }];\n}\nreturn [{ json: items.length === 1 ? items[0].result : { results: items.map((i) => i.result) } }];\n"
},
"id": "m13-format",
"name": "Format for Platform",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
200
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "m13-respond",
"name": "Respond",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1360,
200
]
},
{
"parameters": {
"content": "## Your task\n1. Read **Normalize Tool Calls**: one item per tool call, whatever the platform (Retell, Vapi, args-only).\n2. Add a **Switch** on `{{ $json.tool }}` with outputs for check_availability, book_appointment, cancel_appointment, lookup_customer + a fallback.\n3. Write **book_appointment** and **cancel_appointment** Code nodes that store bookings in `$getWorkflowStaticData('global')`.\n4. Add **lookup_customer** and an Unknown Tool reply. Every tool returns `{ ok, message }`, where message is a sentence the agent can say out loud.\n5. Second flow: **Call Ended** webhook → normalize → AI summary (Information Extractor) → follow-up IF.\n\nTest: `./test/test.sh` (workflow must be published)",
"height": 260,
"width": 420
},
"id": "starter-task-note",
"name": "Your Task",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
0,
-340
]
}
],
"connections": {
"Voice Tools API": {
"main": [
[
{
"node": "Business Settings",
"type": "main",
"index": 0
}
]
]
},
"Business Settings": {
"main": [
[
{
"node": "Normalize Tool Calls",
"type": "main",
"index": 0
}
]
]
},
"Normalize Tool Calls": {
"main": [
[
{
"node": "check_availability",
"type": "main",
"index": 0
}
]
]
},
"check_availability": {
"main": [
[
{
"node": "Format for Platform",
"type": "main",
"index": 0
}
]
]
},
"Format for Platform": {
"main": [
[
{
"node": "Respond",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
}
}