This workflow corresponds to n8n.io template #7851 — we link there as the canonical source.
This workflow follows the Agent → Chainllm 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 →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "fccc9f50-71fa-4e25-9b15-8fd540ddc2fa",
"name": "Model Selector",
"type": "@n8n/n8n-nodes-langchain.modelSelector",
"position": [
800,
1536
],
"parameters": {
"rules": {
"rule": [
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "976d83bb-7e9e-4aab-9722-25a9e238164f",
"operator": {
"type": "number",
"operation": "equals"
},
"leftValue": "={{ $json.output.difficulty }}",
"rightValue": 1
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1e68688d-73fe-47c1-9b35-a1e226220bcd",
"operator": {
"type": "number",
"operation": "equals"
},
"leftValue": "={{ $json.output.difficulty }}",
"rightValue": 2
}
]
},
"modelIndex": 2
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "61d58197-db59-4cd7-bc41-bbeaf5e7b069",
"operator": {
"type": "number",
"operation": "equals"
},
"leftValue": "={{ $json.output.difficulty }}",
"rightValue": 3
}
]
},
"modelIndex": 3
}
]
},
"numberInputs": 3
},
"typeVersion": 1
},
{
"id": "cf5b2ea0-78c5-47bf-a3c1-4c59c9a32f76",
"name": "Structured Output Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
336,
1504
],
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"difficulty\": {\n \"type\": \"integer\",\n \"enum\": [1, 2, 3]\n },\n \"context\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\"difficulty\", \"context\"]\n}\n"
},
"typeVersion": 1.3
},
{
"id": "3a60caa7-eb1a-4bbd-88fc-55d7a3ffae29",
"name": "Gemini 2.5 Flash Lite",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
720,
1728
],
"parameters": {
"options": {},
"modelName": "models/gemini-2.5-flash-lite"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "412d35ed-cd49-4d20-b425-2f556ef175b1",
"name": "Gemini 2.5 Flash",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
880,
1728
],
"parameters": {
"options": {}
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "2cc40e2c-a2be-435e-8540-9235efc41e08",
"name": "Gemini 2.5 Pro",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
1024,
1728
],
"parameters": {
"options": {},
"modelName": "models/gemini-2.5-pro"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "35f56404-6998-4952-a7f3-8716712bf96a",
"name": "Get Chat Memory",
"type": "n8n-nodes-base.postgres",
"onError": "continueRegularOutput",
"position": [
-256,
1328
],
"parameters": {
"limit": 25,
"table": {
"__rl": true,
"mode": "list",
"value": "chat_memory",
"cachedResultName": "chat_memory"
},
"schema": {
"__rl": true,
"mode": "list",
"value": "public"
},
"options": {},
"operation": "select"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6,
"alwaysOutputData": true
},
{
"id": "5e6058bc-a3b8-4827-954e-85e3a000a986",
"name": "MarkdownV2",
"type": "n8n-nodes-base.code",
"position": [
1136,
1264
],
"parameters": {
"jsCode": "/**\n * MarkdownV2-safe formatter + auto-chunker for Telegram (n8n Code node)\n * --------------------------------------------------------------------\n * - Allows: *bold*, _italic_, ||spoiler||, [label](url)\n * - Escapes everything else for Telegram MarkdownV2\n * - Validates/normalizes URLs\n * - Converts \"# Heading\" lines to bold titles\n * - Splits long messages into <= 4096-char chunks (uses a 4000-char budget)\n * - Outputs one item per chunk so the Telegram node sends all parts\n *\n * Recommended: Run this node in \"Run Once for All Items\".\n */\n\nconst MAX_TELEGRAM = 4096;\nconst SAFE_BUDGET = 4000; // small margin to avoid edge overflows\n\n// ============ MarkdownV2 helpers ============\nfunction escapeMarkdownV2(text) {\n if (!text) return '';\n return String(text).replace(/([\\\\_*[\\]()~`>#+\\-=|{}.!])/g, '\\\\$1');\n}\n\nfunction escapeForUrl(url) {\n return String(url).replace(/[)\\\\]/g, '\\\\$&');\n}\n\nfunction normalizeAndValidateUrl(url) {\n let raw = String(url || '').trim();\n try {\n const u = new URL(raw);\n return u.toString();\n } catch {}\n // Try https:// for bare domains\n const domainLike = /^[a-z0-9.-]+\\.[a-z]{2,}([/:?#].*)?$/i.test(raw);\n if (domainLike) {\n try {\n const u2 = new URL('https://' + raw);\n return u2.toString();\n } catch {}\n }\n return null;\n}\n\nfunction normalizeHeadings(text) {\n // Turn \"# Title\" \u2192 \"*Title*\"\n return text.replace(/^(#{1,6})\\s+(.*)$/gm, (m, hashes, title) => `*${title.trim()}*`);\n}\n\nfunction normalizeCommonMd(text) {\n return String(text)\n .replace(/\\*\\*([\\s\\S]*?)\\*\\*/g, '*$1*') // **bold** \u2192 *bold*\n .replace(/__([\\s\\S]*?)__/g, '_$1_'); // __italic__ \u2192 _italic_\n}\n\n/**\n * Convert incoming text to Telegram-safe MarkdownV2.\n */\nfunction processMarkdownV2Safe(inputText) {\n if (!inputText) return '';\n\n let text = normalizeCommonMd(String(inputText));\n text = normalizeHeadings(text);\n\n const placeholders = { links: [], bolds: [], italics: [], spoilers: [] };\n\n // Links: keep safe via placeholders during escaping\n text = text.replace(/\\[([^\\]\\n]+)\\]\\(([^)]+)\\)/g, (m, label, url) => {\n const normalizedUrl = normalizeAndValidateUrl(url);\n if (!normalizedUrl) return escapeMarkdownV2(label);\n const idx = placeholders.links.length;\n const ph = `\u27ecL${idx}\u27ed`;\n const safeLabel = escapeMarkdownV2(label);\n const safeUrl = escapeForUrl(normalizedUrl);\n placeholders.links.push(`[${safeLabel}](${safeUrl})`);\n return ph;\n });\n\n // Bold\n text = text.replace(/\\*([\\s\\S]+?)\\*/g, (m, inner) => {\n const idx = placeholders.bolds.length;\n const ph = `\u27ecB${idx}\u27ed`;\n placeholders.bolds.push(`*${escapeMarkdownV2(inner)}*`);\n return ph;\n });\n\n // Italic\n text = text.replace(/_([\\s\\S]+?)_/g, (m, inner) => {\n const idx = placeholders.italics.length;\n const ph = `\u27ecI${idx}\u27ed`;\n placeholders.italics.push(`_${escapeMarkdownV2(inner)}_`);\n return ph;\n });\n\n // Spoilers\n text = text.replace(/\\|\\|([\\s\\S]+?)\\|\\|/g, (m, inner) => {\n const idx = placeholders.spoilers.length;\n const ph = `\u27ecS${idx}\u27ed`;\n placeholders.spoilers.push(`||${escapeMarkdownV2(inner)}||`);\n return ph;\n });\n\n // Escape everything else\n text = escapeMarkdownV2(text);\n\n // Restore placeholders\n placeholders.links.forEach((md, i) => { text = text.replace(`\u27ecL${i}\u27ed`, md); });\n placeholders.bolds.forEach((md, i) => { text = text.replace(`\u27ecB${i}\u27ed`, md); });\n placeholders.italics.forEach((md, i) => { text = text.replace(`\u27ecI${i}\u27ed`, md); });\n placeholders.spoilers.forEach((md, i) => { text = text.replace(`\u27ecS${i}\u27ed`, md); });\n\n return text;\n}\n\n// ============ Chunking helpers ============\n/**\n * Split text into Telegram-safe chunks <= maxLen.\n * Prefers paragraph boundaries, then sentence boundaries, then words.\n * Falls back to hard cuts only when unavoidable (e.g., extremely long URL).\n */\nfunction chunkForTelegram(text, maxLen = SAFE_BUDGET) {\n if (!text || text.length <= maxLen) return [text || ''];\n\n const parts = [];\n let buffer = '';\n\n const flush = () => {\n if (buffer) {\n parts.push(buffer);\n buffer = '';\n }\n };\n\n // 1) Paragraph-level packing\n const paragraphs = text.split(/\\n{2,}/);\n for (const pRaw of paragraphs) {\n const p = pRaw; // keep paragraph as-is\n const candidate = buffer ? buffer + '\\n\\n' + p : p;\n if (candidate.length <= maxLen) {\n buffer = candidate;\n continue;\n }\n if (p.length <= maxLen) {\n flush();\n buffer = p;\n continue;\n }\n\n // 2) Sentence-level packing (paragraph is still too big)\n flush();\n const sentences = p.split(/(?<=[.!?\u2026])\\s+(?=[^\\s])/u);\n let sBuf = '';\n for (const s of sentences) {\n const sCandidate = sBuf ? sBuf + ' ' + s : s;\n if (sCandidate.length <= maxLen) {\n sBuf = sCandidate;\n continue;\n }\n if (s.length <= maxLen) {\n if (sBuf) parts.push(sBuf);\n sBuf = s;\n continue;\n }\n\n // 3) Word-level packing (sentence is still too big)\n if (sBuf) { parts.push(sBuf); sBuf = ''; }\n let wBuf = '';\n const words = s.split(/\\s+/);\n for (const w of words) {\n const wCandidate = wBuf ? wBuf + ' ' + w : w;\n if (wCandidate.length <= maxLen) {\n wBuf = wCandidate;\n continue;\n }\n if (w.length <= maxLen) {\n if (wBuf) parts.push(wBuf);\n wBuf = w;\n continue;\n }\n // 4) Hard split (extremely long token, e.g., massive URL)\n if (wBuf) { parts.push(wBuf); wBuf = ''; }\n const re = new RegExp(`.{1,${maxLen}}`, 'g');\n const hardPieces = w.match(re) || [];\n parts.push(...hardPieces);\n }\n if (wBuf) parts.push(wBuf);\n }\n if (sBuf) parts.push(sBuf);\n }\n if (buffer) parts.push(buffer);\n\n // Final safety pass: trim chunks that might still exceed MAX_TELEGRAM\n return parts.flatMap(part => {\n if (part.length <= MAX_TELEGRAM) return [part];\n const re = new RegExp(`.{1,${SAFE_BUDGET}}`, 'g');\n return part.match(re) || [];\n });\n}\n\n// ============ Main ============\nconst inputItems = $input.all();\nconst out = [];\n\nfor (const item of inputItems) {\n const j = item.json || {};\n const raw =\n j.message ?? j.output ?? j.text ?? j.content ?? '';\n\n const formatted = processMarkdownV2Safe(raw);\n const chunks = chunkForTelegram(formatted, SAFE_BUDGET);\n\n chunks.forEach((chunk, idx) => {\n out.push({\n json: {\n ...j,\n message: chunk,\n message_part_index: idx + 1,\n message_parts_total: chunks.length,\n },\n binary: item.binary,\n });\n });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "6ce0f61c-03d1-4bef-827f-ac3c80f48939",
"name": "Send a text message",
"type": "n8n-nodes-base.telegram",
"position": [
1312,
1264
],
"parameters": {
"text": "={{ $json.message }}",
"chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
"additionalFields": {
"parse_mode": "MarkdownV2",
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "662e6beb-7498-42c2-ba0c-6530c100bf97",
"name": "Fix mime",
"type": "n8n-nodes-base.code",
"position": [
-1184,
1328
],
"parameters": {
"jsCode": "// --- Mapa Extendido de Tipos MIME ---\n// Una lista completa para cubrir la mayor\u00eda de los formatos de archivo comunes.\nconst mimeMap = {\n // --- Formatos de Documentos ---\n 'pdf': 'application/pdf',\n 'txt': 'text/plain',\n 'rtf': 'application/rtf',\n 'csv': 'text/csv',\n 'html': 'text/html',\n 'htm': 'text/html',\n 'json': 'application/json',\n 'xml': 'application/xml', // 'text/xml' tambi\u00e9n es v\u00e1lido pero 'application/xml' es m\u00e1s com\u00fan\n 'yaml': 'application/x-yaml',\n 'yml': 'application/x-yaml',\n\n // --- Formatos de Microsoft Office ---\n 'doc': 'application/msword',\n 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'xls': 'application/vnd.ms-excel',\n 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'ppt': 'application/vnd.ms-powerpoint',\n 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'pub': 'application/vnd.ms-publisher',\n\n // --- Formatos de OpenOffice / LibreOffice ---\n 'odt': 'application/vnd.oasis.opendocument.text',\n 'ods': 'application/vnd.oasis.opendocument.spreadsheet',\n 'odp': 'application/vnd.oasis.opendocument.presentation',\n 'odg': 'application/vnd.oasis.opendocument.graphics',\n\n // --- Formatos de Apple iWork ---\n 'pages': 'application/vnd.apple.pages',\n 'numbers': 'application/vnd.apple.numbers',\n 'key': 'application/vnd.apple.keynote',\n\n // --- Formatos de Imagen ---\n 'png': 'image/png',\n 'jpg': 'image/jpeg',\n 'jpeg': 'image/jpeg',\n 'gif': 'image/gif',\n 'webp': 'image/webp',\n 'svg': 'image/svg+xml',\n 'bmp': 'image/bmp',\n 'ico': 'image/vnd.microsoft.icon',\n 'tif': 'image/tiff',\n 'tiff': 'image/tiff',\n 'heic': 'image/heic',\n 'heif': 'image/heif',\n\n // --- Formatos de Audio ---\n 'mp3': 'audio/mpeg',\n 'wav': 'audio/wav',\n 'oga': 'audio/ogg',\n 'ogg': 'audio/ogg',\n 'flac': 'audio/flac',\n 'm4a': 'audio/mp4',\n 'aac': 'audio/aac',\n 'opus': 'audio/opus',\n 'wma': 'audio/x-ms-wma',\n 'mid': 'audio/midi',\n 'midi': 'audio/midi',\n\n // --- Formatos de Video ---\n 'mp4': 'video/mp4',\n 'mov': 'video/quicktime',\n 'webm': 'video/webm',\n 'mpeg': 'video/mpeg',\n 'mpg': 'video/mpeg',\n 'avi': 'video/x-msvideo',\n 'wmv': 'video/x-ms-wmv',\n 'flv': 'video/x-flv',\n 'mkv': 'video/x-matroska',\n\n // --- Formatos de Archivos y Compresi\u00f3n ---\n 'zip': 'application/zip',\n 'rar': 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n 'tar': 'application/x-tar',\n 'gz': 'application/gzip',\n 'bz2': 'application/x-bzip2',\n\n // --- Otros Formatos ---\n 'epub': 'application/epub+zip',\n 'ics': 'text/calendar',\n 'vcf': 'text/vcard',\n 'js': 'text/javascript',\n 'css': 'text/css',\n 'sh': 'application/x-sh',\n 'py': 'text/x-python',\n};\n\n// --- L\u00f3gica de Procesamiento (sin cambios) ---\n\n// Obtenemos todos los items que llegan al nodo\nconst items = $input.all();\n\n// Iteramos sobre cada item para procesarlo\nfor (const item of items) {\n // Verificamos que el item tenga datos binarios para procesar\n if (item.binary && item.binary['data']) {\n // Obtenemos el nombre del archivo de forma segura\n const fileName = item.binary['data'].fileName || '';\n if (!fileName) {\n continue; // Si no hay nombre de archivo, pasamos al siguiente item\n }\n\n // Extraemos la extensi\u00f3n del archivo de forma robusta\n const extension = fileName.slice((fileName.lastIndexOf(\".\") - 1 >>> 0) + 2).toLowerCase();\n\n // Buscamos la extensi\u00f3n en nuestro mapa\n const newMimeType = mimeMap[extension];\n\n // Si encontramos una coincidencia en el mapa, actualizamos el mimeType\n if (newMimeType) {\n if(item.binary['data'].mimeType !== newMimeType) {\n console.log(`Cambiando mimeType para '${fileName}' de '${item.binary['data'].mimeType}' a '${newMimeType}'.`);\n item.binary['data'].mimeType = newMimeType;\n }\n }\n }\n}\n\n// Devolvemos todos los items, modificados o no\nreturn items;"
},
"typeVersion": 2
},
{
"id": "07281d83-6c94-4952-819e-288a4435da24",
"name": "Typing\u2026",
"type": "n8n-nodes-base.telegram",
"position": [
-1744,
1184
],
"parameters": {
"chatId": "={{ $json.message.chat.id }}",
"operation": "sendChatAction"
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "e211d4af-9ebd-4524-84ef-934349b6b155",
"name": "get_message (Audio/Video message)",
"type": "n8n-nodes-base.set",
"position": [
-848,
1328
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "d8935452-fe20-469d-a68d-1aad056cb8dd",
"name": "message",
"type": "string",
"value": "=Voice message description:{{ $json.candidates?.[0]?.content?.parts?.[0]?.text || $json.content?.parts?.[0]?.text }}"
},
{
"id": "93f1bba1-1180-404a-93ca-c34cf1d1b7ac",
"name": "chat_id",
"type": "string",
"value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "9c28c37b-1d37-4f23-90a3-872f5e3870a7",
"name": "Analyze voice message",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
-1024,
1328
],
"parameters": {
"text": "What's in this audio message from telegram user?",
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-2.5-pro",
"cachedResultName": "models/gemini-2.5-pro"
},
"options": {},
"resource": "audio",
"inputType": "binary",
"operation": "analyze"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "ea646e8c-e6d0-47ea-8743-45eade4c1c4d",
"name": "get_message (text)",
"type": "n8n-nodes-base.set",
"position": [
-1184,
1152
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "801ec600-22ad-4a94-a2b4-ae72eb271df0",
"name": "message",
"type": "string",
"value": "={{ $('Telegram Trigger').item.json.message.text }}"
},
{
"id": "263071fb-bcdf-42b0-bb46-71b75fa0bf2a",
"name": "chat_id",
"type": "string",
"value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "1ca88076-5c56-4561-bb8a-fdf1db080d2a",
"name": "Input Message Router1",
"type": "n8n-nodes-base.switch",
"position": [
-1600,
1312
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Text",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "fcb767ee-565e-4b56-a54e-6f97f739fc24",
"operator": {
"type": "string",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $('Telegram Trigger').item.json.message.text }}",
"rightValue": ""
}
]
},
"renameOutput": true
},
{
"outputKey": "Voice Message",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c1016c40-f8f2-4e08-8ec8-5cdb88f5c87a",
"operator": {
"type": "object",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $('Telegram Trigger').item.json.message.voice }}",
"rightValue": ""
}
]
},
"renameOutput": true
}
]
},
"options": {
"ignoreCase": false,
"fallbackOutput": "extra",
"allMatchingOutputs": true
}
},
"typeVersion": 3.2
},
{
"id": "298fa06c-d3fc-4d20-a2d4-fe879a01527c",
"name": "Download Voice Message",
"type": "n8n-nodes-base.telegram",
"position": [
-1360,
1328
],
"parameters": {
"fileId": "={{ $('Telegram Trigger').item.json.message.voice.file_id }}",
"resource": "file",
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "5a1dc986-6bfe-4400-bdbf-d4f5817e0f7e",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"position": [
-1888,
1328
],
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "0188bc12-4fc1-4149-8c80-edf8e80009ec",
"name": "Normalize input",
"type": "n8n-nodes-base.set",
"position": [
-464,
1328
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "3c2fa4f9-079c-4729-9737-66ce8f42029f",
"name": "message",
"type": "string",
"value": "={{ $json.message }}"
},
{
"id": "b6e57068-8ece-4725-b07d-1b00069943b0",
"name": "chat_id",
"type": "string",
"value": "={{ $json.chat_id }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "4db66ea5-0817-4326-9991-ab6a905ec0ee",
"name": "Aggregate",
"type": "n8n-nodes-base.aggregate",
"position": [
-48,
1328
],
"parameters": {
"options": {},
"fieldsToAggregate": {
"fieldToAggregate": [
{
"fieldToAggregate": "message"
}
]
}
},
"typeVersion": 1
},
{
"id": "a4ce42c2-d1cc-4233-9ee7-2aac9a5f0c45",
"name": "Google Gemini 2.5 Flash Lite",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
192,
1504
],
"parameters": {
"options": {},
"modelName": "models/gemini-2.5-flash-lite"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "5c2cd387-3b15-4869-87da-6873437a0d33",
"name": "get_error_message",
"type": "n8n-nodes-base.set",
"position": [
-1184,
1504
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "d8935452-fe20-469d-a68d-1aad056cb8dd",
"name": "message",
"type": "string",
"value": "=It was not possible to process the file.File type not supported."
},
{
"id": "38ba2498-2141-4a04-a22a-64563fe2ee6f",
"name": "chat_id",
"type": "string",
"value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "d31898fc-4d10-43ea-bc5d-402ac29f3f4b",
"name": "Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
800,
1328
],
"parameters": {
"text": "=Context: {{ $json.output.context }}\nUser request: {{ $('Normalize input').item.json.message }}",
"options": {
"returnIntermediateSteps": true
},
"promptType": "define"
},
"typeVersion": 2.1
},
{
"id": "ee146b55-17b6-414c-be6b-f8416ece7075",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1984,
1104
],
"parameters": {
"color": 5,
"width": 1312,
"height": 624,
"content": ""
},
"typeVersion": 1
},
{
"id": "e677f7d7-2cfc-476d-bdc0-08b4cf17cb4d",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-576,
1248
],
"parameters": {
"color": 3,
"width": 1216,
"height": 400,
"content": ""
},
"typeVersion": 1
},
{
"id": "d36b2018-ca63-4c1f-a40e-46d1452753e2",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-1888,
880
],
"parameters": {},
"typeVersion": 1
},
{
"id": "451eb06f-5b47-44c5-86fa-20587cf89870",
"name": "Create Chat Memory Table",
"type": "n8n-nodes-base.postgres",
"position": [
-1696,
880
],
"parameters": {
"query": "CREATE TABLE IF NOT EXISTS public.chat_memory (\n id SERIAL PRIMARY KEY,\n session_id VARCHAR(255) NOT NULL,\n message TEXT DEFAULT 'Could not get data'\n) TABLESPACE pg_default;\n\nCREATE INDEX IF NOT EXISTS chat_memory_session_id_idx \nON public.chat_memory USING btree (session_id) \nTABLESPACE pg_default;\n",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "fb4c1415-dd98-4944-8c12-e69f8d445be7",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1984,
848
],
"parameters": {
"color": 7,
"width": 512,
"height": 208,
"content": ""
},
"typeVersion": 1
},
{
"id": "50ac5adc-ce42-4f35-84ac-cf8e2bea5d38",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
672,
1248
],
"parameters": {
"color": 6,
"width": 880,
"height": 624,
"content": ""
},
"typeVersion": 1
},
{
"id": "97859eb8-951f-4abf-8b89-63299b456304",
"name": "Update Chat Memory (User and Agent)",
"type": "n8n-nodes-base.postgres",
"position": [
1136,
1424
],
"parameters": {
"table": {
"__rl": true,
"mode": "list",
"value": "chat_memory",
"cachedResultName": "chat_memory"
},
"schema": {
"__rl": true,
"mode": "list",
"value": "public"
},
"columns": {
"value": {
"message": "=User: {{ $('Normalize input').item.json.message }}\nAgent: {{ $json.output }}\n",
"session_id": "={{ $('Normalize input').item.json.chat_id }}"
},
"schema": [
{
"id": "id",
"type": "number",
"display": true,
"removed": true,
"required": false,
"displayName": "id",
"defaultMatch": true,
"canBeUsedToMatch": true
},
{
"id": "session_id",
"type": "string",
"display": true,
"required": true,
"displayName": "session_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "message",
"type": "string",
"display": true,
"required": false,
"displayName": "message",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"id"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "632c9d0c-5ab3-46a6-bb4d-0d4bd3341fde",
"name": "Summarize & Categorize",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
192,
1328
],
"parameters": {
"text": "=Chat Memory: {{ $('Aggregate').item.json.message.join('\\n') }}\n\nUser Request: {{ $('Normalize input').item.json.message }}",
"batching": {},
"messages": {
"messageValues": [
{
"message": "=You are a system that analyzes a user request and its chat history.\n\n## Your goals:\n1. Summarize the chat history into only the relevant context for the current user request.\n2. Determine the difficulty of the request:\n - 1: Very simple (short answers, reminders, basic actions).\n - 2: Medium (some reasoning, structured outputs, combining info).\n - 3: Complex (multi-step reasoning, ambiguous queries, coding-level reasoning).\n\n## Output Format:\nYou must return JSON strictly following this schema:\n\n{\n \"difficulty\": 1 | 2 | 3,\n \"context\": \"string - summary of the relevant history\"\n}\n"
}
]
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 1.7
},
{
"id": "8979f56c-2809-426d-acda-9d455df8836e",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1984,
576
],
"parameters": {
"color": 7,
"width": 512,
"height": 256,
"content": "## \u2699\ufe0f Database Initialization (Chat Memory Table)\n\n**Purpose:** \nThis section is responsible for creating and preparing the `chat_memory` table in PostgreSQL. It ensures that chat interactions are stored persistently for later use in context management, summarization, and categorization.\n"
},
"typeVersion": 1
},
{
"id": "cf047f39-85cd-41a6-8d2a-76ecec83b6f2",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1392,
432
],
"parameters": {
"color": 5,
"width": 704,
"height": 640,
"content": "## \ud83d\udd35 Input Handling (Telegram Trigger & Preprocessing)\n\n### Purpose:\nThis section receives and processes incoming messages from Telegram. It detects whether the input is text, voice, or unsupported media.\n\n### Key Steps:\n\n* **Telegram Trigger** \u2013 Listens for new updates (messages from users).\n* **Input Message Router** \u2013 Classifies whether the input is:\n * Text message\n * Voice message\n * Unsupported media \u2192 redirects to an error handler\n* **Voice Handling** \u2013 If it\u2019s a voice message:\n * Downloads the file\n * Normalizes MIME type\n * Sends it to speech-to-text analysis\n * Extracts the text message from audio\n* **Error Handling** \u2013 If the input is not supported, an error message is returned.\n\n### Optional Extension:\nFor multimodal and media group support (multiple files, images, videos), you can extend this section with the following template: https://n8n.io/workflows/7455-process-multiple-media-files-in-telegram-with-gemini-ai-and-postgresql-database/"
},
"typeVersion": 1
},
{
"id": "3557d1c3-c155-430a-a11b-5bc901111d46",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-352,
496
],
"parameters": {
"color": 3,
"width": 784,
"height": 720,
"content": "## \ud83d\udd34 Chat Memory Retrieval & Context Optimization\n\n### Purpose:\nThis section retrieves past interactions from the database, aggregates them into a single string, and summarizes them before passing them to the agent.\n\n### Key Steps:\n\n* **Normalize Input** \u2013 Standardizes the message before processing.\n* **Get Chat Memory** \u2013 Queries PostgreSQL for previous messages linked to the `session_id`.\n* **Aggregate** \u2013 Combines past interactions into one text block.\n* **Summarize & Categorize** \u2013 Uses Google Gemini 2.5 Flash Lite (low-cost, low-latency) to:\n * Summarize the chat history.\n * Extract relevant context.\n * Categorize the difficulty level of the task.\n\n### Why Summarization First?\n\n* Only relevant and important context is passed to the agent.\n* Reduces token usage, speeding up responses and lowering costs.\n* Prevents irrelevant history from cluttering the model\u2019s attention.\n\n### Advantages:\n\n* Saves processing time and cost by using a lightweight summarization model.\n* Produces more accurate and focused responses.\n* Optimizes memory queries by reducing data size passed downstream."
},
"typeVersion": 1
},
{
"id": "42fdc20f-2c28-4826-8704-20ffb8792bd7",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
704,
384
],
"parameters": {
"color": 6,
"width": 784,
"height": 832,
"content": "## \ud83d\udfe3 Agent Processing & Response Delivery\n\n### Purpose:\nThis section routes the request to the appropriate Gemini model depending on task difficulty, generates the final response, and sends it back to Telegram. It also updates the chat memory.\n\n### Key Steps:\n\n* **Agent Node** \u2013 Receives:\n * User request\n * Relevant summarized context\n * Difficulty level\n* **Model Selector** \u2013 Dynamically chooses the model:\n * **Difficulty 1** \u2192 Gemini 2.5 Flash Lite (fastest & cheapest)\n * **Difficulty 2** \u2192 Gemini 2.5 Flash\n * **Difficulty 3** \u2192 Gemini 2.5 Pro (advanced reasoning)\n* **Markdown Formatting** \u2013 Converts model output into Telegram-compatible Markdown V2.\n* **Send Message** \u2013 Sends the response back to Telegram.\n* **Update Chat Memory** \u2013 Inserts a single row containing both the user and agent message.\n\n### Why single-row storage for user & agent?\n\n* Since the message is sent to Telegram first and then the memory is updated, this saves an average of 0.3 seconds in response time.\n* Reduces the number of queries (`Get Chat Memory` fetches fewer rows).\n* Updates are faster since both messages are stored at once.\n* Optimizes resource usage while maintaining full conversational context.\n\n### Advantages:\n\n* Cost-optimized model usage (only advanced models used when strictly necessary).\n* Faster response times by minimizing context size and database operations.\n"
},
"typeVersion": 1
},
{
"id": "1c29f5cf-c95e-40fc-9dc0-362dcda71574",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2560,
1104
],
"parameters": {
"color": 4,
"width": 544,
"height": 592,
"content": "## \u2705 Key Benefits of This Architecture\n\n### Cost Efficiency\n\n* Uses cheaper LLMs for summarization and simple queries.\n* Reserves expensive models only for complex tasks.\n\n### Performance Optimization\n\n* Reduced token consumption through summarization.\n* Faster memory queries due to single-row storage.\n\n### Flexibility & Scalability\n\n* Easy integration of multimodal inputs (images, audio, video).\n* Modular structure allows replacing/upgrading models or database logic.\n\n### User Experience\n\n* Quick Telegram responses.\n* Consistent memory of past interactions without overwhelming the LLM."
},
"typeVersion": 1
},
{
"id": "84019987-a071-44d0-8e4b-d80c838007dc",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2496,
848
],
"parameters": {
"color": 2,
"width": 368,
"height": 192,
"content": "## Acknowledgment\n\nA special thank you to Davide for the inspiration behind this template. \nHis work on the [**AI Orchestrator that dynamically selects models based on input type**](https://n8n.io/workflows/7004-ai-orchestrator-dynamically-selects-models-based-on-input-type/) served as a foundational guide for this architecture.\n"
},
"typeVersion": 1
},
{
"id": "0c425ab2-41d3-43c9-86bb-9c7cbfe968a4",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
1584,
1648
],
"parameters": {
"color": 2,
"width": 368,
"height": 192,
"content": "## \ud83d\udca1 Need Assistance?\n\nIf you\u2019d like help customizing or extending this workflow, feel free to reach out: \n\n\ud83d\udce7 Email: [johnsilva11031@gmail.com](mailto:johnsilva11031@gmail.com) \n\ud83d\udd17 LinkedIn: [John Alejandro Silva Rodr\u00edguez](https://www.linkedin.com/in/john-alejandro-silva-rodriguez-48093526b/)\n"
},
"typeVersion": 1
}
],
"connections": {
"Agent": {
"main": [
[
{
"node": "MarkdownV2",
"type": "main",
"index": 0
},
{
"node": "Update Chat Memory (User and Agent)",
"type": "main",
"index": 0
}
]
]
},
"Fix mime": {
"main": [
[
{
"node": "Analyze voice message",
"type": "main",
"index": 0
}
]
]
},
"Aggregate": {
"main": [
[
{
"node": "Summarize & Categorize",
"type": "main",
"index": 0
}
]
]
},
"MarkdownV2": {
"main": [
[
{
"node": "Send a text message",
"type": "main",
"index": 0
}
]
]
},
"Gemini 2.5 Pro": {
"ai_languageModel": [
[
{
"node": "Model Selector",
"type": "ai_languageModel",
"index": 2
}
]
]
},
"Model Selector": {
"ai_languageModel": [
[
{
"node": "Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Get Chat Memory": {
"main": [
[
{
"node": "Aggregate",
"type": "main",
"index": 0
}
]
]
},
"Normalize input": {
"main": [
[
{
"node": "Get Chat Memory",
"type": "main",
"index": 0
}
]
]
},
"Gemini 2.5 Flash": {
"ai_languageModel": [
[
{
"node": "Model Selector",
"type": "ai_languageModel",
"index": 1
}
]
]
},
"Telegram Trigger": {
"main": [
[
{
"node": "Input Message Router1",
"type": "main",
"index": 0
},
{
"node": "Typing\u2026",
"type": "main",
"index": 0
}
]
]
},
"get_error_message": {
"main": [
[
{
"node": "Normalize input",
"type": "main",
"index": 0
}
]
]
},
"get_message (text)": {
"main": [
[
{
"node": "Normalize input",
"type": "main",
"index": 0
}
]
]
},
"Analyze voice message": {
"main": [
[
{
"node": "get_message (Audio/Video message)",
"type": "main",
"index": 0
}
]
]
},
"Gemini 2.5 Flash Lite": {
"ai_languageModel": [
[
{
"node": "Model Selector",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Input Message Router1": {
"main": [
[
{
"node": "get_message (text)",
"type": "main",
"index": 0
}
],
[
{
"node": "Download Voice Message",
"type": "main",
"index": 0
}
],
[
{
"node": "get_error_message",
"type": "main",
"index": 0
}
]
]
},
"Download Voice Message": {
"main": [
[
{
"node": "Fix mime",
"type": "main",
"index": 0
}
]
]
},
"Summarize & Categorize": {
"main": [
[
{
"node": "Agent",
"type": "main",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "Summarize & Categorize",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Google Gemini 2.5 Flash Lite": {
"ai_languageModel": [
[
{
"node": "Summarize & Categorize",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"get_message (Audio/Video message)": {
"main": [
[
{
"node": "Normalize input",
"type": "main",
"index": 0
}
]
]
},
"Update Chat Memory (User and Agent)": {
"main": [
[]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Create Chat Memory Table",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googlePalmApipostgrestelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
> Optimize your AI workflows, cut costs, and get faster, more accurate answers.
Source: https://n8n.io/workflows/7851/ — 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 transforms your Telegram bot into an intelligent creative assistant. It can chat conversationally, fetch trending image prompts from PromptHero for inspiration, or perform a deep "remix"
This workflow acts as an AI-powered "Viral Architect" for YouTube creators. Simply send a video topic (e.g., "Kling 2.6") to your Telegram bot, and it will scrape top-performing competitor thumbnails,
This workflow acts as an intelligent content engine. Simply send a link to your Telegram bot (e.g., a product page or news article), and it will automatically scrape the content, rewrite it into a hig
This workflow automates Facebook posting and appointment booking directly from a Telegram bot, making it especially useful for pet grooming businesses that want to keep their social media active while
This workflow transforms text-based resume data into visually stunning images by leveraging Google Gemini's reasoning and vision capabilities. It autonomously analyzes the candidate's profile, selects