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.
Template Carnaval - time instagram. Uses toolWorkflow, lmChatOpenAi, memoryBufferWindow, agent. Event-driven trigger; 56 nodes.
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
LinkedIn Growth & Intelligence Agent. Uses telegram, postgres, executeWorkflowTrigger, @brightdata/n8n-nodes-brightdata. Event-driven trigger; 46 nodes.
Analysis Anna. Uses httpRequest, memoryBufferWindow, telegram, outputParserStructured. Event-driven trigger; 38 nodes.
Analysis Anna. Uses httpRequest, memoryBufferWindow, telegram, outputParserStructured. Event-driven trigger; 38 nodes.