This workflow corresponds to n8n.io template #14914 — we link there as the canonical source.
This workflow follows the Agent → Google Calendar 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 →
{
"id": "a8tblGbKxqvTjRTe",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Voice-Activated Travel Assistant with Claude AI",
"tags": [],
"nodes": [
{
"id": "62124a33-21f9-4aea-8a63-e0c3fdbb5092",
"name": "Main Documentation",
"type": "n8n-nodes-base.stickyNote",
"position": [
-16,
304
],
"parameters": {
"width": 820,
"height": 992,
"content": "## Voice-Activated Travel Assistant with Claude AI\n\nA hands-free travel planning assistant that accepts voice messages via WhatsApp and Telegram, understands natural language travel requests, searches across multiple providers, and automatically books to your calendar with smart recommendations.\n\n### How it works\n\n1. **Voice Message Reception** - WhatsApp/Telegram webhooks capture incoming voice notes and calls\n2. **Audio Transcription** - Converts voice to text using OpenAI Whisper or Google Speech-to-Text\n3. **Intent Classification** - Claude AI analyzes the request to determine travel intent and parameters\n4. **Context Enrichment** - Pulls user preferences, past trips, and budget profiles from database\n5. **Multi-Source Travel Search** - Queries flights (Skyscanner), hotels (Booking.com), activities in parallel\n6. **Smart Filtering & Ranking** - AI applies user preferences, budget constraints, and optimal timing\n7. **Natural Response Generation** - Claude crafts conversational voice-friendly responses\n8. **Calendar Auto-Add** - Creates Google Calendar events with travel details and reminders\n9. **Voice Response Delivery** - Sends text + voice message back via original messaging platform\n10. **Confirmation & Booking Links** - Provides quick-action buttons for booking or modifying search\n11. **Proactive Follow-ups** - Sends price drop alerts and departure reminders\n12. **Multi-Turn Conversation** - Maintains context for refinement requests\n\n### Setup Steps\n\n1. Import workflow into n8n\n2. Configure credentials:\n - **Anthropic API** - Claude AI for NLP and response generation\n - **OpenAI API** - Whisper for voice transcription\n - **WhatsApp Business API** - Voice message reception and sending\n - **Telegram Bot API** - Alternative messaging platform\n - **Google Calendar API** - Automatic event creation\n - **Flight Search API** - Skyscanner, Amadeus, or Kiwi.com\n - **Hotel API** - Booking.com or Hotels.com partner API\n - **Google Sheets** - User preferences and conversation history\n - **MongoDB or PostgreSQL** - Conversation state management\n3. Set up WhatsApp Business account and webhook\n4. Create Telegram bot via @BotFather\n5. Configure Google Calendar shared calendar for travel\n6. Populate user preferences sheet with defaults\n7. Set API keys for travel search providers\n8. Activate workflow and test with sample voice message\n\n"
},
"typeVersion": 1
},
{
"id": "512961ee-0137-4997-b9f8-65253b9a4610",
"name": "Section 1 Header",
"type": "n8n-nodes-base.stickyNote",
"position": [
992,
624
],
"parameters": {
"color": 3,
"width": 616,
"height": 420,
"content": "## 1. Voice Reception & Transcription"
},
"typeVersion": 1
},
{
"id": "a3165670-3a26-4933-aa44-59554ef72814",
"name": "Section 2 Header",
"type": "n8n-nodes-base.stickyNote",
"position": [
1634,
144
],
"parameters": {
"color": 5,
"width": 780,
"height": 1168,
"content": "## 2. NLP Understanding & Context Enrichment"
},
"typeVersion": 1
},
{
"id": "c00db148-c476-4605-9b54-ce4dae6b7990",
"name": "Section 3 Header",
"type": "n8n-nodes-base.stickyNote",
"position": [
2432,
304
],
"parameters": {
"color": 3,
"width": 1104,
"height": 1020,
"content": "## 3. Multi-Source Travel Search & Smart Ranking"
},
"typeVersion": 1
},
{
"id": "7ed73a54-5ffc-428f-acf2-f9820b920766",
"name": "Section 4 Header",
"type": "n8n-nodes-base.stickyNote",
"position": [
3584,
400
],
"parameters": {
"color": 5,
"width": 980,
"height": 900,
"content": "## 4. Response Generation, Calendar & Delivery"
},
"typeVersion": 1
},
{
"id": "1582a4c4-4f4c-4fbe-920d-25ceee00da31",
"name": "WhatsApp Voice Message Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
1056,
716
],
"parameters": {
"path": "whatsapp-voice",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "c4224756-51c9-4d46-b171-3d8b7e79210e",
"name": "Telegram Voice Note Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
1056,
908
],
"parameters": {
"path": "telegram-voice",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "7fb052d8-bd1b-4388-801f-854616d6cd5a",
"name": "Normalize Message Input",
"type": "n8n-nodes-base.code",
"position": [
1280,
864
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const body = $input.item.json.body || $input.item.json;\nconst headers = $input.item.json.headers || {};\n\n// Detect platform from webhook path or payload structure\nlet platform = 'unknown';\nlet audioUrl = null;\nlet senderId = null;\nlet messageId = null;\nlet audioMetadata = {};\n\nif (body.platform === 'whatsapp' || body.object === 'whatsapp_business_account') {\n platform = 'whatsapp';\n const message = body.entry?.[0]?.changes?.[0]?.value?.messages?.[0] || body;\n senderId = message.from || body.from;\n messageId = message.id || body.messageId;\n \n if (message.type === 'audio' || message.audio) {\n const audio = message.audio || {};\n audioUrl = audio.url || body.audio?.url;\n audioMetadata = {\n mimeType: audio.mime_type || audio.mimeType || 'audio/ogg',\n duration: audio.duration || 0,\n sha256: audio.sha256 || null\n };\n } else if (message.type === 'voice' || message.voice) {\n const voice = message.voice || {};\n audioUrl = voice.url || body.audio?.url;\n audioMetadata = {\n mimeType: 'audio/ogg',\n duration: voice.duration || 0,\n sha256: voice.sha256 || null\n };\n }\n} else if (body.update_id || body.message?.voice || body.platform === 'telegram') {\n platform = 'telegram';\n const message = body.message || body;\n senderId = message.from?.id?.toString() || message.chat?.id?.toString() || body.from;\n messageId = message.message_id?.toString() || body.messageId;\n \n if (message.voice) {\n audioUrl = `https://api.telegram.org/file/bot${process.env.TELEGRAM_BOT_TOKEN}/${message.voice.file_path}`;\n audioMetadata = {\n mimeType: message.voice.mime_type || 'audio/ogg',\n duration: message.voice.duration || 0,\n fileId: message.voice.file_id\n };\n } else if (message.audio) {\n audioUrl = `https://api.telegram.org/file/bot${process.env.TELEGRAM_BOT_TOKEN}/${message.audio.file_path}`;\n audioMetadata = {\n mimeType: message.audio.mime_type || 'audio/mpeg',\n duration: message.audio.duration || 0,\n fileId: message.audio.file_id\n };\n }\n}\n\nif (!audioUrl) {\n throw new Error(`No audio/voice message found in ${platform} webhook payload`);\n}\n\nif (!senderId) {\n throw new Error('Unable to identify sender from webhook payload');\n}\n\n// Normalize sender ID to E.164 format if possible\nlet normalizedSenderId = senderId;\nif (platform === 'whatsapp' && !senderId.startsWith('+')) {\n normalizedSenderId = '+' + senderId;\n}\n\nconst normalizedMessage = {\n messageId: `${platform}-${messageId || Date.now()}`,\n platform,\n senderId: normalizedSenderId,\n audioUrl,\n audioMetadata,\n receivedAt: new Date().toISOString(),\n conversationId: body.context?.conversationId || `conv-${normalizedSenderId}`,\n isFollowUp: !!body.context?.previousMessageId,\n previousMessageId: body.context?.previousMessageId || null,\n webhookSource: $node.name,\n rawPayload: body\n};\n\nreturn { json: { normalizedMessage } };"
},
"typeVersion": 2
},
{
"id": "4b9e55f0-1e71-462b-9bd4-bf322cd1b7c9",
"name": "Download Audio File",
"type": "n8n-nodes-base.httpRequest",
"position": [
1504,
864
],
"parameters": {
"url": "={{ $json.normalizedMessage.audioUrl }}",
"options": {
"timeout": 30000,
"response": {
"response": {
"responseFormat": "file"
}
}
},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "ff409526-8edc-4101-b1e0-29a2bdf787ee",
"name": "Transcribe Audio with OpenAI Whisper",
"type": "n8n-nodes-base.httpRequest",
"position": [
1792,
768
],
"parameters": {
"url": "https://api.openai.com/v1/audio/transcriptions",
"method": "POST",
"options": {
"timeout": 60000
},
"sendBody": true,
"contentType": "multipart-form-data",
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "file",
"value": "={{ $binary.data }}"
},
{
"name": "model",
"value": "whisper-1"
},
{
"name": "language",
"value": "auto"
},
{
"name": "response_format",
"value": "verbose_json"
},
{
"name": "temperature",
"value": "0.2"
}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "7e5ca379-24e8-482b-b3eb-db2b4ff3cb8a",
"name": "Extract and Validate Transcription",
"type": "n8n-nodes-base.code",
"position": [
2080,
812
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const transcriptionResponse = $input.item.json;\nconst normalizedMessage = $('Normalize Message Input').item.json.normalizedMessage;\n\n// Extract transcription text\nconst transcribedText = transcriptionResponse.text || '';\nconst language = transcriptionResponse.language || 'en';\nconst duration = transcriptionResponse.duration || normalizedMessage.audioMetadata.duration || 0;\n\nif (!transcribedText || transcribedText.trim().length < 3) {\n throw new Error('Transcription failed or audio was too short/unclear');\n}\n\n// Calculate confidence proxy from metadata\nconst segments = transcriptionResponse.segments || [];\nconst avgConfidence = segments.length > 0 \n ? segments.reduce((sum, seg) => sum + (seg.no_speech_prob || 0), 0) / segments.length\n : 0;\n\nconst transcriptionData = {\n normalizedMessage,\n transcription: {\n text: transcribedText.trim(),\n language,\n duration,\n wordCount: transcribedText.split(/\\s+/).length,\n detectedLanguage: language,\n confidenceScore: Math.round((1 - avgConfidence) * 100),\n segments: segments.map(s => ({\n text: s.text,\n start: s.start,\n end: s.end,\n confidence: 1 - (s.no_speech_prob || 0)\n }))\n },\n transcribedAt: new Date().toISOString()\n};\n\nreturn { json: transcriptionData };"
},
"typeVersion": 2
},
{
"id": "58f8ad36-238a-4317-a7c6-6d60b98f099d",
"name": "Claude AI Intent Classification & Entity Extraction",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
1728,
368
],
"parameters": {
"text": "=You are a travel intent classification and entity extraction engine with deep knowledge of travel terminology, destination names, date expressions, and natural language patterns.\n\nAnalyze this voice transcription and extract all travel-related information.\n\n**Transcribed Voice Message:**\n\"{{ $json.transcription.text }}\"\n\n**Metadata:**\n- Language: {{ $json.transcription.language }}\n- Duration: {{ $json.transcription.duration }}s\n- Confidence: {{ $json.transcription.confidenceScore }}%\n- User ID: {{ $json.normalizedMessage.senderId }}\n- Platform: {{ $json.normalizedMessage.platform }}\n- Is Follow-up: {{ $json.normalizedMessage.isFollowUp }}\n\n**Your Task:**\n1. Classify the primary travel intent (flight_search, hotel_search, activity_search, full_package, itinerary_query, price_alert, booking_modification, general_question)\n2. Extract all travel entities: destinations, dates, passenger count, budget, preferences\n3. Identify date flexibility (exact dates vs. flexible)\n4. Detect urgency level from tone and phrasing\n5. Extract any special requirements (dietary, accessibility, loyalty programs)\n6. Determine if this is a simple or complex multi-leg journey\n7. Identify any constraints (budget, time, companions)\n\n**Entity Extraction Guidelines:**\n- Dates: Convert natural language (\"next month\", \"in March\", \"June 15\") to structured format\n- Destinations: Resolve city names to IATA codes when possible, handle multi-city\n- Budget: Extract amount and currency, distinguish per-person vs. total\n- Preferences: Capture travel class, hotel stars, activity types\n- Duration: Convert \"a week\", \"3 days\" to exact night counts\n\n**Response Format (JSON only, no markdown):**\n{\n \"primaryIntent\": \"flight_search\",\n \"intentConfidence\": 95,\n \"secondaryIntents\": [\"hotel_search\"],\n \"travelType\": \"one_way | round_trip | multi_city\",\n \"complexity\": \"simple | moderate | complex\",\n \"urgencyLevel\": \"low | medium | high\",\n \"destinations\": {\n \"from\": {\n \"city\": \"New York\",\n \"country\": \"USA\",\n \"iataCode\": \"NYC\",\n \"confidence\": 100\n },\n \"to\": [\n {\n \"city\": \"Paris\",\n \"country\": \"France\",\n \"iataCode\": \"PAR\",\n \"confidence\": 100\n }\n ],\n \"multiCity\": []\n },\n \"dates\": {\n \"departure\": {\n \"exact\": \"2025-06-15\",\n \"flexible\": true,\n \"flexibilityDays\": 3,\n \"originalPhrase\": \"next month\"\n },\n \"return\": {\n \"exact\": \"2025-06-22\",\n \"flexible\": true,\n \"flexibilityDays\": 2,\n \"originalPhrase\": \"a week later\"\n },\n \"duration\": {\n \"nights\": 7,\n \"originalPhrase\": \"for a week\"\n }\n },\n \"passengers\": {\n \"adults\": 2,\n \"children\": 0,\n \"infants\": 0\n },\n \"budget\": {\n \"amount\": 2000,\n \"currency\": \"USD\",\n \"perPerson\": false,\n \"category\": \"budget | economy | moderate | premium | luxury\",\n \"originalPhrase\": \"under $2000 total\"\n },\n \"preferences\": {\n \"flightClass\": \"economy | premium_economy | business | first\",\n \"hotelStars\": 3,\n \"hotelType\": \"hotel | resort | boutique | hostel | apartment\",\n \"activities\": [\"museums\", \"food tours\", \"beach\"],\n \"mealPreferences\": [],\n \"accessibility\": [],\n \"loyaltyPrograms\": []\n },\n \"specialRequirements\": [\n \"vegetarian meals\",\n \"window seat\"\n ],\n \"extractedEntities\": {\n \"locations\": [\"Paris\"],\n \"dates\": [\"next month\"],\n \"numbers\": [2000],\n \"timeExpressions\": [\"a week\"]\n },\n \"missingInformation\": [\n \"departure city not specified, will use user profile default\"\n ],\n \"needsClarification\": false,\n \"clarificationQuestions\": [],\n \"conversationalTone\": \"casual | formal | urgent | exploratory\",\n \"sentiment\": \"excited | neutral | frustrated | uncertain\"\n}",
"options": {
"systemMessage": "You are a travel NLP expert. Extract all relevant travel information from natural language. Respond with valid JSON only \u2014 no markdown, no preamble."
},
"promptType": "define"
},
"typeVersion": 1.6
},
{
"id": "875e3164-9dda-492a-8aac-98365c721915",
"name": "Claude Sonnet 4 Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
3048,
1088
],
"parameters": {
"model": "claude-sonnet-4-20250514",
"options": {
"temperature": 0.7
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "416ac6ff-85df-45d8-86e9-edb4aa1eea8a",
"name": "Fetch User Profile & Preferences",
"type": "n8n-nodes-base.googleSheets",
"position": [
1792,
960
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "user_profiles"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.5,
"continueOnFail": true
},
{
"id": "6f389856-0fe5-477c-b5a6-b71ec92c9800",
"name": "Fetch Recent Conversation Context",
"type": "n8n-nodes-base.googleSheets",
"position": [
1792,
1152
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "conversation_history"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.5,
"continueOnFail": true
},
{
"id": "5b519e3c-d56c-4ab0-a9d2-1fa9b4200114",
"name": "Merge Intent, Profile & Context",
"type": "n8n-nodes-base.code",
"position": [
2304,
812
],
"parameters": {
"jsCode": "// Get intent classification\nconst intentRaw = $('Claude AI Intent Classification & Entity Extraction').item.json;\nlet intentText = intentRaw.response || intentRaw.output || intentRaw.text || '';\nif (intentRaw.content && Array.isArray(intentRaw.content)) {\n intentText = intentRaw.content[0]?.text || '';\n}\nconst cleanIntent = intentText.replace(/```json\\s*/g, '').replace(/```\\s*/g, '').trim();\nconst travelIntent = JSON.parse(cleanIntent);\n\n// Get transcription data\nconst transcriptionData = $('Extract and Validate Transcription').item.json;\n\n// Get user profile\nconst userProfiles = $('Fetch User Profile & Preferences').all().map(i => i.json);\nconst senderId = transcriptionData.normalizedMessage.senderId;\nconst userProfile = userProfiles.find(p => p.phoneNumber === senderId || p.telegramId === senderId) || {\n phoneNumber: senderId,\n name: 'Unknown User',\n defaultDepartureCity: 'Not Set',\n preferredCurrency: 'USD',\n loyaltyPrograms: [],\n dietaryRestrictions: [],\n travelClass: 'economy',\n budgetLevel: 'moderate'\n};\n\n// Get conversation history\nconst conversationHistory = $('Fetch Recent Conversation Context').all().map(i => i.json)\n .filter(c => c.userId === senderId)\n .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))\n .slice(0, 5);\n\n// Fill missing information from user profile\nif (!travelIntent.destinations.from?.city && userProfile.defaultDepartureCity !== 'Not Set') {\n travelIntent.destinations.from = {\n city: userProfile.defaultDepartureCity,\n country: userProfile.defaultDepartureCountry || 'USA',\n iataCode: userProfile.defaultDepartureIATA || 'NYC',\n confidence: 80,\n source: 'user_profile_default'\n };\n travelIntent.missingInformation = travelIntent.missingInformation.filter(\n m => !m.includes('departure city')\n );\n}\n\n// Apply budget preferences if not specified\nif (!travelIntent.budget?.amount) {\n const budgetDefaults = {\n 'budget': 1000,\n 'economy': 2000,\n 'moderate': 4000,\n 'premium': 8000,\n 'luxury': 15000\n };\n travelIntent.budget = {\n amount: budgetDefaults[userProfile.budgetLevel] || 3000,\n currency: userProfile.preferredCurrency || 'USD',\n perPerson: true,\n category: userProfile.budgetLevel,\n source: 'user_profile_default'\n };\n}\n\n// Apply travel class preference if not specified\nif (!travelIntent.preferences?.flightClass) {\n travelIntent.preferences = travelIntent.preferences || {};\n travelIntent.preferences.flightClass = userProfile.travelClass || 'economy';\n}\n\n// Merge dietary and accessibility requirements\nif (userProfile.dietaryRestrictions?.length > 0) {\n travelIntent.specialRequirements = [\n ...travelIntent.specialRequirements,\n ...userProfile.dietaryRestrictions.map(d => `dietary: ${d}`)\n ];\n}\n\nif (userProfile.loyaltyPrograms?.length > 0) {\n travelIntent.preferences.loyaltyPrograms = userProfile.loyaltyPrograms;\n}\n\nconst enrichedRequest = {\n transcriptionData,\n travelIntent,\n userProfile: {\n id: senderId,\n name: userProfile.name,\n defaultDepartureCity: userProfile.defaultDepartureCity,\n preferredCurrency: userProfile.preferredCurrency,\n budgetLevel: userProfile.budgetLevel,\n travelClass: userProfile.travelClass,\n loyaltyPrograms: userProfile.loyaltyPrograms || [],\n pastTripCount: userProfile.pastTripCount || 0\n },\n conversationContext: {\n recentMessages: conversationHistory.length,\n lastInteraction: conversationHistory[0]?.timestamp || null,\n priorSearches: conversationHistory.map(c => c.searchSummary).filter(Boolean)\n },\n enrichedAt: new Date().toISOString(),\n readyForSearch: !travelIntent.needsClarification\n};\n\nreturn [{ json: enrichedRequest }];"
},
"typeVersion": 2
},
{
"id": "df9f02d8-f657-4a52-8d76-09a77559e6bc",
"name": "Search Flights (Skyscanner/Kiwi.com)",
"type": "n8n-nodes-base.httpRequest",
"position": [
2528,
620
],
"parameters": {
"url": "https://api.skyscanner.net/v3/flights/live/search/create",
"method": "POST",
"options": {
"timeout": 30000
},
"sendBody": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "f68704a6-e900-44be-85ba-e4c14f4d89a3",
"name": "Search Hotels (Booking.com API)",
"type": "n8n-nodes-base.httpRequest",
"position": [
2528,
812
],
"parameters": {
"url": "https://api.booking.com/v1/hotels/search",
"method": "POST",
"options": {
"timeout": 30000
},
"sendBody": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "14e960c5-3711-42ec-980c-a2f34048de05",
"name": "Search Activities & Tours",
"type": "n8n-nodes-base.httpRequest",
"position": [
2528,
1004
],
"parameters": {
"url": "https://api.viator.com/v1/search/products",
"method": "POST",
"options": {
"timeout": 25000
},
"sendBody": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "c7511e46-8edb-4cc3-9100-71145f74e24c",
"name": "Aggregate and Rank Results",
"type": "n8n-nodes-base.code",
"position": [
2752,
864
],
"parameters": {
"jsCode": "const enrichedRequest = $('Merge Intent, Profile & Context').item.json;\nconst flightResults = $('Search Flights (Skyscanner/Kiwi.com)').item.json;\nconst hotelResults = $('Search Hotels (Booking.com API)').item.json;\nconst activityResults = $('Search Activities & Tours').item.json;\n\n// Extract and normalize flight options\nconst flights = (flightResults.itineraries || flightResults.data?.itineraries || []).map(flight => ({\n type: 'flight',\n airline: flight.legs?.[0]?.carriers?.marketing?.[0]?.name || 'Unknown',\n flightNumber: flight.legs?.[0]?.segments?.[0]?.flightNumber || 'N/A',\n departure: {\n time: flight.legs?.[0]?.departure,\n airport: flight.legs?.[0]?.origin?.displayCode\n },\n arrival: {\n time: flight.legs?.[0]?.arrival,\n airport: flight.legs?.[0]?.destination?.displayCode\n },\n duration: flight.legs?.[0]?.durationInMinutes || 0,\n stops: flight.legs?.[0]?.stopCount || 0,\n price: {\n amount: flight.pricingOptions?.[0]?.price?.amount || 9999,\n currency: flight.pricingOptions?.[0]?.price?.unit || 'USD'\n },\n bookingLink: flight.pricingOptions?.[0]?.items?.[0]?.deepLink || '#',\n score: 0\n})).slice(0, 5);\n\n// Extract and normalize hotel options\nconst hotels = (hotelResults.results || hotelResults.data?.hotels || []).map(hotel => ({\n type: 'hotel',\n name: hotel.name || 'Unknown Hotel',\n stars: hotel.stars || hotel.class || 3,\n rating: hotel.reviewScore || hotel.rating || 0,\n reviewCount: hotel.reviewCount || 0,\n price: {\n amount: hotel.minTotalPrice || hotel.price?.total || 999,\n currency: hotel.currencyCode || 'USD',\n perNight: true\n },\n location: hotel.address || hotel.location?.address || 'Unknown',\n amenities: hotel.facilities || [],\n images: hotel.photos?.slice(0, 3) || [],\n bookingLink: hotel.url || '#',\n score: 0\n})).slice(0, 5);\n\n// Extract and normalize activity options\nconst activities = (activityResults.products || activityResults.data?.activities || []).map(activity => ({\n type: 'activity',\n title: activity.title || activity.name || 'Unknown Activity',\n rating: activity.rating || 0,\n reviewCount: activity.reviewCount || 0,\n duration: activity.duration || 'Variable',\n category: activity.category || 'General',\n price: {\n amount: activity.price?.amount || activity.fromPrice || 50,\n currency: activity.price?.currency || 'USD'\n },\n description: activity.description?.substring(0, 150) || '',\n bookingLink: activity.productUrl || '#',\n score: 0\n})).slice(0, 5);\n\n// Smart ranking algorithm\nfunction calculateScore(item, budget, preferences) {\n let score = 0;\n \n if (item.type === 'flight') {\n // Price factor (40%)\n const priceFactor = 1 - (item.price.amount / budget.amount);\n score += priceFactor * 40;\n \n // Duration factor (30%) - shorter is better\n const durationFactor = item.duration > 0 ? Math.max(0, 1 - (item.duration / 1000)) : 0.5;\n score += durationFactor * 30;\n \n // Stops penalty (30%)\n score += (item.stops === 0 ? 30 : item.stops === 1 ? 15 : 5);\n }\n \n if (item.type === 'hotel') {\n // Price factor (30%)\n const nightlyBudget = budget.amount / 7; // Assume week trip\n const priceFactor = 1 - Math.min(1, item.price.amount / nightlyBudget);\n score += priceFactor * 30;\n \n // Rating factor (40%)\n score += (item.rating / 10) * 40;\n \n // Stars match (30%)\n const starMatch = Math.abs(item.stars - (preferences.hotelStars || 3)) === 0 ? 30 : 15;\n score += starMatch;\n }\n \n if (item.type === 'activity') {\n // Rating factor (50%)\n score += (item.rating / 5) * 50;\n \n // Review count factor (20%)\n const reviewFactor = Math.min(1, item.reviewCount / 100);\n score += reviewFactor * 20;\n \n // Price factor (30%)\n const activityBudget = budget.amount * 0.2; // 20% of budget for activities\n const priceFactor = 1 - Math.min(1, item.price.amount / activityBudget);\n score += priceFactor * 30;\n }\n \n return Math.round(score);\n}\n\n// Calculate scores\nflights.forEach(f => f.score = calculateScore(f, enrichedRequest.travelIntent.budget, enrichedRequest.travelIntent.preferences));\nhotels.forEach(h => h.score = calculateScore(h, enrichedRequest.travelIntent.budget, enrichedRequest.travelIntent.preferences));\nactivities.forEach(a => a.score = calculateScore(a, enrichedRequest.travelIntent.budget, enrichedRequest.travelIntent.preferences));\n\n// Sort by score\nflights.sort((a, b) => b.score - a.score);\nhotels.sort((a, b) => b.score - a.score);\nactivities.sort((a, b) => b.score - a.score);\n\n// Calculate total package estimate\nconst topFlight = flights[0];\nconst topHotel = hotels[0];\nconst estimatedTotal = (topFlight?.price.amount || 0) + (topHotel?.price.amount || 0) * 7;\nconst withinBudget = estimatedTotal <= enrichedRequest.travelIntent.budget.amount;\n\nreturn [{\n json: {\n enrichedRequest,\n searchResults: {\n flights,\n hotels,\n activities,\n totalResults: flights.length + hotels.length + activities.length\n },\n topRecommendations: {\n flight: flights[0] || null,\n hotel: hotels[0] || null,\n activity: activities[0] || null,\n estimatedTotal,\n withinBudget,\n budgetRemaining: enrichedRequest.travelIntent.budget.amount - estimatedTotal\n },\n searchedAt: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "93e82586-de1a-427a-ba33-cc86f8b07f65",
"name": "Claude AI Response Generation",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2976,
864
],
"parameters": {
"text": "=You are a friendly, conversational travel assistant speaking directly to a user via voice message. Generate a natural, enthusiastic response that sounds like a helpful friend sharing travel recommendations.\n\n**User's Original Request:**\n\"{{ $json.enrichedRequest.transcriptionData.transcription.text }}\"\n\n**User Profile:**\n- Name: {{ $json.enrichedRequest.userProfile.name }}\n- Budget Level: {{ $json.enrichedRequest.userProfile.budgetLevel }}\n- Past Trips: {{ $json.enrichedRequest.userProfile.pastTripCount }}\n\n**Travel Intent Understood:**\n- Going to: {{ $json.enrichedRequest.travelIntent.destinations.to[0].city }}\n- From: {{ $json.enrichedRequest.travelIntent.destinations.from.city }}\n- Dates: {{ $json.enrichedRequest.travelIntent.dates.departure.exact }} to {{ $json.enrichedRequest.travelIntent.dates.return.exact }}\n- Budget: {{ $json.enrichedRequest.travelIntent.budget.currency }} {{ $json.enrichedRequest.travelIntent.budget.amount }}\n- Passengers: {{ $json.enrichedRequest.travelIntent.passengers.adults }} adult(s)\n\n**Top Recommendations Found:**\n\nFlight: {{ $json.topRecommendations.flight?.airline || 'No flights found' }} - {{ $json.topRecommendations.flight?.price.currency }} {{ $json.topRecommendations.flight?.price.amount }} ({{ $json.topRecommendations.flight?.stops }} stops, Score: {{ $json.topRecommendations.flight?.score }}/100)\n\nHotel: {{ $json.topRecommendations.hotel?.name || 'No hotels found' }} - {{ $json.topRecommendations.hotel?.stars }} stars, {{ $json.topRecommendations.hotel?.price.currency }} {{ $json.topRecommendations.hotel?.price.amount }}/night (Rating: {{ $json.topRecommendations.hotel?.rating }}/10, Score: {{ $json.topRecommendations.hotel?.score }}/100)\n\nTop Activity: {{ $json.topRecommendations.activity?.title || 'No activities found' }} - {{ $json.topRecommendations.activity?.price.currency }} {{ $json.topRecommendations.activity?.price.amount }} ({{ $json.topRecommendations.activity?.rating }}/5 stars)\n\n**Budget Summary:**\n- Estimated Total: {{ $json.topRecommendations.estimatedTotal }}\n- Budget: {{ $json.enrichedRequest.travelIntent.budget.amount }}\n- Within Budget: {{ $json.topRecommendations.withinBudget ? 'Yes! \ud83c\udf89' : 'Slightly over, but great value' }}\n- Remaining: {{ $json.topRecommendations.budgetRemaining }}\n\n**Additional Options:**\n- Found {{ $json.searchResults.flights.length }} flight options\n- Found {{ $json.searchResults.hotels.length }} hotel options\n- Found {{ $json.searchResults.activities.length }} activities\n\n**Your Task:**\nGenerate a warm, conversational voice-optimized response (300-400 words max) that:\n1. Acknowledges their request with enthusiasm\n2. Confirms the understood intent (destination, dates, budget)\n3. Presents the TOP recommendation as the clear winner with compelling reasons\n4. Mentions 1-2 alternative options briefly\n5. Highlights if it fits their budget perfectly or offers great value\n6. Adds a personal touch based on their travel history or preferences\n7. Ends with clear next steps (\"Say 'book it' to proceed\" or \"Want to see more options?\")\n8. Keep the tone casual, friendly, and optimistic\n\n**Voice-Optimized Writing Style:**\n- Use short sentences and natural speech patterns\n- Include conversational fillers (\"So\", \"Actually\", \"Plus\")\n- Avoid complex terminology or jargon\n- Use numbers sparingly, round when possible\n- Add excitement with emojis (used in text response, not spoken)\n\n**Response Format (JSON only):**\n{\n \"textResponse\": \"Full conversational response text for messaging apps\",\n \"voiceScript\": \"Slightly shorter version optimized for text-to-speech, no emojis\",\n \"shortSummary\": \"One-sentence summary for notification\",\n \"actionButtons\": [\n { \"label\": \"Book Flight\", \"action\": \"book_flight\", \"url\": \"booking_link\" },\n { \"label\": \"See Hotels\", \"action\": \"view_hotels\" },\n { \"label\": \"Modify Search\", \"action\": \"modify_search\" }\n ],\n \"calendarEvent\": {\n \"title\": \"Trip to Paris\",\n \"description\": \"Brief trip summary\",\n \"startDate\": \"2025-06-15\",\n \"endDate\": \"2025-06-22\",\n \"location\": \"Paris, France\",\n \"reminders\": [\n { \"method\": \"notification\", \"minutes\": 2880 }\n ]\n }\n}",
"options": {
"systemMessage": "You are an enthusiastic travel assistant. Write conversationally as if speaking to a friend via voice message. Keep it warm, concise, and action-oriented. Respond with valid JSON only."
},
"promptType": "define"
},
"typeVersion": 1.6
},
{
"id": "e91aece6-60db-44ea-ba74-c867ee7cf959",
"name": "Parse Response & Create Calendar Event",
"type": "n8n-nodes-base.code",
"position": [
3328,
864
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const responseRaw = $input.item.json;\nlet responseText = responseRaw.response || responseRaw.output || responseRaw.text || '';\nif (responseRaw.content && Array.isArray(responseRaw.content)) {\n responseText = responseRaw.content[0]?.text || '';\n}\n\nconst cleanResponse = responseText.replace(/```json\\s*/g, '').replace(/```\\s*/g, '').trim();\nconst aiResponse = JSON.parse(cleanResponse);\n\nconst aggregateData = $('Aggregate and Rank Results').item.json;\n\n// Build calendar event object for Google Calendar API\nconst calendarEvent = {\n summary: aiResponse.calendarEvent?.title || `Trip to ${aggregateData.enrichedRequest.travelIntent.destinations.to[0].city}`,\n description: aiResponse.calendarEvent?.description || aiResponse.shortSummary,\n location: aiResponse.calendarEvent?.location || aggregateData.enrichedRequest.travelIntent.destinations.to[0].city,\n start: {\n date: aggregateData.enrichedRequest.travelIntent.dates.departure.exact,\n timeZone: 'America/New_York'\n },\n end: {\n date: aggregateData.enrichedRequest.travelIntent.dates.return.exact,\n timeZone: 'America/New_York'\n },\n reminders: {\n useDefault: false,\n overrides: [\n { method: 'popup', minutes: 2880 }, // 2 days before\n { method: 'email', minutes: 10080 } // 1 week before\n ]\n },\n colorId: '9', // Blue color for travel events\n extendedProperties: {\n private: {\n tripType: aggregateData.enrichedRequest.travelIntent.primaryIntent,\n budget: aggregateData.enrichedRequest.travelIntent.budget.amount.toString(),\n flightPrice: aggregateData.topRecommendations.flight?.price.amount.toString() || '0',\n hotelPrice: aggregateData.topRecommendations.hotel?.price.amount.toString() || '0'\n }\n }\n};\n\nreturn {\n json: {\n aggregateData,\n aiResponse,\n calendarEvent,\n delivery: {\n platform: aggregateData.enrichedRequest.transcriptionData.normalizedMessage.platform,\n recipientId: aggregateData.enrichedRequest.transcriptionData.normalizedMessage.senderId,\n messageId: aggregateData.enrichedRequest.transcriptionData.normalizedMessage.messageId\n },\n generatedAt: new Date().toISOString()\n }\n};"
},
"typeVersion": 2
},
{
"id": "0a167727-9986-4cc5-b89f-0134e850dc40",
"name": "Add Event to Google Calendar",
"type": "n8n-nodes-base.googleCalendar",
"position": [
3600,
624
],
"parameters": {
"end": "={{ $json.calendarEvent.end.date }}",
"start": "={{ $json.calendarEvent.start.date }}",
"calendar": {
"__rl": true,
"mode": "id",
"value": "=b343rsafaehyu65"
},
"additionalFields": {}
},
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1.2,
"continueOnFail": true
},
{
"id": "acff6677-6df4-4396-957a-c41cc00600e0",
"name": "Send WhatsApp Response",
"type": "n8n-nodes-base.httpRequest",
"position": [
3776,
620
],
"parameters": {
"url": "https://graph.facebook.com/v18.0/YOUR_PHONE_NUMBER_ID/messages",
"method": "POST",
"options": {},
"sendBody": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "49bcfe34-f337-460c-96d5-7ef035fe682f",
"name": "Send Telegram Response",
"type": "n8n-nodes-base.telegram",
"position": [
3776,
812
],
"parameters": {
"text": "={{ $json.aiResponse.textResponse }}",
"chatId": "={{ $json.delivery.recipientId }}",
"additionalFields": {
"parse_mode": "Markdown",
"disable_web_page_preview": false
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2,
"continueOnFail": true
},
{
"id": "cea47ec8-b86f-4e27-9bfe-20548bee3c7b",
"name": "Send Interactive Action Buttons",
"type": "n8n-nodes-base.httpRequest",
"position": [
3776,
1004
],
"parameters": {
"url": "=https://graph.facebook.com/v18.0/YOUR_PHONE_NUMBER_ID/messages",
"method": "POST",
"options": {},
"sendBody": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "7ce5edfd-16ca-4dc1-a6b5-864fb2f845fb",
"name": "Log Conversation to History",
"type": "n8n-nodes-base.googleSheets",
"position": [
4000,
864
],
"parameters": {
"columns": {
"value": {
"budget": "={{ $json.aggregateData.enrichedRequest.travelIntent.budget.amount }}",
"intent": "={{ $json.aggregateData.enrichedRequest.travelIntent.primaryIntent }}",
"userId": "={{ $json.delivery.recipientId }}",
"platform": "={{ $json.delivery.platform }}",
"timestamp": "={{ $json.generatedAt }}",
"destination": "={{ $json.aggregateData.enrichedRequest.travelIntent.destinations.to[0].city }}",
"userMessage": "={{ $json.aggregateData.enrichedRequest.transcriptionData.transcription.text }}",
"calendarAdded": "=true",
"searchSummary": "={{ $json.aiResponse.shortSummary }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "conversation_history"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.5
},
{
"id": "f91db0a8-fb0e-469e-bd77-865e4b99ce0e",
"name": "Webhook Response - Success",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
4224,
864
],
"parameters": {
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"respondWith": "json",
"responseBody": "={\n \"status\": \"success\",\n \"messageId\": \"{{ $json.delivery.messageId }}\",\n \"recipientId\": \"{{ $json.delivery.recipientId }}\",\n \"platform\": \"{{ $json.delivery.platform }}\",\n \"calendarEventCreated\": true,\n \"destination\": \"{{ $json.aggregateData.enrichedRequest.travelIntent.destinations.to[0].city }}\",\n \"timestamp\": \"{{ $json.generatedAt }}\"\n}"
},
"typeVersion": 1
},
{
"id": "e2de5dfb-5093-46cc-ab93-e82bf763ac60",
"name": "Claude Sonnet 4 Model1",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
1800,
592
],
"parameters": {
"model": "=claude-sonnet-4-20250514",
"options": {
"temperature": 0.7
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "6c72cae6-9f17-4122-bf86-91104f5ea6e7",
"connections": {
"Download Audio File": {
"main": [
[
{
"node": "Transcribe Audio with OpenAI Whisper",
"type": "main",
"index": 0
},
{
"node": "Claude AI Intent Classification & Entity Extraction",
"type": "main",
"index": 0
},
{
"node": "Fetch User Profile & Preferences",
"type": "main",
"index": 0
},
{
"node": "Fetch Recent Conversation Context",
"type": "main",
"index": 0
}
]
]
},
"Claude Sonnet 4 Model": {
"ai_languageModel": [
[
{
"node": "Claude AI Response Generation",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Claude Sonnet 4 Model1": {
"ai_languageModel": [
[
{
"node": "Claude AI Intent Classification & Entity Extraction",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Send Telegram Response": {
"main": [
[
{
"node": "Log Conversation to History",
"type": "main",
"index": 0
}
]
]
},
"Send WhatsApp Response": {
"main": [
[
{
"node": "Log Conversation to History",
"type": "main",
"index": 0
}
]
]
},
"Normalize Message Input": {
"main": [
[
{
"node": "Download Audio File",
"type": "main",
"index": 0
}
]
]
},
"Search Activities & Tours": {
"main": [
[
{
"node": "Aggregate and Rank Results",
"type": "main",
"index": 0
}
]
]
},
"Aggregate and Rank Results": {
"main": [
[
{
"node": "Claude AI Response Generation",
"type": "main",
"index": 0
}
]
]
},
"Log Conversation to History": {
"main": [
[
{
"node": "Webhook Response - Success",
"type": "main",
"index": 0
}
]
]
},
"Telegram Voice Note Webhook": {
"main": [
[
{
"node": "Normalize Message Input",
"type": "main",
"index": 0
}
]
]
},
"Add Event to Google Calendar": {
"main": [
[
{
"node": "Send WhatsApp Response",
"type": "main",
"index": 0
}
]
]
},
"Claude AI Response Generation": {
"main": [
[
{
"node": "Parse Response & Create Calendar Event",
"type": "main",
"index": 0
}
]
]
},
"WhatsApp Voice Message Webhook": {
"main": [
[
{
"node": "Normalize Message Input",
"type": "main",
"index": 0
}
]
]
},
"Merge Intent, Profile & Context": {
"main": [
[
{
"node": "Search Flights (Skyscanner/Kiwi.com)",
"type": "main",
"index": 0
},
{
"node": "Search Hotels (Booking.com API)",
"type": "main",
"index": 0
},
{
"node": "Search Activities & Tours",
"type": "main",
"index": 0
}
]
]
},
"Search Hotels (Booking.com API)": {
"main": [
[
{
"node": "Aggregate and Rank Results",
"type": "main",
"index": 0
}
]
]
},
"Send Interactive Action Buttons": {
"main": [
[
{
"node": "Log Conversation to History",
"type": "main",
"index": 0
}
]
]
},
"Fetch User Profile & Preferences": {
"main": [
[
{
"node": "Extract and Validate Transcription",
"type": "main",
"index": 0
}
]
]
},
"Fetch Recent Conversation Context": {
"main": [
[
{
"node": "Extract and Validate Transcription",
"type": "main",
"index": 0
}
]
]
},
"Extract and Validate Transcription": {
"main": [
[
{
"node": "Merge Intent, Profile & Context",
"type": "main",
"index": 0
}
]
]
},
"Search Flights (Skyscanner/Kiwi.com)": {
"main": [
[
{
"node": "Aggregate and Rank Results",
"type": "main",
"index": 0
}
]
]
},
"Transcribe Audio with OpenAI Whisper": {
"main": [
[
{
"node": "Extract and Validate Transcription",
"type": "main",
"index": 0
}
]
]
},
"Parse Response & Create Calendar Event": {
"main": [
[
{
"node": "Add Event to Google Calendar",
"type": "main",
"index": 0
},
{
"node": "Send Telegram Response",
"type": "main",
"index": 0
},
{
"node": "Send Interactive Action Buttons",
"type": "main",
"index": 0
}
]
]
},
"Claude AI Intent Classification & Entity Extraction": {
"main": [
[
{
"node": "Extract and Validate Transcription",
"type": "main",
"index": 0
}
]
]
}
}
}
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.
anthropicApigoogleCalendarOAuth2ApigoogleSheetsOAuth2ApihttpHeaderAuthtelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
A hands-free travel planning assistant that accepts voice messages via WhatsApp and Telegram, understands natural language travel requests, searches across multiple providers, and automatically books to your calendar with smart recommendations. Voice Message Reception -…
Source: https://n8n.io/workflows/14914/ — 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.
This workflow turns your WhatsApp Business number into a 24/7 AI-powered customer assistant — without any third-party chatbot platform. It receives incoming WhatsApp messages via Evolution API, unders
This workflow creates a multi-talented AI assistant named Simran that interacts with users via Telegram. It can handle text and voice messages, understand the user's intent, and perform various tasks.
⏺ 🚀 How it works
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
Fully automates your service order pipeline from incoming booking to supplier confirmation — with built-in SLA enforcement and automatic escalation if a supplier goes silent. 📥 Receives orders via web