This workflow follows the Chainllm → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Voice Agent Pro: Agentic Booking with TTS and Robust Error Handling",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "voice-agent-pro",
"responseMode": "responseNode",
"options": {}
},
"id": "2c484d2e-8645-44de-98d5-0fbd4ccee388",
"name": "Webhook Trigger1",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.1,
"position": [
-688,
-2304
]
},
{
"parameters": {
"jsCode": "// Context Builder Function Node\n// Builds comprehensive context for LLM based on session state and conversation history\n\nconst webhookData = $('Webhook Trigger1').item.json;\nconst sttData = $input.item.json;\n\n// Get form fields from webhook\nconst sessionState = JSON.parse(webhookData.sessionState || '{}');\nconst sessionId = webhookData.sessionId || '';\nconst authToken = webhookData.authToken || '';\n\n// Get transcript from STT output\nconst transcript = sttData.text || sttData.transcript || sttData.data?.text || '';\n\n// Determine current step and build context accordingly\nconst currentStep = sessionState.currentStep || 'org_search';\nconst selectedOrgId = sessionState.selectedOrganizationId;\nconst selectedDeptId = sessionState.selectedDepartmentId;\nconst selectedProviderId = sessionState.selectedProviderId;\nconst userId = sessionState.userId;\nconst conversationHistory = sessionState.conversationHistory || [];\n\n// Add current user message to history for the LLM prompt\nconst messages = [...conversationHistory, { role: 'user', content: transcript }];\n\n// Base system prompt\nlet systemPrompt = `You are a helpful, professional voice assistant for a booking system. Your role is to guide users through booking appointments with healthcare providers.\n\nAvailable steps in the booking flow:\n1. org_search - User is searching for an organization\n2. org_selected - User has selected an organization\n3. auth_required - User needs to authenticate\n4. dept_selection - User is selecting a department\n5. provider_selection - User is selecting a provider\n6. availability_query - User is querying provider availability\n7. booking - User is creating a booking\n\nCurrent session state:\n- Step: ${currentStep}\n- Selected Organization ID: ${selectedOrgId || 'None'}\n- Selected Department ID: ${selectedDeptId || 'None'}\n- Selected Provider ID: ${selectedProviderId || 'None'}\n- User ID: ${userId || 'Not authenticated'}\n\nYou must respond with a JSON object in this exact format:\n{\n \"responseText\": \"Natural language response for the user\",\n \"action\": {\n \"type\": \"search_org\" | \"select_org\" | \"list_departments\" | \"list_providers\" | \"query_availability\" | \"create_booking\" | \"auth_required\" | \"continue_conversation\" | \"help\",\n \"parameters\": {\n \"orgId\": \"string (optional)\",\n \"deptId\": \"string (optional)\",\n \"providerId\": \"string (optional)\",\n \"eventId\": \"string (optional)\",\n \"query\": \"string (optional)\"\n }\n },\n \"sessionUpdate\": {\n \"currentStep\": \"string\",\n \"selectedOrganizationId\": \"string (optional)\",\n \"selectedDepartmentId\": \"string (optional)\",\n \"selectedProviderId\": \"string (optional)\",\n \"conversationHistory\": [ /* Array of {role: string, content: string} objects for the last 5 turns */ ]\n }\n}`;\n\n// Add step-specific context (omitted for brevity, assume similar to original)\n\n// Build a single prompt string for the LLM Chain node\nconst conversationText = messages.map(m => m.role + ': ' + m.content).join('\\n');\nconst fullPrompt = systemPrompt + '\\n\\nConversation so far:\\n' + conversationText + '\\n\\nRespond with the JSON object as specified above.';\n\nreturn {\n json: {\n systemPrompt,\n messages,\n fullPrompt,\n sessionId,\n authToken,\n sessionState: JSON.stringify(sessionState),\n originalTranscript: transcript\n }\n};"
},
"id": "a37d02b5-720e-4316-957a-cd6f34a0bd3f",
"name": "Context Builder (History)1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-208,
-2272
]
},
{
"parameters": {
"jsCode": "// Parse LLM response and extract action, and update history\n// Basic LLM Chain (langchain) outputs { response: \"...\" } instead of OpenAI format\nconst llmResponse = $input.item.json.response || $input.item.json.text || '';\nlet parsedResponse;\n\ntry {\n parsedResponse = JSON.parse(llmResponse);\n} catch (error) {\n // Fallback if JSON parsing fails\n parsedResponse = {\n responseText: 'I apologize, I had a momentary lapse. Could you please repeat that?',\n action: { type: 'continue_conversation' },\n sessionUpdate: {}\n };\n}\n\n// Extract data\nconst actionType = parsedResponse.action?.type || 'continue_conversation';\nconst actionParams = parsedResponse.action?.parameters || {};\nconst responseText = parsedResponse.responseText || 'I understand.';\nlet sessionUpdate = parsedResponse.sessionUpdate || {};\n\n// Get context from previous node\nconst context = $('Context Builder (History)1').item.json;\nconst transcript = context.originalTranscript;\nconst conversationHistory = JSON.parse(context.sessionState).conversationHistory || [];\n\n// Update conversation history for next turn\nconst newHistory = [...conversationHistory];\nnewHistory.push({ role: 'user', content: transcript });\nnewHistory.push({ role: 'assistant', content: responseText });\n\n// Keep only the last 5 turns (10 messages)\nsessionUpdate.conversationHistory = newHistory.slice(-10);\n\nreturn {\n json: {\n actionType,\n actionParams,\n responseText,\n sessionUpdate,\n sessionId: context.sessionId,\n authToken: context.authToken,\n sessionState: JSON.parse(context.sessionState),\n transcript,\n requiresApiCall: ['search_org', 'list_departments', 'list_providers', 'query_availability', 'create_booking'].includes(actionType)\n }\n};"
},
"id": "e8043379-9c79-4ebe-93cd-c961155dcd46",
"name": "Parse Action & Update History1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
192,
-2304
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.actionType }}",
"rightValue": "search_org",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.actionType }}",
"rightValue": "list_departments",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.actionType }}",
"rightValue": "list_providers",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.actionType }}",
"rightValue": "query_availability",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.actionType }}",
"rightValue": "create_booking",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.actionType }}",
"rightValue": "auth_required",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "a3792b97-eed8-4761-971c-f85e7b4fd410",
"name": "Switch Action Type1",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
416,
-2304
]
},
{
"parameters": {
"url": "={{ $env.FASTIFY_API_URL }}/api/organizations/search",
"options": {}
},
"id": "6e2f8896-6bd6-4d72-9e2e-a300e9d15b3b",
"name": "API: Search Organizations1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
640,
-2464
]
},
{
"parameters": {
"url": "={{ $env.FASTIFY_API_URL }}/api/client/organizations/{{ $json.actionParams.orgId }}/departments",
"authentication": "header",
"options": {}
},
"id": "00a24cf2-a66f-41aa-b6f4-e4adb184f9cd",
"name": "API: List Departments1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
640,
-2336
]
},
{
"parameters": {
"url": "={{ $env.FASTIFY_API_URL }}/api/client/departments/{{ $json.actionParams.deptId }}/providers",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{}
]
},
"options": {}
},
"id": "9e325ae4-190f-4cc9-8175-14324461c4ed",
"name": "API: List Providers1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
640,
-2224
]
},
{
"parameters": {
"url": "={{ $env.FASTIFY_API_URL }}/api/client/providers/{{ $json.actionParams.providerId }}/available-events",
"authentication": "header",
"options": {}
},
"id": "61775aa2-a353-44f0-bf8b-e0217b7f3fab",
"name": "API: Query Availability1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
640,
-2096
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.FASTIFY_API_URL }}/api/client/bookings",
"authentication": "header",
"options": {}
},
"id": "db9eac41-8f8c-4946-b496-cd1ea74c1b7d",
"name": "API: Create Booking1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
640,
-1984
]
},
{
"parameters": {
"jsCode": "// API Response Handler\n// Checks for 401 Unauthorized and generates a new response for the LLM to process\n\nconst apiResponse = $input.item.json;\nconst statusCode = apiResponse.statusCode;\nconst originalData = $('Parse Action & Update History1').item.json;\n\nif (statusCode === 401) {\n // Authentication required\n return {\n json: {\n responseText: 'Authentication is required for this action. Please log in.',\n actionType: 'auth_required',\n apiResponse: apiResponse\n }\n };\n} else if (statusCode >= 400) {\n // General API error\n return {\n json: {\n responseText: 'I encountered an error while communicating with the booking system. The error was: ' + (apiResponse.statusText || 'Unknown Error'),\n actionType: 'continue_conversation',\n apiResponse: apiResponse\n }\n };\n} else {\n // Successful API call\n return {\n json: {\n responseText: 'API_SUCCESS_PROCEED_TO_SUMMARY',\n actionType: originalData.actionType,\n apiResponse: apiResponse\n }\n };\n}"
},
"id": "1e682ed4-a260-4946-9372-cbe8810a74cf",
"name": "API Response Handler1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
864,
-2224
]
},
{
"parameters": {
"prompt": "=Summarize the following API response in a natural, conversational way for a voice assistant user. Be concise and clear.\n\nAction type: {{ $json.actionType }}\nAPI Response: {{ JSON.stringify($json.apiResponse) }}\n\nProvide a brief, friendly summary that the user can understand.",
"batching": {}
},
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"typeVersion": 1.9,
"position": [
1088,
-2224
],
"id": "b8cc04ff-4d95-41f0-a2d0-3dd5bd99ef73",
"name": "LLM: Summarize API Result1"
},
{
"parameters": {
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"typeVersion": 1,
"position": [
1088,
-2096
],
"id": "a9f1c2d3-e4b5-6789-abcd-ef0123456789",
"name": "Google Gemini Chat Model (Summarize)",
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.LOGGING_API_URL }}/log/voice-agent",
"options": {}
},
"id": "46962006-3e6c-44dd-9169-020fdfaa1dec",
"name": "Transaction Logger1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1312,
-2224
]
},
{
"parameters": {
"respondWith": "json",
"options": {}
},
"id": "ac2359f0-ed70-4f6a-80fd-f5de9973383a",
"name": "Webhook Response (Final)1",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1744,
-2224
]
},
{
"parameters": {
"jsCode": "// Error Handler\n// Catches errors from STT, LLM, or API and generates a friendly response.\n\nconst errorData = $input.item.error;\nconst originalData = $('Webhook Trigger1').item.json;\n\nconst errorType = errorData.context.node.name;\nconst errorMessage = errorData.message;\n\n// Log the error internally (optional, can be replaced by a dedicated logging node)\nconsole.error(`Error in ${errorType}: ${errorMessage}`);\n\nreturn {\n json: {\n responseText: 'I am sorry, I seem to have run into a technical issue while processing your request. Please try again or rephrase your question.',\n sessionUpdate: JSON.parse(originalData.sessionState || '{}'),\n errorDetails: { type: errorType, message: errorMessage }\n }\n};"
},
"id": "7656473e-c6d9-45a1-bf80-1657d4b170ae",
"name": "Error Handler1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
-1792
]
},
{
"parameters": {
"respondWith": "json",
"options": {}
},
"id": "809347a6-1069-43e3-b106-fdb7d6abfdc0",
"name": "Webhook Response (Error)1",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1088,
-1792
]
},
{
"parameters": {
"prompt": "={{ $json.fullPrompt }}",
"batching": {}
},
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"typeVersion": 1.9,
"position": [
-16,
-2304
],
"id": "11049307-2d64-4ed2-91e1-bd2c24834eb1",
"name": "Basic LLM Chain"
},
{
"parameters": {
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"typeVersion": 1,
"position": [
-16,
-2176
],
"id": "5f3de4cf-3d1f-4cd4-b89b-eeb30fc8ed07",
"name": "Google Gemini Chat Model",
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Webhook Trigger1": {
"main": [
[
{
"node": "Context Builder (History)1",
"type": "main",
"index": 0
}
]
]
},
"Context Builder (History)1": {
"main": [
[
{
"node": "Basic LLM Chain",
"type": "main",
"index": 0
}
]
]
},
"Basic LLM Chain": {
"main": [
[
{
"node": "Parse Action & Update History1",
"type": "main",
"index": 0
}
]
]
},
"Parse Action & Update History1": {
"main": [
[
{
"node": "Switch Action Type1",
"type": "main",
"index": 0
}
]
]
},
"Switch Action Type1": {
"main": [
[
{
"node": "API: Search Organizations1",
"type": "main",
"index": 0
}
],
[
{
"node": "API: List Departments1",
"type": "main",
"index": 0
}
],
[
{
"node": "API: List Providers1",
"type": "main",
"index": 0
}
],
[
{
"node": "API: Query Availability1",
"type": "main",
"index": 0
}
],
[
{
"node": "API: Create Booking1",
"type": "main",
"index": 0
}
],
[
{
"node": "Webhook Response (Final)1",
"type": "main",
"index": 0
}
],
[
{
"node": "Webhook Response (Final)1",
"type": "main",
"index": 0
}
]
]
},
"API: Search Organizations1": {
"main": [
[
{
"node": "API Response Handler1",
"type": "main",
"index": 0
}
]
]
},
"API: List Departments1": {
"main": [
[
{
"node": "API Response Handler1",
"type": "main",
"index": 0
}
]
]
},
"API: List Providers1": {
"main": [
[
{
"node": "API Response Handler1",
"type": "main",
"index": 0
}
]
]
},
"API: Query Availability1": {
"main": [
[
{
"node": "API Response Handler1",
"type": "main",
"index": 0
}
]
]
},
"API: Create Booking1": {
"main": [
[
{
"node": "API Response Handler1",
"type": "main",
"index": 0
}
]
]
},
"API Response Handler1": {
"main": [
[
{
"node": "LLM: Summarize API Result1",
"type": "main",
"index": 0
}
]
]
},
"LLM: Summarize API Result1": {
"main": [
[
{
"node": "Transaction Logger1",
"type": "main",
"index": 0
}
]
]
},
"Transaction Logger1": {
"main": [
[
{
"node": "Webhook Response (Final)1",
"type": "main",
"index": 0
}
]
]
},
"Error Handler1": {
"main": [
[
{
"node": "Webhook Response (Error)1",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "Basic LLM Chain",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Google Gemini Chat Model (Summarize)": {
"ai_languageModel": [
[
{
"node": "LLM: Summarize API Result1",
"type": "ai_languageModel",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "0ff04401-4582-4e04-9afe-d0a77ee9464b",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "XS1G1gyAzzS1uDAN",
"tags": []
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googlePalmApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Voice Agent Pro: Agentic Booking with TTS and Robust Error Handling. Uses httpRequest, chainLlm, lmChatGoogleGemini. Webhook trigger; 18 nodes.
Source: https://github.com/hbela/booking-n8n-workflows/blob/8c9a15486d7ffe0de8d4a4a4d1ae4befb615f5b3/workflows/voice-event.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
ANIS_HUB 1. Uses gmail, googleDrive, googleSheets, httpRequest. Webhook trigger; 89 nodes.
CLINICAINTEGRAL_secretary. Uses postgres, mcpClientTool, googleDriveTool, toolWorkflow. Webhook trigger; 89 nodes.
secretaria. Uses postgres, n8n-nodes-evolution-api, openAi, httpRequest. Webhook trigger; 71 nodes.
Resume Screening & Behavioral Interviews with Gemini, Elevenlabs, & Notion ATS copy. Uses outputParserStructured, chainLlm, googleDrive, stickyNote. Webhook trigger; 67 nodes.
Candidate Engagement | Resume Screening | AI Voice Interviews | Applicant Insights