This workflow follows the Agent → Execute Workflow Trigger 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": "6_Multi-Agent_4vaEvzlaMrgovhNz",
"nodes": [
{
"parameters": {
"jsCode": "// \u0410\u0414\u0410\u041f\u0422\u0415\u0420: \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u043d\u043e\u0432\u043e\u0439 \u0446\u0435\u043f\u043e\u0447\u043a\u0438\n // \u0412\u0445\u043e\u0434: turn_analysis.merged_message, session_id, user_id\n // \u0418\u041b\u0418 \u0441\u0442\u0430\u0440\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 body.message (\u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0439 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0441\u0442\u0438)\n\n const raw = $json;\n\n // \u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0434\u0430\u043d\u043d\u044b\u0445\n let phone, remoteJid, text, originalText, senderName, messageId, msgTimestamp;\n\n if (raw.turn_analysis) {\n // \u041d\u041e\u0412\u042b\u0419 \u0424\u041e\u0420\u041c\u0410\u0422 \u0438\u0437 TurnDetector\n phone = raw.user_id || '';\n remoteJid = raw.session_id || '';\n text = raw.turn_analysis.merged_message || '';\n originalText = raw.merged_text || text;\n senderName = raw.sender_name || '';\n messageId = raw.buffered_messages?.[0]?.message_id || `turn_${Date.now()}`;\n msgTimestamp = Date.now();\n } else {\n // \u0421\u0422\u0410\u0420\u042b\u0419 \u0424\u041e\u0420\u041c\u0410\u0422 (body.message) - \u043e\u0431\u0440\u0430\u0442\u043d\u0430\u044f \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0441\u0442\u044c\n const body = raw.body || {};\n const metadata = body.metadata || {};\n remoteJid = metadata.remoteJid || '';\n phone = remoteJid.replace('@s.whatsapp.net', '');\n text = body.message || '';\n originalText = text;\n senderName = metadata.sender || '';\n messageId = metadata.messageId || '';\n msgTimestamp = (metadata.timestamp || 0) * 1000 || Date.now();\n }\n\n // === VALIDATION ===\n if (!text.trim() || text.trim().length < 2) {\n return [];\n }\n\n if (text.length > 1000) {\n text = text.substring(0, 1000) + '...';\n }\n\n // \u0417\u0430\u0449\u0438\u0442\u0430 \u043e\u0442 replay \u0430\u0442\u0430\u043a (\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0440\u0448\u0435 5 \u043c\u0438\u043d\u0443\u0442) - \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0441\u0442\u0430\u0440\u043e\u0433\u043e \u0444\u043e\u0440\u043c\u0430\u0442\u0430\n const now = Date.now();\n if (msgTimestamp && now - msgTimestamp > 300000) {\n // \u041f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0434\u043b\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u0444\u043e\u0440\u043c\u0430\u0442\u0430 (\u0431\u0443\u0444\u0435\u0440 \u0443\u0436\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u043b)\n if (!$json.turn_analysis) {\n return [];\n }\n }\n\n return [{\n json: {\n phone,\n remoteJid,\n text: text.trim(),\n originalText: originalText.trim(),\n messageType: 'text',\n senderName,\n messageId,\n timestamp: msgTimestamp,\n client_slug: raw.client_slug || 'truffles'\n }\n }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4368,
688
],
"id": "22a5bb61-5a2f-4c04-a222-08a8917d6da5",
"name": "Parse Input"
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH client AS (\n SELECT id FROM clients WHERE name = '{{ $('Parse Input').first().json.client_slug }}'\n),\nupserted_user AS (\n INSERT INTO users (client_id, phone, remote_jid, name, last_active_at)\n SELECT\n (SELECT id FROM client),\n '{{ $('Parse Input').first().json.phone }}',\n '{{ $('Parse Input').first().json.remoteJid }}',\n '{{ $('Parse Input').first().json.senderName }}',\n NOW()\n ON CONFLICT (client_id, phone) DO UPDATE SET\n last_active_at = NOW(),\n name = COALESCE(NULLIF('{{ $('Parse Input').first().json.senderName }}', ''), users.name)\n RETURNING id\n),\nexisting_conv AS (\n SELECT id FROM conversations\n WHERE user_id = (SELECT id FROM upserted_user)\n AND status = 'active'\n ORDER BY last_message_at DESC\n LIMIT 1\n),\nnew_conv AS (\n INSERT INTO conversations (client_id, user_id, channel, status, last_message_at)\n SELECT\n (SELECT id FROM client),\n (SELECT id FROM upserted_user),\n 'whatsapp',\n 'active',\n NOW()\n WHERE NOT EXISTS (SELECT 1 FROM existing_conv)\n RETURNING id\n)\nSELECT\n (SELECT id FROM upserted_user) AS user_id,\n COALESCE(\n (SELECT id FROM existing_conv),\n (SELECT id FROM new_conv)\n ) AS conversation_id,\n (SELECT id FROM client) AS client_id;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-2672,
656
],
"id": "3f451a65-5b58-4fe1-a0e9-4d1df86a7c93",
"name": "Upsert User",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT role, content\n FROM messages\n WHERE conversation_id = '{{ $('Upsert User').item.json.conversation_id }}'\n AND content IS NOT NULL\n AND content != ''\n AND LENGTH(content) < 1000\n ORDER BY created_at DESC\n LIMIT 10;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-2224,
656
],
"id": "d51cd10e-5881-412e-9a94-5d27fb1f0eda",
"name": "Load History",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO messages (conversation_id, client_id, role, content)\n VALUES\n ('{{ $json.conversation_id }}', '{{ $json.client_id }}', 'user', '{{ $json.safe_message }}'),\n ('{{ $json.conversation_id }}', '{{ $json.client_id }}', 'assistant', '{{ $json.safe_response }}');",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
720,
688
],
"id": "58d4e715-9929-43ea-83eb-b2bc07fa07f1",
"name": "Save Messages",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook?url=https://n8n.truffles.kz/webhook/flow",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-4592,
-320
],
"id": "644656fb-e2e8-4b15-8bdd-d19e1dde7074",
"name": "HTTP Request1",
"disabled": true
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO messages (conversation_id, client_id, role, content, metadata)\n SELECT\n '{{ $json.conversation_id }}',\n '{{ $json.client_id }}',\n 'user',\n '{{ $('Parse Input').first().json.text.replace(/'/g, \"''\") }}',\n '{\"message_id\": \"{{ $('Parse Input').first().json.messageId }}\"}'\n WHERE NOT EXISTS (\n SELECT 1 FROM messages\n WHERE metadata->>'message_id' = '{{ $('Parse Input').first().json.messageId }}'\n );",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-2448,
656
],
"id": "e8b5bc48-6bc1-4146-b4f7-adbb0219efcd",
"name": "Save User Message",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini"
},
"builtInTools": {},
"options": {
"temperature": 0
}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.3,
"position": [
-3248,
688
],
"id": "bb864994-88b2-4b95-a0fe-64371bfcd49d",
"name": "OpenAI Chat Model",
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"intent\": {\n \"type\": \"string\",\n \"enum\": [\"greeting\", \"pricing\", \"what_is_this\", \"how_to_start\", \"availability\", \"order\", \"payment\", \"delivery\", \"details\", \"integration\", \"objection_expensive\", \"objection_later\", \"objection_doubt\", \"complaint\", \"frustration\", \"human_request\", \"thanks_positive\", \"thanks_bye\", \"out_of_domain\", \"attack\"],\n \"description\": \"\u0422\u0438\u043f \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430\"\n },\n \"confidence\": {\n \"type\": \"number\",\n \"description\": \"\u0423\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u044c 0.0-1.0\"\n }\n },\n \"required\": [\"intent\", \"confidence\"]\n}"
},
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"typeVersion": 1.3,
"position": [
-3120,
688
],
"id": "fd40fa94-ed28-4bb3-b92e-2f95b6f28d85",
"name": "Structured Output Parser"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "5b6e6bf2-e38c-4d2d-9772-aed284b3fec2",
"leftValue": "={{ !['out_of_domain', 'attack'].includes($json.output.intent) }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-2896,
560
],
"id": "d433b9bc-de00-4acc-8146-0a2d43fce766",
"name": "Is On Topic"
},
{
"parameters": {
"jsCode": "const prev = $('RAG Search').first().json;\n\nreturn [{\n json: {\n ...prev\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1104,
800
],
"id": "cdc8f869-bffa-4d1d-b5f1-ad03b4e0d53a",
"name": "Add Knowledge"
},
{
"parameters": {
"model": {
"__rl": true,
"value": "gpt-5",
"mode": "list",
"cachedResultName": "gpt-5"
},
"builtInTools": {},
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.3,
"position": [
-656,
1024
],
"id": "382e3b3c-c97b-4283-97b9-a79d9b6c083d",
"name": "OpenAI Chat Model1",
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"thinking\": {\n \"type\": \"string\",\n \"description\": \"\u0422\u0432\u043e\u0438 \u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043d\u0438\u044f: \u0447\u0442\u043e \u0441\u043f\u0440\u043e\u0441\u0438\u043b \u043a\u043b\u0438\u0435\u043d\u0442, \u0435\u0441\u0442\u044c \u043b\u0438 \u043e\u0442\u0432\u0435\u0442 \u0432 \u0431\u0430\u0437\u0435, \u0447\u0442\u043e \u0434\u0435\u043b\u0430\u0442\u044c\"\n },\n \"has_answer\": {\n \"type\": \"boolean\",\n \"description\": \"true \u0435\u0441\u043b\u0438 \u043c\u043e\u0436\u0435\u0448\u044c \u0434\u0430\u0442\u044c \u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442, false \u0435\u0441\u043b\u0438 \u043d\u0435 \u0437\u043d\u0430\u0435\u0448\u044c\"\n },\n \"needs_escalation\": {\n \"type\": \"boolean\",\n \"description\": \"true \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u0435\u043d \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 (\u0441\u043b\u043e\u0436\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441, \u0436\u0430\u043b\u043e\u0431\u0430, \u0432\u043e\u0437\u0432\u0440\u0430\u0442)\"\n },\n \"source\": {\n \"type\": \"string\",\n \"description\": \"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u043e\u0442\u043a\u0443\u0434\u0430 \u0432\u0437\u044f\u043b \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e: faq.md, objections.md, cases.md, examples.md, \u0438\u043b\u0438 none \u0435\u0441\u043b\u0438 \u043d\u0435\u0442 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\"\n },\n \"response\": {\n \"type\": \"string\",\n \"description\": \"\u041e\u0442\u0432\u0435\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0443\"\n }\n },\n \"required\": [\n \"thinking\",\n \"has_answer\",\n \"needs_escalation\",\n \"source\",\n \"response\"\n ]\n}"
},
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"typeVersion": 1.3,
"position": [
-528,
1024
],
"id": "e2decc2c-d8a7-4dc8-9718-2dd1d7244575",
"name": "Structured Output Parser1"
},
{
"parameters": {
"jsCode": "const input = $('Parse Input').first().json;\nconst history = $('Load History').all() || [];\nconst user = $('Upsert User').first().json;\n\n// Get summary if exists\nlet summaryContext = '';\ntry {\n const summaryData = $('Load Summary').first()?.json;\n if (summaryData?.summary) {\n summaryContext = '[\u041f\u0420\u0415\u0414\u042b\u0414\u0423\u0429\u0418\u0419 \u041a\u041e\u041d\u0422\u0415\u041a\u0421\u0422] ' + summaryData.summary + '\\n\\n';\n }\n} catch (e) {}\n\n// Check escalation cooldown\nlet isInCooldown = false;\nlet escalatedAt = null;\ntry {\n const escStatus = $('Load Escalation Status').first()?.json;\n isInCooldown = escStatus?.is_in_cooldown || false;\n escalatedAt = escStatus?.escalated_at;\n} catch (e) {}\n\n// \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c intent\nlet currentIntent = 'unknown';\ntry {\n const classifyResult = $('Classify Intent').first()?.json?.output;\n if (classifyResult?.intent) {\n currentIntent = classifyResult.intent;\n }\n} catch (e) {\n const routing = input._routing || {};\n if (routing.isGreetingByText || routing.routeReason === 'greeting_on_topic') {\n currentIntent = 'greeting';\n } else if (routing.isShortAnswer || routing.routeReason === 'answer_in_dialog') {\n currentIntent = 'details';\n }\n}\n\n// History\nconst historyLimit = summaryContext ? 5 : 10;\nconst recentHistory = history.slice(0, historyLimit);\nconst historyText = recentHistory\n .reverse()\n .map(m => { const r = m.json.role; if (r === 'user') return '\u041a\u043b\u0438\u0435\u043d\u0442: ' + m.json.content; if (r === 'assistant') return '\u0411\u043e\u0442: ' + m.json.content; if (r === 'manager') return '\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440: ' + m.json.content; if (r === 'system') return '[' + m.json.content + ']'; return m.json.content; })\n .join('\\n') || '(\u043d\u043e\u0432\u044b\u0439 \u0434\u0438\u0430\u043b\u043e\u0433)';\n\n// Deadlock detection (\u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e, \u043d\u043e \u0434\u043b\u044f \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f)\nconst isFrustration = currentIntent === 'frustration';\nconst isHumanRequest = currentIntent === 'human_request';\nconst isDeadlock = isFrustration || isHumanRequest;\nlet deadlockReason = null;\nif (isFrustration) deadlockReason = 'frustration';\nelse if (isHumanRequest) deadlockReason = 'human_request';\n\nreturn [{\n json: {\n message: input.text,\n originalMessage: input.originalText || input.text,\n history: summaryContext + historyText,\n conversation_id: user.conversation_id,\n client_id: user.client_id,\n remoteJid: input.remoteJid,\n phone: input.phone,\n currentIntent,\n isInCooldown,\n escalatedAt,\n isDeadlock,\n deadlockReason\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1776,
656
],
"id": "11c44d56-f39f-4514-9685-e901f862bcad",
"name": "Build Context"
},
{
"parameters": {
"operation": "executeQuery",
"query": " INSERT INTO messages (conversation_id, client_id, role, content, metadata)\n VALUES\n ('{{ $json.conversation_id }}', '{{ $json.client_id }}', 'user', '{{ $json.safe_message }}', '{\"fallback\": true}'),\n ('{{ $json.conversation_id }}', '{{ $json.client_id }}', 'assistant', '{{ $json.safe_response }}', '{\"fallback\":\n true, \"reason\": \"{{ $json.quality_reason }}\"}');",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-2896,
368
],
"id": "37ab71b5-1ee1-4c88-beae-49e5c6a0e435",
"name": "Save Messages (Fallback)1",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"url": "https://app.chatflow.kz/api/v1/send-text",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "token",
"value": "REDACTED_JWT"
},
{
"name": "instance_id",
"value": "={{ $('Prepare Response').first().json.instance_id }}"
},
{
"name": "jid",
"value": "={{ $('Parse Quality').item.json.remoteJid }}"
},
{
"name": "msg",
"value": "={{ $('Quality Decision').item.json.response }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-2672,
368
],
"id": "c9dbd13f-0523-48e1-ae96-379e85dd7548",
"name": "Send Fallback2"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT m.role, m.content FROM messages m JOIN conversations c ON m.conversation_id = c.id JOIN users u ON c.user_id = u.id WHERE u.phone = '{{ $json.phone }}' AND m.content IS NOT NULL AND m.content != '' ORDER BY m.created_at DESC LIMIT 5;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-3696,
464
],
"id": "load-history-classifier-001",
"name": "Load History for Classifier",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const input = $('Skip Classifier?').first().json;\nconst historyRows = $('Load History for Classifier').all() || [];\n\nconst historyText = historyRows\n .reverse()\n .map(m => { const r = m.json.role; if (r === 'user') return '\u041a\u043b\u0438\u0435\u043d\u0442: ' + m.json.content; if (r === 'assistant') return '\u0411\u043e\u0442: ' + m.json.content; if (r === 'manager') return '\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440: ' + m.json.content; if (r === 'system') return '[' + m.json.content + ']'; return m.json.content; })\n .join('\\n') || '(\u043d\u043e\u0432\u044b\u0439 \u0434\u0438\u0430\u043b\u043e\u0433)';\n\nreturn [{\n json: {\n ...input,\n text: input.text,\n historyContext: historyText\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-3472,
464
],
"id": "format-classifier-input-001",
"name": "Format Classifier Input"
},
{
"parameters": {
"promptType": "define",
"text": "=\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u0434\u0438\u0430\u043b\u043e\u0433\u0430:\n{{ $json.historyContext }}\n\n\u0422\u0435\u043a\u0443\u0449\u0435\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430:\n{{ $json.text }}",
"hasOutputParser": true,
"options": {
"systemMessage": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438 intent \u0422\u0415\u041a\u0423\u0429\u0415\u0413\u041e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430. \u0418\u0441\u0442\u043e\u0440\u0438\u044f \u0434\u0430\u043d\u0430 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430, \u043d\u043e \u043e\u0446\u0435\u043d\u0438\u0432\u0430\u0439 \u0422\u0415\u041a\u0423\u0429\u0415\u0415 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435.\n\nINTENT'\u042b:\n- greeting: \u043f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435\n- pricing: \u0432\u043e\u043f\u0440\u043e\u0441 \u043e \u0446\u0435\u043d\u0435\n- what_is_this: \u0447\u0442\u043e \u044d\u0442\u043e \u0437\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\n- how_to_start: \u043a\u0430\u043a \u043d\u0430\u0447\u0430\u0442\u044c/\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\n- availability: \u043d\u0430\u043b\u0438\u0447\u0438\u0435/\u0437\u0430\u043f\u0438\u0441\u044c\n- order: \u0437\u0430\u043a\u0430\u0437\n- payment: \u043e\u043f\u043b\u0430\u0442\u0430/Kaspi\n- delivery: \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0430\n- details: \u0443\u0442\u043e\u0447\u043d\u0435\u043d\u0438\u0435 \u0434\u0435\u0442\u0430\u043b\u0435\u0439, \u043e\u0442\u0432\u0435\u0442 \u043d\u0430 \u0432\u043e\u043f\u0440\u043e\u0441 \u0431\u043e\u0442\u0430\n- integration: \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\n- objection_expensive: \u0434\u043e\u0440\u043e\u0433\u043e\n- objection_later: \u043f\u043e\u0434\u0443\u043c\u0430\u044e/\u043f\u043e\u0437\u0436\u0435\n- objection_doubt: \u0441\u043e\u043c\u043d\u0435\u043d\u0438\u044f, \u0441\u043a\u0435\u043f\u0442\u0438\u0446\u0438\u0437\u043c (\u0432\u044b \u0442\u043e\u0447\u043d\u043e \u0443\u043c\u0435\u0435\u0442\u0435? \u0430 \u044d\u0442\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442? \u043d\u0435 \u0432\u0435\u0440\u044e)\n- complaint: \u0436\u0430\u043b\u043e\u0431\u0430 \u043d\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0442/\u0441\u0435\u0440\u0432\u0438\u0441 (\u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u0432\u043e\u0437\u0432\u0440\u0430\u0442)\n- frustration: \u0422\u041e\u041b\u042c\u041a\u041e \u043c\u0430\u0442, \u043e\u0441\u043a\u043e\u0440\u0431\u043b\u0435\u043d\u0438\u044f, \u044f\u0432\u043d\u0430\u044f \u0430\u0433\u0440\u0435\u0441\u0441\u0438\u044f (\u0431\u043b\u044f\u0442\u044c, \u0445\u0443\u0439\u043d\u044f, \u0438\u0434\u0438\u043e\u0442\u044b)\n- human_request: \u0445\u043e\u0447\u0435\u0442 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430/\u0447\u0435\u043b\u043e\u0432\u0435\u043a\u0430\n- thanks_positive: \u0431\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u043d\u043e\u0441\u0442\u044c\n- thanks_bye: \u043f\u0440\u043e\u0449\u0430\u043d\u0438\u0435\n- out_of_domain: \u0441\u043e\u0432\u0441\u0435\u043c \u043d\u0435 \u043f\u043e \u0442\u0435\u043c\u0435 (\u043f\u043e\u0433\u043e\u0434\u0430, \u0440\u0435\u0446\u0435\u043f\u0442\u044b)\n- attack: \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u0432\u0437\u043b\u043e\u043c\u0430\n\n\u0412\u0410\u0416\u041d\u041e:\n1. frustration = \u0422\u041e\u041b\u042c\u041a\u041e \u041c\u0410\u0422 \u0418 \u041e\u0421\u041a\u041e\u0420\u0411\u041b\u0415\u041d\u0418\u042f. \u0411\u0435\u0437 \u043c\u0430\u0442\u0430 = \u041d\u0415 frustration\n2. \u0421\u043e\u043c\u043d\u0435\u043d\u0438\u044f \u0431\u0435\u0437 \u043c\u0430\u0442\u0430 (\u0432\u044b \u0442\u043e\u0447\u043d\u043e \u0443\u043c\u0435\u0435\u0442\u0435?) = objection_doubt, \u041d\u0415 frustration\n3. \u0421\u043a\u0435\u043f\u0442\u0438\u0446\u0438\u0437\u043c = objection_doubt\n4. \u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0435\u0433\u0430\u0442\u0438\u0432\u043d\u043e\u0439, \u043d\u043e \u0435\u0441\u043b\u0438 \u0422\u0415\u041a\u0423\u0429\u0415\u0415 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u043c\u0430\u0442\u0430 \u2014 \u044d\u0442\u043e \u041d\u0415 frustration"
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3,
"position": [
-3248,
464
],
"id": "385c47d3-57b7-431d-87b6-841dd3a703cf",
"name": "Classify Intent",
"retryOnFail": true,
"waitBetweenTries": 2000,
"onError": "continueErrorOutput"
},
{
"parameters": {
"promptType": "define",
"text": "={{ $json.message }}",
"hasOutputParser": true,
"options": {
"systemMessage": "={{ $json.full_prompt }}"
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3,
"position": [
-656,
800
],
"id": "d0c2e59a-bda7-4af9-a4cb-5ec61efbbc78",
"name": "Generate Response"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.output.needs_escalation }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"id": "0a52ed57-fcfd-4cc8-a9d5-520a62eb4f23"
}
],
"combinator": "and"
}
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.3,
"position": [
-304,
800
],
"id": "38848881-1ebf-4306-8a7d-2d80fc7be538",
"name": "Check Escalation"
},
{
"parameters": {
"jsCode": "const prev = $('Build Context').first().json;\nconst generation = $('Generate Response').first().json.output;\nconst instanceId = $('Prepare Prompt').first().json.instance_id;\n\nconst escapeSQL = (str) => String(str || '').replace(/'/g, \"''\");\n\nreturn [{\n json: {\n conversation_id: prev.conversation_id,\n client_id: prev.client_id,\n remoteJid: prev.remoteJid,\n phone: prev.phone,\n message: prev.message,\n response: generation.response,\n safe_message: escapeSQL(prev.message),\n safe_response: escapeSQL(generation.response),\n thinking: generation.thinking,\n has_answer: generation.has_answer,\n needs_escalation: generation.needs_escalation,\n source: generation.source || 'none',\n instance_id: instanceId\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
496,
688
],
"id": "29dc5b93-4bd1-4147-8a58-1b2bbf58077e",
"name": "Prepare Response"
},
{
"parameters": {
"jsCode": "const input = $('Parse Input').first().json;\n const intent = $json.output?.intent || $json.intent;\n\n let response;\n if (intent === 'attack') {\n response = '\u042f \u043a\u043e\u043d\u0441\u0443\u043b\u044c\u0442\u0430\u043d\u0442 Truffles. \u0427\u0435\u043c \u043c\u043e\u0433\u0443 \u043f\u043e\u043c\u043e\u0447\u044c \u043f\u043e \u0432\u043e\u043f\u0440\u043e\u0441\u0430\u043c AI-\u0431\u043e\u0442\u043e\u0432 \u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430?';\n } else {\n response = '\u042f \u043a\u043e\u043d\u0441\u0443\u043b\u044c\u0442\u0430\u043d\u0442 Truffles \u2014 \u043c\u044b \u0434\u0435\u043b\u0430\u0435\u043c AI-\u0431\u043e\u0442\u043e\u0432 \u0434\u043b\u044f WhatsApp, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043e\u0442\u0432\u0435\u0447\u0430\u044e\u0442 \u0432\u0430\u0448\u0438\u043c \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c 24/7. \u0411\u043e\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0434\u0430\u0436\u0435 \u043d\u043e\u0447\u044c\u044e, \u043a\u043e\u0433\u0434\u0430 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u043f\u0438\u0442 \ud83d\ude0a\\n\\n\u0415\u0441\u043b\u0438 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0443\u0435\u0442 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430 \u2014 \u0441\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0439\u0442\u0435!'; }\n\n return [{\n json: {\n phone: input.phone,\n remoteJid: input.remoteJid,\n response: response,\n intent: intent\n }\n }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2672,
848
],
"id": "79f0bd09-f6a1-4310-a0a3-793b14fecd3c",
"name": "Build Off-Topic Response"
},
{
"parameters": {
"url": "https://app.chatflow.kz/api/v1/send-text",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "token",
"value": "REDACTED_JWT"
},
{
"name": "instance_id",
"value": "={{ $('Load Prompt').first().json.instance_id }}"
},
{
"name": "jid",
"value": "={{ $json.remoteJid }}"
},
{
"name": "msg",
"value": "={{ $json.response }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-2448,
944
],
"id": "9393af23-76e9-4624-81e2-588808d82b48",
"name": "Send Off-Topic"
},
{
"parameters": {
"url": "https://app.chatflow.kz/api/v1/send-text",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "token",
"value": "REDACTED_JWT"
},
{
"name": "instance_id",
"value": "={{ $('Prepare Response').first().json.instance_id }}"
},
{
"name": "jid",
"value": "={{ $('Prepare Response').item.json.remoteJid }}"
},
{
"name": "msg",
"value": "={{ $('Prepare Response').first().json.response }}"
}
]
},
"options": {}
},
"name": "Me",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
944,
784
],
"id": "6e23777c-3f15-43a5-8f7a-4f046b7e291e"
},
{
"parameters": {
"url": "https://app.chatflow.kz/api/v1/send-text",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "token",
"value": "REDACTED_JWT"
},
{
"name": "instance_id",
"value": "={{ $('Prepare Response').first().json.instance_id }}"
},
{
"name": "jid",
"value": "={{ $('Prepare Response').item.json.remoteJid }}"
},
{
"name": "msg",
"value": "={{ $('Prepare Response').item.json.response }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-4592,
-96
],
"id": "dbaa64be-2315-4980-9766-076ba5f3f1ff",
"name": "Send to WhatsApp",
"disabled": true
},
{
"parameters": {
"chatId": "1969855532",
"text": "=\ud83d\udcf1 \u041a\u043b\u0438\u0435\u043d\u0442: {{ $('Prepare Response').item.json.phone }}\n\ud83d\udcac \u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435: {{ $('Prepare Response').item.json.message }}\n\n\ud83e\udd16 \u0411\u043e\u0442: {{ $('Prepare Response').item.json.response }}\n\ud83e\udde0 Intent: {{ $('Build Context').item.json.currentIntent }}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"id": "dc623a97-5934-4324-9f24-61814ac3a6c0",
"name": "Me1",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
944,
592
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"disabled": true
},
{
"parameters": {
"chatId": "1969855532",
"text": "=\ud83d\udea8 \u042d\u0421\u041a\u0410\u041b\u0410\u0426\u0418\u042f\n\n\ud83d\udcf1 \u041a\u043b\u0438\u0435\u043d\u0442: {{ $('Build Context').first().json.phone }}\n\ud83d\udcac \u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435: {{ $('Build Context').first().json.message }}\n\n\ud83e\udd16 \u041e\u0442\u0432\u0435\u0442 \u0431\u043e\u0442\u0430: {{ $json.output.response }}\n\ud83e\udde0 \u041f\u0440\u0438\u0447\u0438\u043d\u0430: {{ $json.output.thinking }}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"id": "12b399c1-a6d0-4752-a631-1206cda1b03d",
"name": "Me2",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
208,
1296
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"inputSource": "passthrough"
},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
-4592,
688
],
"id": "95c425cb-c680-4eee-87c0-e124020f1118",
"name": "Start"
},
{
"parameters": {
"jsCode": "// Intent Router \u2014 \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u043e\u0442\u043e\u043a \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 intent_type\n// greeting/answer \u2192 Main Flow (skip Classifier)\n// question/statement \u2192 Classifier\n\nconst input = $json;\n\n// \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0442 TurnDetector \u0447\u0435\u0440\u0435\u0437 Start\nconst rawData = $('Start').first().json;\nconst turnAnalysis = rawData.turn_analysis || {};\nconst intentType = turnAnalysis.intent_type || 'unknown';\nconst mergedMessage = turnAnalysis.merged_message || input.text || '';\nconst contextHint = turnAnalysis.context_hint || '';\n\n// \u041d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0442\u0435\u043a\u0441\u0442 \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438\nconst textLower = mergedMessage.toLowerCase().trim();\n\n// === \u042d\u0412\u0420\u0418\u0421\u0422\u0418\u041a\u0418 \u0414\u041b\u042f \u041f\u0420\u0418\u0412\u0415\u0422\u0421\u0422\u0412\u0418\u0419 ===\nconst greetingPatterns = [\n '\u043f\u0440\u0438\u0432\u0435\u0442', '\u0437\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439', '\u0434\u043e\u0431\u0440\u044b\u0439 \u0434\u0435\u043d\u044c', '\u0434\u043e\u0431\u0440\u044b\u0439 \u0432\u0435\u0447\u0435\u0440',\n '\u0434\u043e\u0431\u0440\u043e\u0435 \u0443\u0442\u0440\u043e', '\u0434\u043e\u0431\u0440\u044b\u0439', '\u0445\u0430\u0439', '\u0445\u0435\u043b\u043b\u043e', 'hello', 'hi',\n '\u0441\u0430\u043b\u0435\u043c', '\u0441\u04d9\u043b\u0435\u043c', '\u0430\u0441\u0441\u0430\u043b\u0430\u043c', '\u0437\u0434\u043e\u0440\u043e\u0432\u043e', '\u043f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e',\n '\u0434\u043e\u0431\u0440\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438', '\u0434\u0435\u043d\u044c \u0434\u043e\u0431\u0440\u044b\u0439', '\u0432\u0435\u0447\u0435\u0440 \u0434\u043e\u0431\u0440\u044b\u0439'\n];\n\nconst isGreetingByText = greetingPatterns.some(p => textLower.includes(p));\nconst isGreetingByHint = contextHint.toLowerCase().includes('\u043f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432');\n\n// === \u042d\u0412\u0420\u0418\u0421\u0422\u0418\u041a\u0418 \u0414\u041b\u042f \u041a\u041e\u0420\u041e\u0422\u041a\u0418\u0425 \u041e\u0422\u0412\u0415\u0422\u041e\u0412 ===\nconst shortAnswers = [\n '\u0434\u0430', '\u043d\u0435\u0442', '\u043e\u043a', '\u043e\u043a\u0435\u0439', '\u0445\u043e\u0440\u043e\u0448\u043e', '\u043b\u0430\u0434\u043d\u043e', '\u043f\u043e\u043d\u044f\u043b',\n '\u0430\u0433\u0430', '\u0443\u0433\u0443', '\u0434\u0430\u0432\u0430\u0439', '\u0433\u043e', '\u043a\u043e\u043d\u0435\u0447\u043d\u043e', '\u0441\u043e\u0433\u043b\u0430\u0441\u0435\u043d'\n];\nconst isShortAnswer = shortAnswers.includes(textLower);\n\n// === \u041e\u041f\u0420\u0415\u0414\u0415\u041b\u042f\u0415\u041c \u0420\u041e\u0423\u0422\u0418\u041d\u0413 ===\nlet skipClassifier = false;\nlet routeReason = '';\n\n// 1. \u041f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435 \u2014 \u0432\u0441\u0435\u0433\u0434\u0430 ON-TOPIC\nif (intentType === 'greeting' || isGreetingByText || isGreetingByHint) {\n skipClassifier = true;\n routeReason = 'greeting_on_topic';\n}\n// 2. \u041e\u0442\u0432\u0435\u0442 \u043d\u0430 \u0432\u043e\u043f\u0440\u043e\u0441 \u0431\u043e\u0442\u0430 \u2014 \u043a\u043b\u0438\u0435\u043d\u0442 \u0443\u0436\u0435 \u0432 \u0434\u0438\u0430\u043b\u043e\u0433\u0435\nelse if (intentType === 'answer' || isShortAnswer) {\n skipClassifier = true;\n routeReason = 'answer_in_dialog';\n}\n// 3. \u0412\u043e\u043f\u0440\u043e\u0441 \u0438\u043b\u0438 \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u2014 \u043d\u0443\u0436\u043d\u0430 \u0442\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\nelse {\n skipClassifier = false;\n routeReason = 'needs_topic_check';\n}\n\nreturn [{\n json: {\n ...input,\n _routing: {\n intentType,\n skipClassifier,\n routeReason,\n isGreetingByText,\n isGreetingByHint,\n isShortAnswer,\n mergedMessage\n }\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-4144,
592
],
"id": "045ea37e-bd83-4bef-9b90-bfaa8404b665",
"name": "Intent Router"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "f5f218a5-0a1e-412f-92a3-b3b5c7521df0",
"leftValue": "={{ $json._routing.skipClassifier }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-3920,
592
],
"id": "aa432e5b-f3bd-482c-9c15-137f4be2b020",
"name": "Skip Classifier?"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "deadlock-check-1",
"leftValue": "={{ $json.isDeadlock }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-1552,
656
],
"id": "deadlock-router-001",
"name": "Is Deadlock"
},
{
"parameters": {
"jsCode": "const ctx = $('Build Context').first().json;\nconst escapeSQL = (str) => String(str || '').replace(/'/g, \"''\");\n\nlet response;\nif (ctx.deadlockReason === 'human_request') {\n response = '\u041f\u0435\u0440\u0435\u0434\u0430\u044e \u0432\u0430\u0448 \u0432\u043e\u043f\u0440\u043e\u0441 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0443 \u2014 \u0441\u0432\u044f\u0436\u0435\u0442\u0441\u044f \u0432 \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0435\u0435 \u0432\u0440\u0435\u043c\u044f.';\n} else if (ctx.deadlockReason === 'frustration') {\n response = '\u041f\u043e\u043d\u0438\u043c\u0430\u044e, \u0447\u0442\u043e \u0432\u044b \u0440\u0430\u0441\u0441\u0442\u0440\u043e\u0435\u043d\u044b. \u041f\u0435\u0440\u0435\u0434\u0430\u044e \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0443 \u2014 \u0441\u0432\u044f\u0436\u0435\u0442\u0441\u044f \u0441 \u0432\u0430\u043c\u0438 \u043b\u0438\u0447\u043d\u043e.';\n} else if (ctx.deadlockReason === 'post_deadlock') {\n response = '\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0443\u0436\u0435 \u0432 \u043a\u0443\u0440\u0441\u0435 \u0438 \u0441\u0432\u044f\u0436\u0435\u0442\u0441\u044f \u0441 \u0432\u0430\u043c\u0438.';\n} else {\n response = '\u041f\u0435\u0440\u0435\u0434\u0430\u044e \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0443 \u2014 \u0441\u0432\u044f\u0436\u0435\u0442\u0441\u044f \u0432 \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0435\u0435 \u0432\u0440\u0435\u043c\u044f.';\n}\n\nreturn [{\n json: {\n conversation_id: ctx.conversation_id,\n client_id: ctx.client_id,\n remoteJid: ctx.remoteJid,\n phone: ctx.phone,\n message: ctx.message,\n response: response,\n safe_message: escapeSQL(ctx.message),\n safe_response: escapeSQL(response),\n isDeadlock: true,\n deadlockReason: ctx.deadlockReason\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2672,
1040
],
"id": "deadlock-response-001",
"name": "Deadlock Response"
},
{
"parameters": {
"jsCode": "const ctx = $('Build Context').first().json;\nconst prep = $('Prepare Response').first().json;\n\n// Format conversation for summarization\nconst history = ctx.history || '';\nconst lastMessage = ctx.message;\nconst botResponse = prep.response;\n\nconst fullConversation = history + '\\n\u041a\u043b\u0438\u0435\u043d\u0442: ' + lastMessage + '\\n\u0411\u043e\u0442: ' + botResponse;\n\nreturn [{\n json: {\n conversation_id: ctx.conversation_id,\n client_id: ctx.client_id,\n conversation: fullConversation,\n current_intent: ctx.currentIntent\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-80,
928
],
"id": "summarize-prep-001",
"name": "Prepare for Summary"
},
{
"parameters": {
"promptType": "define",
"text": "={{ $json.conversation }}",
"hasOutputParser": true,
"options": {
"systemMessage": "\u0422\u044b \u2014 \u0441\u0443\u043c\u043c\u0430\u0440\u0438\u0437\u0430\u0442\u043e\u0440 \u0434\u0438\u0430\u043b\u043e\u0433\u043e\u0432. \u0421\u0436\u043c\u0438 \u0434\u0438\u0430\u043b\u043e\u0433 \u0432 \u043a\u0440\u0430\u0442\u043a\u0443\u044e \u0432\u044b\u0436\u0438\u043c\u043a\u0443.\n\n\u0427\u0422\u041e \u0412\u041a\u041b\u042e\u0427\u0410\u0422\u042c:\n- \u0427\u0442\u043e \u0445\u043e\u0442\u0435\u043b \u043a\u043b\u0438\u0435\u043d\u0442 (intent)\n- \u041a\u0430\u043a\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441 (\u0435\u0441\u043b\u0438 \u0441\u043a\u0430\u0437\u0430\u043b)\n- \u0427\u0442\u043e \u0440\u0435\u0448\u0438\u043b\u0438: interested, refused, thinking, unknown\n- \u0412\u0430\u0436\u043d\u044b\u0435 \u0434\u0435\u0442\u0430\u043b\u0438\n\n\u0427\u0422\u041e \u041d\u0415 \u0412\u041a\u041b\u042e\u0427\u0410\u0422\u042c:\n- \u041f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u044f, \u043c\u0430\u0442, \u0436\u0430\u043b\u043e\u0431\u044b\n- \u041f\u043e\u0432\u0442\u043e\u0440\u044b\n\n\u0424\u041e\u0420\u041c\u0410\u0422 JSON:\n{\n \"summary\": \"1-2 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u044f\",\n \"key_facts\": {\n \"intent\": \"\u0447\u0442\u043e \u0445\u043e\u0442\u0435\u043b\",\n \"business\": \"\u0442\u0438\u043f \u0431\u0438\u0437\u043d\u0435\u0441\u0430 \u0438\u043b\u0438 unknown\",\n \"decision\": \"interested | refused | thinking | unknown\"\n },\n \"last_intent\": \"\u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 intent\"\n}"
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3,
"position": [
144,
816
],
"id": "summarize-llm-001",
"name": "Summarize Conversation"
},
{
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"summary\": {\"type\": \"string\"},\n \"key_facts\": {\n \"type\": \"object\",\n \"properties\": {\n \"intent\": {\"type\": \"string\"},\n \"business\": {\"type\": \"string\"},\n \"decision\": {\"type\": \"string\", \"enum\": [\"interested\", \"refused\", \"thinking\", \"unknown\"]}\n }\n },\n \"last_intent\": {\"type\": \"string\"}\n },\n \"required\": [\"summary\", \"key_facts\", \"last_intent\"]\n}"
},
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"typeVersion": 1.3,
"position": [
288,
1040
],
"id": "summary-parser-001",
"name": "Summary Parser"
},
{
"parameters": {
"model": {
"__rl": true,
"value": "gpt-4.1-mini",
"mode": "list",
"cachedResultName": "gpt-4.1-mini"
},
"builtInTools": {},
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.3,
"position": [
160,
1040
],
"id": "summary-model-001",
"name": "Summary Model",
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO conversation_summaries (conversation_id, client_id, summary, key_facts, last_intent)\nVALUES (\n '{{ $('Prepare for Summary').item.json.conversation_id }}',\n '{{ $('Prepare for Summary').item.json.client_id }}',\n '{{ $json.output.summary.replace(/'/g, \"''\") }}',\n '{{ JSON.stringify($json.output.key_facts).replace(/'/g, \"''\") }}'::jsonb,\n '{{ $json.output.last_intent.replace(/'/g, \"''\") }}'\n)\nON CONFLICT (conversation_id) DO UPDATE SET\n summary = EXCLUDED.summary,\n key_facts = EXCLUDED.key_facts,\n last_intent = EXCLUDED.last_intent,\n updated_at = NOW();",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
496,
1008
],
"id": "save-summary-001",
"name": "Save Summary",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT summary, key_facts, last_intent\nFROM conversation_summaries\nWHERE conversation_id = '{{ $('Upsert User').item.json.conversation_id }}'\nLIMIT 1;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-4592,
1584
],
"id": "load-summary-001",
"name": "Load Summary",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n escalated_at,\n CASE \n WHEN escalated_at IS NULL THEN FALSE\n WHEN escalated_at > NOW() - INTERVAL '30 minutes' THEN TRUE\n ELSE FALSE\n END as is_in_cooldown\nFROM conversations\nWHERE id = '{{ $('Upsert User').item.json.conversation_id }}';",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-4592,
128
],
"id": "load-escalation-status-001",
"name": "Load Escalation Status",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "UPDATE conversations \nSET escalated_at = NOW()\nWHERE id = '{{ $('Build Context').first().json.conversation_id }}';",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
496,
1360
],
"id": "update-escalation-001",
"name": "Update Escalation Time",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const ctx = $('Build Context').first().json;\nconst clientSlug = $('Parse Input').first().json.client_slug || 'truffles';\n\n// 1. Get embedding from BGE-M3\nconst bgeResponse = await this.helpers.httpRequest({\n method: 'POST',\n url: 'http://bge-m3:80/embed',\n body: { inputs: ctx.message },\n json: true\n});\n\nconst vector = bgeResponse[0];\n\n// 2. Search Qdrant with client filter\nconst searchPayload = {\n vector: vector,\n limit: 5,\n with_payload: true,\n filter: {\n must: [\n { key: 'metadata.client_slug', match: { value: clientSlug } }\n ]\n }\n};\n\nconst searchResponse = await this.helpers.httpRequest({\n method: 'POST',\n url: 'http://qdrant:6333/collections/truffles_knowledge/points/search',\n headers: {\n 'api-key': 'REDACTED_PASSWORD',\n 'Content-Type': 'application/json'\n },\n body: searchPayload,\n json: true\n});\n\nconst results = searchResponse.result || [];\n\n// 3. Format knowledge\nconst knowledge = results\n .map(r => r.payload?.content || '')\n .filter(t => t)\n .join('\\n\\n---\\n\\n') || '(\u043d\u0435\u0442 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0432 \u0431\u0430\u0437\u0435)';\n\nreturn {\n json: {\n ...ctx,\n knowledge: knowledge,\n rag_scores: results.map(r => r.score),\n client_slug: clientSlug\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1328,
800
],
"id": "7b1a9b49-1856-4f17-9c74-4a9e2fb3fa59",
"name": "RAG Search"
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO message_traces (\n phone,\n conversation_id,\n message,\n intent,\n rag_top_score,\n rag_top_doc,\n rag_scores,\n response,\n has_answer,\n needs_escalation\n) VALUES (\n '{{ $('Build Context').first().json.phone }}',\n '{{ $('Build Context').first().json.conversation_id }}',\n '{{ $('Build Context').first().json.message.replace(/'/g, \"''\") }}',\n '{{ $('Build Context').first().json.currentIntent }}',\n {{ $('RAG Search').first().json.rag_scores[0] || 0 }},\n '{{ $('RAG Search').first().json.knowledge.substring(0, 50).replace(/'/g, \"''\") }}',\n '{{ JSON.stringify($('RAG Search').first().json.rag_scores || []) }}',\n '{{ $json.response.replace(/'/g, \"''\") }}',\n {{ $json.has_answer || false }},\n {{ $json.needs_escalation || false }}\n);",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
720,
880
],
"id": "09f4ae61-aa8a-4f8e-9ab2-7c3e880b18e8",
"name": "Save Trace",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n p.text as system_prompt,\n c.config->>'instance_id' as instance_id\nFROM clients c\nLEFT JOIN prompts p ON p.client_id = c.id AND p.name IN ('system', 'system_prompt') AND p.is_active = true\nWHERE c.name = '{{ $('Parse Input').first().json.client_slug }}'\nLIMIT 1;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-2000,
656
],
"id": "load-prompt-001",
"name": "Load Prompt",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// \u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u043f\u043e\u043b\u043d\u044b\u0439 prompt + instance_id\nconst ctx = $json; // \u043e\u0442 Add Knowledge\nconst loadedPrompt = $('Load Prompt').first().json;\n\nconst basePrompt = loadedPrompt.system_prompt || `\u0422\u044b \u2014 AI-\u043f\u043e\u043c\u043e\u0449\u043d\u0438\u043a. \u041e\u0442\u0432\u0435\u0447\u0430\u0439 \u043a\u0440\u0430\u0442\u043a\u043e \u0438 \u043f\u043e \u0434\u0435\u043b\u0443.`;\nconst instanceId = loadedPrompt.instance_id || '';\n\nconst fullPrompt = basePrompt + `\n\n## \u0414\u0410\u041d\u041d\u042b\u0415\n\u0418\u0441\u0442\u043e\u0440\u0438\u044f: ${ctx.history}\n\u0411\u0430\u0437\u0430 \u0437\u043d\u0430\u043d\u0438\u0439: ${ctx.knowledge}\n\nIntent: ${ctx.currentIntent}\nisInCooldown: ${ctx.isInCooldown}\n\n## \u042d\u0421\u041a\u0410\u041b\u0410\u0426\u0418\u042f (needs_escalation = true)\n\u0421\u0442\u0430\u0432\u044c needs_escalation = true \u043a\u043e\u0433\u0434\u0430:\n1. \u041c\u0410\u0422 \u0438\u043b\u0438 \u041e\u0421\u041a\u041e\u0420\u0411\u041b\u0415\u041d\u0418\u042f \u0432 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0438\n2. \u041a\u043b\u0438\u0435\u043d\u0442 \u042f\u0412\u041d\u041e \u043f\u0440\u043e\u0441\u0438\u0442 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430/\u0447\u0435\u043b\u043e\u0432\u0435\u043a\u0430\n3. \u041a\u043b\u0438\u0435\u043d\u0442 2+ \u0440\u0430\u0437\u0430 \u0432\u044b\u0440\u0430\u0436\u0430\u043b \u043d\u0435\u0434\u043e\u0432\u043e\u043b\u044c\u0441\u0442\u0432\u043e\n4. \u0421\u043b\u043e\u0436\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441 \u0432\u043d\u0435 \u0431\u0430\u0437\u044b \u0437\u043d\u0430\u043d\u0438\u0439\n\n## COOLDOWN\n\u0415\u0441\u043b\u0438 isInCooldown = true \u0418 intent \u041d\u0415 human_request:\n- \u041d\u0415 \u044d\u0441\u043a\u0430\u043b\u0438\u0440\u0443\u0439\n- \u041e\u0442\u0432\u0435\u0442\u044c: \"\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0443\u0436\u0435 \u0432 \u043a\u0443\u0440\u0441\u0435.\"\n\n## \u041f\u0420\u0410\u0412\u0418\u041b\u0410\n1. \u041a\u043e\u0440\u043e\u0442\u043a\u043e (3-4 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u044f)\n2. \u041d\u0415 \u0412\u042b\u0414\u0423\u041c\u042b\u0412\u0410\u0419 \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0437 \u0431\u0430\u0437\u044b \u0437\u043d\u0430\u043d\u0438\u0439\n3. \u0423\u043a\u0430\u0437\u044b\u0432\u0430\u0439 SOURCE\n4. \u0415\u0441\u043b\u0438 \u043d\u0435\u0442 \u0438\u043d\u0444\u043e \u2014 \u0441\u043a\u0430\u0436\u0438 \u0447\u0435\u0441\u0442\u043d\u043e\n\n## SOURCE\n\u0423\u043a\u0430\u0436\u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442: faq.md, services.md, objections.md, rules.md, \u0438\u043b\u0438 'none'\n`;\n\nreturn [{\n json: {\n ...ctx,\n full_prompt: fullPrompt,\n instance_id: instanceId\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-880,
800
],
"id": "prepare-prompt-001",
"name": "Prepare Prompt"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n c.bot_status,\n c.bot_muted_until,\n CASE \n WHEN c.bot_status = 'muted' AND c.bot_muted_until > NOW() THEN true\n ELSE false\n END as is_muted\nFROM conversations c\nJOIN users u ON c.user_id = u.id\nWHERE u.phone = '{{ $('Parse Input').first().json.phone }}'\n AND c.status = 'active'\nORDER BY c.last_message_at DESC\nLIMIT 1;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-4144,
992
],
"id": "check-muted-001",
"name": "Check Bot Muted",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "muted-check",
"leftValue": "={{ $json.is_muted }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-3920,
992
],
"id": "is-muted-001",
"name": "Is Bot Muted?"
},
{
"parameters": {
"jsCode": "// \u0411\u043e\u0442 \u0437\u0430\u043c\u044c\u044e\u0447\u0435\u043d - \u0432\u044b\u0445\u043e\u0434\u0438\u043c \u043c\u043e\u043b\u0447\u0430, \u043d\u0435 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u043c\nreturn [];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-3696,
992
],
"id": "silent-exit-muted-001",
"name": "Silent Exit (Muted)"
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "7jGZrdbaAAvtTnQX",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "autoMapInputData",
"value": {}
},
"options": {}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
208,
528
],
"id": "call-escalation-001",
"name": "Call Escalation Handler"
},
{
"parameters": {
"jsCode": "// \u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f Escalation Handler\nconst ctx = $('Build Context').first().json;\nconst genResponse = $json.output || {};\n\nreturn [{\n json: {\n conversation_id: ctx.conversation_id,\n client_id: ctx.client_id,\n phone: ctx.phone,\n remoteJid: ctx.remoteJid,\n message: ctx.message,\n reason: ctx.deadlockReaso
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.
openAiApipostgrestelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
6_Multi-Agent_4vaEvzlaMrgovhNz. Uses postgres, httpRequest, lmChatOpenAi, outputParserStructured. Event-driven trigger; 54 nodes.
Source: https://github.com/k1ddy/Truffles-AI-Employee/blob/2c3d53ca9cf17e420c8cd053b6b0a89e27f2c138/.archive/workflow/6_Multi-Agent_4vaEvzlaMrgovhNz.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.
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
Automated Research Report Generation with OpenAI, Wikipedia, Google Search, and Gmail/Telegram. Uses lmChatOpenAi, memoryBufferWindow, toolHttpRequest, agent. Event-driven trigger; 26 nodes.
This workflow automates the process of generating professional research reports for researchers, students, and professionals. It eliminates manual research and report formatting by aggregating data, g
[HUB] Жора AI Classifier. Uses executeWorkflowTrigger, httpRequest, supabase, telegram. Event-driven trigger; 14 nodes.
Generate AI viral videos with NanoBanana & VEO3, shared on socials via Blotato 2. Uses @blotato/n8n-nodes-blotato, googleSheets, lmChatOpenAi, toolThink. Event-driven trigger; 94 nodes.