This workflow corresponds to n8n.io template #14051 — we link there as the canonical source.
This workflow follows the Google Sheets → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "",
"meta": {
"templateCredsSetupCompleted": false
},
"name": "Telegram Expense Tracker \u2014 Query Bot (Template)",
"tags": [],
"nodes": [
{
"id": "58535881-40c5-4d75-82c6-f0faa9443e1a",
"name": "MSG | Telegram Inbound",
"type": "n8n-nodes-base.telegramTrigger",
"position": [
2144,
1088
],
"parameters": {
"updates": [
"message",
"callback_query"
],
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "69f61c14-bd15-4c5a-b0b8-ada463bff839",
"name": "LLM | Parse Intent",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
2880,
1632
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-nano",
"cachedResultName": "GPT-4.1-NANO"
},
"options": {},
"messages": {
"values": [
{
"role": "system",
"content": "=You are a financial intent parser.\nYour ONLY task is to convert a natural language finance question into structured JSON.\nYou must:\n- Return ONLY valid JSON.\n- Do NOT include explanations.\n- Do NOT include markdown.\n- Do NOT answer the question.\n- Do NOT guess missing information.\n- Do NOT add extra fields.\n\nThe user's first name is: {{ $('MSG | Telegram Inbound').item.json.message.from.first_name }}\nThe current year is: {{ new Date().getFullYear() }}\nThe current date is: {{ new Date().toISOString().split('T')[0] }}\n\nAlways return the following JSON structure:\n{\n \"intent\": \"spending_query\",\n \"time_reference\": \"last_week, this_week, last_month, this_month, last_year, this_year, or null\",\n \"explicit_start_date\": \"YYYY-MM-DD or null\",\n \"explicit_end_date\": \"YYYY-MM-DD or null\",\n \"person\": \"string or null\",\n \"category\": [\"string\"] or null,\n \"common_only\": true, false, or null,\n \"comparison\": \"previous_period, previous_month, previous_year, or null\",\n \"group_by\": \"category, person, or null\"\n}\n\nInterpretation Rules:\n1. Date Handling (IMPORTANT):\n- If the user mentions a specific month without a year (e.g. \"february\", \"januar\"),\n assume the current year and set explicit_start_date and explicit_end_date accordingly.\n Example: \"february\" \u2192 explicit_start_date: \"2026-02-01\", explicit_end_date: \"2026-02-28\"\n- If the user refers to relative time (e.g., last week, this month, last month, this year, etc.), \n set the correct value in \"time_reference\".\n- DO NOT calculate actual dates for relative time references.\n- Leave explicit_start_date and explicit_end_date as null for relative time references.\n- If the user provides an explicit date range (e.g., 01.01.2026 - 31.01.2026),\n set explicit_start_date and explicit_end_date accordingly,\n and set time_reference to null.\n- If no time reference is mentioned \u2192 set all three fields to null.\n\n2. Person Handling:\n- If user says \"I\", \"me\", \"ich\", \"mir\" \u2192 use \"{{ $('MSG | Telegram Inbound').item.json.message.from.first_name }}\"\n- If user says \"we\", \"wir\", \"uns\", \"beide\" \u2192 null (refers to both persons, no filter needed).\n- If a specific name or nickname is mentioned that refers to another person \u2192 use that exact name as written.\n- If no person is mentioned \u2192 null.\n- NEVER invent or guess a person name.\n- The value for \"person\" must always be either the mentioned name/nickname, \"{{ $('MSG | Telegram Inbound').item.json.message.from.first_name }}\", or null.\n\n3. category:\n- If one category is mentioned, return it as a single-element array.\n- If multiple categories are mentioned, return all of them as an array.\n- Use lowercase strings.\n- If none mentioned \u2192 null.\n\n4. Common Expense Handling (VERY IMPORTANT):\nSet \"common_only\" to true if the user explicitly refers to shared expenses.\nThis includes words or phrases such as:\n- shared\n- common\n- joint\n- together\n- gemeinschaftlich\n- gemeinsam\n- gemeinsame ausgaben\n- geteilte ausgaben\n- f\u00fcr uns\n- zusammen\nIf the user explicitly says:\n- \"f\u00fcr gemeinsame Ausgaben\"\n- \"f\u00fcr gemeinsame Kosten\"\nyou MUST set \"common_only\" to true.\nSet \"common_only\" to false if the user explicitly refers to private expenses.\nThis includes:\n- private\n- pers\u00f6nlich\n- eigene ausgaben\n- nur f\u00fcr sich\n- privat\nIf shared/private is NOT explicitly mentioned \u2192 set \"common_only\" to null.\nNever assume.\n\n5. Comparison:\n- If user compares with previous month \u2192 \"previous_month\"\n- If compares with previous period \u2192 \"previous_period\"\n- If compares with previous year \u2192 \"previous_year\"\n- Otherwise \u2192 null\n\n6. Grouping:\n- If user asks for breakdown by category \u2192 \"category\"\n- If breakdown by person \u2192 \"person\"\n- Otherwise \u2192 null\n\nReturn ONLY JSON."
},
{
"content": "={{ $json.message.text }}"
}
]
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.4
},
{
"id": "1f1984d4-cc31-4730-8354-69c6ec425bbe",
"name": "JS | Extract Intent JSON",
"type": "n8n-nodes-base.code",
"position": [
3232,
1632
],
"parameters": {
"jsCode": "const rawContent = $input.first().json.message?.content || $input.first().json.choices?.[0]?.message?.content || '{}';\nlet parsed = {};\ntry {\n const cleaned = rawContent.replace(/```json|```/g, '').trim();\n parsed = JSON.parse(cleaned);\n} catch(e) {\n parsed = { intent: 'spending_query', start_date: null, end_date: null, person: null, category: null, common_only: null, comparison: null, group_by: null };\n}\nreturn [{ json: parsed }];"
},
"typeVersion": 2
},
{
"id": "4e830963-ede6-4d56-b328-7f3119040143",
"name": "GS | Load Expenses",
"type": "n8n-nodes-base.googleSheets",
"position": [
9824,
1440
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_SPREADSHEET_ID",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "expenses"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "4271bf57-4b8d-4f68-8896-2d55f162161e",
"name": "GS | Load Categories",
"type": "n8n-nodes-base.googleSheets",
"position": [
9824,
1152
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_SPREADSHEET_ID",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "expense_categories"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "21d72a8f-e9cd-4fac-b832-f0804777d270",
"name": "JS | Filter & Aggregate",
"type": "n8n-nodes-base.code",
"position": [
10576,
1264
],
"parameters": {
"jsCode": "// =============================\n// Date Engine\n// =============================\n\nconst now = new Date();\n\nfunction normalizeDate(date) {\n const d = new Date(date);\n if (isNaN(d)) return null;\n d.setHours(0,0,0,0);\n return d;\n}\n\nfunction getDateRange(timeReference) {\n\n switch (timeReference) {\n\n case \"this_month\":\n return {\n start: normalizeDate(new Date(now.getFullYear(), now.getMonth(), 1)),\n end: normalizeDate(now)\n };\n\n case \"last_month\":\n return {\n start: normalizeDate(new Date(now.getFullYear(), now.getMonth() - 1, 1)),\n end: normalizeDate(new Date(now.getFullYear(), now.getMonth(), 0))\n };\n\n case \"this_year\":\n return {\n start: normalizeDate(new Date(now.getFullYear(), 0, 1)),\n end: normalizeDate(now)\n };\n\n case \"last_year\":\n return {\n start: normalizeDate(new Date(now.getFullYear() - 1, 0, 1)),\n end: normalizeDate(new Date(now.getFullYear() - 1, 11, 31))\n };\n\n case \"last_week\":\n const day = now.getDay();\n const diffToMonday = (day + 6) % 7;\n\n const lastMonday = new Date(now);\n lastMonday.setDate(now.getDate() - diffToMonday - 7);\n\n const lastSunday = new Date(lastMonday);\n lastSunday.setDate(lastMonday.getDate() + 6);\n\n return {\n start: normalizeDate(lastMonday),\n end: normalizeDate(lastSunday)\n };\n\n case \"this_week\":\n const currentDay = now.getDay();\n const diff = (currentDay + 6) % 7;\n\n const monday = new Date(now);\n monday.setDate(now.getDate() - diff);\n\n return {\n start: normalizeDate(monday),\n end: normalizeDate(now)\n };\n\n default:\n return { start: null, end: null };\n }\n}\n\nfunction formatDate(date) {\n if (!date) return null;\n return date.toISOString().split('T')[0];\n}\n\n// =============================\n// Retrieve Intent\n// =============================\n\nconst intent = $('JS | Extract Intent JSON').first().json;\nconst edit3 = $('SET | Assemble Resolved Intent').first().json;\n\n// IMPORTANT: overwrite values if edited\nintent.person = edit3.person || intent.person;\nintent.category = edit3.category || intent.category;\n\n// =============================\n// Load & Normalize Data\n// =============================\n\nconst expenseItems = $('GS | Load Expenses').all().map(i => ({\n date: i.json.date,\n amount: parseFloat(i.json.amount) || 0,\n category: i.json.category?.toLowerCase() || null,\n description: i.json.description,\n common_expense: i.json.common_expense,\n person: i.json.Person?.toLowerCase() || null\n}));\n\n// =============================\n// Date Handling\n// =============================\n\nlet startDate = null;\nlet endDate = null;\n\nif (intent.time_reference) {\n const range = getDateRange(intent.time_reference);\n startDate = range.start;\n endDate = range.end;\n}\n\nif (intent.explicit_start_date) {\n startDate = normalizeDate(intent.explicit_start_date);\n}\n\nif (intent.explicit_end_date) {\n endDate = normalizeDate(intent.explicit_end_date);\n}\n\nconst formattedStart = formatDate(startDate);\nconst formattedEnd = formatDate(endDate);\n\n// =============================\n// Other Filters\n// =============================\n\nconst filterPerson = intent.person ? intent.person.toLowerCase() : null;\n\nconst filterCategories = (() => {\n const c = intent.category;\n if (!c) return [];\n if (Array.isArray(c)) {\n return c.map(x => String(x).trim().toLowerCase()).filter(Boolean);\n }\n return [String(c).trim().toLowerCase()].filter(Boolean);\n})();\n\nconst filterCommon = intent.common_only;\nconst groupBy = intent.group_by;\n\n// =============================\n// Apply Filters\n// =============================\n\nconst filtered = expenseItems.filter(row => {\n\n if (!row.date) return false;\n\n const rowDate = normalizeDate(row.date);\n if (!rowDate) return false;\n\n if (startDate && rowDate < startDate) return false;\n if (endDate && rowDate > endDate) return false;\n\n if (filterPerson && row.person !== filterPerson) return false;\n\n if (filterCategories.length > 0) {\n if (!row.category) return false;\n if (!filterCategories.includes(row.category)) return false;\n }\n\n if (filterCommon !== null && filterCommon !== undefined) {\n\n const isCommon =\n String(row.common_expense).toLowerCase() === 'true' ||\n row.common_expense === true ||\n row.common_expense === 1;\n\n if (filterCommon && !isCommon) return false;\n if (!filterCommon && isCommon) return false;\n }\n\n return true;\n\n});\n\n// =============================\n// Aggregations\n// =============================\n\nconst total = filtered.reduce((sum, row) => sum + row.amount, 0);\n\n// Category breakdown\nlet categoryBreakdown = null;\n\nif (groupBy === \"category\") {\n\n const breakdown = {};\n\n filtered.forEach(row => {\n\n const cat = row.category || \"uncategorized\";\n\n breakdown[cat] = (breakdown[cat] || 0) + row.amount;\n\n });\n\n categoryBreakdown = breakdown;\n}\n\n// Person breakdown\nlet personBreakdown = null;\n\nif (groupBy === \"person\") {\n\n const breakdown = {};\n\n filtered.forEach(row => {\n\n const person = row.person || \"unknown\";\n\n breakdown[person] = (breakdown[person] || 0) + row.amount;\n\n });\n\n personBreakdown = breakdown;\n}\n\n// =============================\n// Return Result\n// =============================\n\nreturn [{\n json: {\n intent,\n total: Math.round(total * 100) / 100,\n count: filtered.length,\n categoryBreakdown,\n personBreakdown,\n period: {\n start: formattedStart,\n end: formattedEnd\n },\n filters: {\n person: intent.person,\n category: intent.category,\n common_only: intent.common_only\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "b24a37e4-d82f-43e6-b343-e91f7f838d19",
"name": "JS | Format Response Message",
"type": "n8n-nodes-base.code",
"position": [
11056,
1264
],
"parameters": {
"jsCode": "const data = $input.first().json;\nconst filters = data.filters || {};\nconst period = data.period || {};\n\nlet lines = [];\n\n// =============================\n// Header\n// =============================\n\nlines.push('\ud83d\udcca *Expense Summary*');\nlines.push('');\n\n// =============================\n// Period Display (NEW)\n// =============================\n\nif (period.start || period.end) {\n const from = period.start || 'Beginning';\n const to = period.end || 'Today';\n lines.push(`\ud83d\udcc5 Period: ${from} \u2192 ${to}`);\n} else {\n lines.push(`\ud83d\udcc5 Period: All time`);\n}\n\n// =============================\n// Applied filters\n// =============================\n\nif (filters.person) lines.push(`\ud83d\udc64 Person: ${filters.person}`);\nif (filters.category) lines.push(`\ud83c\udff7\ufe0f Category: ${filters.category}`);\n\nif (filters.common_only !== null && filters.common_only !== undefined) {\n lines.push(`\ud83d\udd17 Common expenses only: ${filters.common_only ? 'Yes' : 'No'}`);\n}\n\nlines.push('');\nlines.push(`\ud83d\udcb0 *Total: \u20ac${data.total.toFixed(2)}*`);\nlines.push(`\ud83d\udcdd Transactions: ${data.count}`);\n\n// =============================\n// Category breakdown\n// =============================\n\nif (data.categoryBreakdown) {\n lines.push('');\n lines.push('*By Category:*');\n\n const sorted = Object.entries(data.categoryBreakdown)\n .sort((a, b) => b[1] - a[1]);\n\n sorted.forEach(([cat, amt]) => {\n const pct = data.total > 0\n ? ((amt / data.total) * 100).toFixed(1)\n : 0;\n\n lines.push(` \u2022 ${cat}: $${amt.toFixed(2)} (${pct}%)`);\n });\n}\n\n// =============================\n// Person breakdown\n// =============================\n\nif (data.personBreakdown) {\n lines.push('');\n lines.push('*By Person:*');\n\n const sorted = Object.entries(data.personBreakdown)\n .sort((a, b) => b[1] - a[1]);\n\n sorted.forEach(([person, amt]) => {\n const pct = data.total > 0\n ? ((amt / data.total) * 100).toFixed(1)\n : 0;\n\n lines.push(` \u2022 ${person}: $${amt.toFixed(2)} (${pct}%)`);\n });\n}\n\n// =============================\n// Comparison placeholder\n// =============================\n\nif (data.comparison) {\n lines.push('');\n lines.push(`\ud83d\udcc8 Comparison: [Feature coming soon]`);\n}\n\n// =============================\n// Empty state\n// =============================\n\nif (data.count === 0) {\n lines.push('');\n lines.push('_No expenses found for the given filters._');\n}\n\nreturn [{ json: { message: lines.join('\\n') } }];"
},
"typeVersion": 2
},
{
"id": "c7dc9c2c-8bc0-4b0a-a988-27a966b3f814",
"name": "TG | Send Reply",
"type": "n8n-nodes-base.telegram",
"position": [
11312,
1264
],
"parameters": {
"text": "={{ $json.message }}",
"chatId": "={{ $('MSG | Telegram Inbound').first().json.message.chat.id }}",
"additionalFields": {
"parse_mode": "Markdown",
"appendAttribution": false,
"reply_to_message_id": "={{ $('MSG | Telegram Inbound').first().json.message.message_id }}"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "4876799b-af52-4218-80aa-7603258ff084",
"name": "MERGE | Combine Expenses + Categories",
"type": "n8n-nodes-base.merge",
"position": [
10144,
1264
],
"parameters": {
"mode": "chooseBranch"
},
"typeVersion": 3.2
},
{
"id": "db13a254-5fde-42e4-9e82-2060a04451f0",
"name": "IF | User Authorized?",
"type": "n8n-nodes-base.if",
"position": [
2528,
1072
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "or",
"conditions": [
{
"id": "0f85f7d5-8b7e-4bcd-ae10-d7a1785192c4",
"operator": {
"type": "number",
"operation": "equals"
},
"leftValue": "={{ $json.message.chat.id }}",
"rightValue": 1000000000
},
{
"id": "2d18f8cf-2910-4e20-b2f3-16a9ef04e63e",
"operator": {
"type": "number",
"operation": "equals"
},
"leftValue": "={{ $json.message.chat.id }}",
"rightValue": 1000000001
}
]
}
},
"typeVersion": 2.2
},
{
"id": "7a76b7d6-f533-4785-b822-8d7ac464d883",
"name": "SPLIT | Split Categories",
"type": "n8n-nodes-base.splitOut",
"position": [
4016,
768
],
"parameters": {
"options": {},
"fieldToSplitOut": "category"
},
"typeVersion": 1
},
{
"id": "8e963faf-3079-447d-bfd9-89eff8da207e",
"name": "GS | Read Category Mapping",
"type": "n8n-nodes-base.googleSheets",
"position": [
4016,
1072
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_SPREADSHEET_ID",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "categories_mapping"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "e992cd5e-ba4e-4ab5-b885-14c55546d0a3",
"name": "MERGE | Join Categories with Mapping",
"type": "n8n-nodes-base.merge",
"position": [
4272,
768
],
"parameters": {
"mode": "combineBySql",
"query": "SELECT * FROM input1 LEFT JOIN input2 ON input1.category = input2.find",
"options": {}
},
"typeVersion": 3.2
},
{
"id": "58c889ef-e918-43d4-a7a3-1a47bf890ffc",
"name": "TG | Confirm Category Suggestion",
"type": "n8n-nodes-base.telegram",
"position": [
6224,
368
],
"parameters": {
"chatId": "={{ $('MSG | Telegram Inbound').item.json.message.chat.id }}",
"message": "=I couldn\u2019t find the category \"{{ $('IF | Category Known?').item.json.category_new }}\".\n\nDid you mean \"{{ $json.content }}\"?\n\n\u2705 Yes \n\u274c No",
"options": {
"appendAttribution": false
},
"operation": "sendAndWait",
"approvalOptions": {
"values": {
"approvalType": "double",
"approveLabel": "\u2705 Yes",
"disapproveLabel": "\u274c No"
}
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "fd9c3d1c-223a-44d8-afbf-4773a11d09bd",
"name": "GS | Read Allowed Categories",
"type": "n8n-nodes-base.googleSheets",
"position": [
4464,
1072
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_SPREADSHEET_ID",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "expense_categories"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "2f1f1de8-cae3-4792-920c-ba490986019e",
"name": "SET | Normalize Category",
"type": "n8n-nodes-base.set",
"position": [
4464,
768
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "8f458013-1006-4634-a1c8-aedff820a48f",
"name": "category_new",
"type": "string",
"value": "={{ $json.replace ?? $json.category }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "7a819214-50e5-460a-bd4d-a546eb68dc20",
"name": "MERGE | Check Category against Allowed",
"type": "n8n-nodes-base.merge",
"position": [
4720,
768
],
"parameters": {
"mode": "combineBySql",
"query": "SELECT * FROM input1\n \nLEFT JOIN input2 ON input1.category_new = input2.category",
"options": {}
},
"typeVersion": 3.2,
"alwaysOutputData": false
},
{
"id": "ed0de8c5-8c55-49f2-96a0-973b49b95d6a",
"name": "IF | Category Known?",
"type": "n8n-nodes-base.if",
"position": [
4960,
768
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "e329be08-8c46-4898-9480-5946735ad54d",
"operator": {
"type": "string",
"operation": "notExists",
"singleValue": true
},
"leftValue": "={{ $json.description }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3,
"alwaysOutputData": false
},
{
"id": "9ecd305a-5e42-43f1-91fa-21fbb0d89e53",
"name": "LLM | Classify Category",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
5712,
368
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-nano",
"cachedResultName": "GPT-4.1-NANO"
},
"options": {},
"messages": {
"values": [
{
"role": "system",
"content": "=You are a strict category classification system.\n\nYour task:\nMap the given input category to exactly ONE category from the allowed category list.\n\nRules:\n- You MUST select one category from the allowed list.\n- Do NOT invent new categories.\n- Do NOT return explanations.\n- Do NOT return multiple categories.\n- Output ONLY the exact category name from the allowed list.\n- Matching must be case-insensitive.\n- Choose the closest semantic match if there is no exact match.\n\nInput category:\n{{ $('IF | Category Known?').item.json.category_new }}\n\nAllowed categories:\n{{ $json.category }}"
}
]
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.4
},
{
"id": "73085ca0-da81-4f8f-a232-5680c7d88b88",
"name": "MERGE | Combine Categories with List",
"type": "n8n-nodes-base.merge",
"position": [
5216,
672
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineAll"
},
"typeVersion": 3.2
},
{
"id": "19fe45dc-435b-4e70-9b7a-cd7c0bedd41e",
"name": "SET | Extract LLM Category",
"type": "n8n-nodes-base.set",
"position": [
5968,
368
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "={{ $json.message }}"
},
"typeVersion": 3.4
},
{
"id": "20b22d0e-35bf-476f-9aba-e2abb14f0377",
"name": "IF | Category Suggestion Accepted?",
"type": "n8n-nodes-base.if",
"position": [
6464,
368
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4fb343c7-9eb2-4491-b017-3637fb543564",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.data.approved }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "1d692432-6601-47f1-9bdd-cafd50aeef5c",
"name": "GS | Save Category Mapping",
"type": "n8n-nodes-base.googleSheets",
"position": [
7968,
368
],
"parameters": {
"columns": {
"value": {
"find": "={{ $json.old }}",
"replace": "={{ $json.new }}"
},
"schema": [
{
"id": "find",
"type": "string",
"display": true,
"required": false,
"displayName": "find",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "replace",
"type": "string",
"display": true,
"required": false,
"displayName": "replace",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_SPREADSHEET_ID",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "categories_mapping"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "750a9069-a57b-4b46-8725-42367602aad2",
"name": "MERGE | Loop Entry (Category)",
"type": "n8n-nodes-base.merge",
"position": [
5712,
768
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "20498665-76a1-45b8-978f-e92e0fc19ed1",
"name": "SET | Set Resolved Category",
"type": "n8n-nodes-base.set",
"position": [
5968,
768
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "47a00b45-f580-4568-9913-b2359e1c1d8c",
"name": "category_new",
"type": "string",
"value": "={{ $json.replace ?? $('IF | Category Known?').item.json.category_new}}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "e6824c4c-a839-40d0-b334-40f19aad19d0",
"name": "SET | Assemble Resolved Intent",
"type": "n8n-nodes-base.set",
"position": [
9472,
1264
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "dc8b496a-4e1a-4ba3-9180-61b77a616b99",
"name": "intent",
"type": "string",
"value": "={{ $('JS | Extract Intent JSON').item.json.intent }}"
},
{
"id": "1e76280f-f49b-4225-ba90-fccc0ba3d9f2",
"name": "time_reference",
"type": "string",
"value": "={{ $('JS | Extract Intent JSON').item.json.time_reference }}"
},
{
"id": "1b10b1a5-e35d-4e13-8a28-945271eb4cd6",
"name": "explicit_start_date",
"type": "string",
"value": "={{ $('JS | Extract Intent JSON').item.json.explicit_start_date }}"
},
{
"id": "51c694b2-c10b-485d-8e1e-dedd797308a9",
"name": "explicit_end_date",
"type": "string",
"value": "={{ $('JS | Extract Intent JSON').item.json.explicit_end_date }}"
},
{
"id": "56a2b084-547f-412f-b534-4c2a97ec52c8",
"name": "person",
"type": "string",
"value": "={{ $json.person_new }}"
},
{
"id": "aa1b02a7-6908-4ba0-b8c2-a74b15516b40",
"name": "category",
"type": "array",
"value": "={{ $json.category_new }}"
},
{
"id": "4bd911ba-a8e3-423c-aa2e-eab59e16a33a",
"name": "common_only",
"type": "string",
"value": "={{ $('JS | Extract Intent JSON').item.json.common_only }}"
},
{
"id": "720e91ea-fc42-48b4-af78-1853736d8720",
"name": "comparison",
"type": "string",
"value": "={{ $('JS | Extract Intent JSON').item.json.comparison }}"
},
{
"id": "27656405-d197-4f6e-b1d8-f90a72351f20",
"name": "group_by",
"type": "string",
"value": "={{ $('JS | Extract Intent JSON').item.json.group_by }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "cc31446d-2ffb-4411-afd7-64e69182b448",
"name": "AGG | Aggregate Category List",
"type": "n8n-nodes-base.aggregate",
"position": [
4720,
1072
],
"parameters": {
"options": {},
"fieldsToAggregate": {
"fieldToAggregate": [
{
"fieldToAggregate": "category"
}
]
}
},
"typeVersion": 1
},
{
"id": "6f2ace60-279f-40ce-8459-53a13a603748",
"name": "LOOP | Iterate Categories",
"type": "n8n-nodes-base.splitInBatches",
"position": [
5472,
768
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "165edc36-f6bd-4231-a1e4-bd7c7cd4ecea",
"name": "SET | Set New+Old Category (LLM)",
"type": "n8n-nodes-base.set",
"position": [
7216,
368
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "5f52894c-c285-46f0-822d-44665a58555f",
"name": "new",
"type": "string",
"value": "={{ $('SET | Extract LLM Category').item.json.content }}"
},
{
"id": "e02f6b7f-c27d-4620-84e4-346dcb5f26f0",
"name": "old",
"type": "string",
"value": "={{ $('LOOP | Iterate Categories').item.json.category_new }}"
}
]
}
},
"typeVersion": 3.4,
"alwaysOutputData": true
},
{
"id": "8294d338-81df-4799-8d83-335699eb38d9",
"name": "JS | Build Category Inline Buttons",
"type": "n8n-nodes-base.code",
"position": [
6720,
176
],
"parameters": {
"jsCode": "const items = $('AGG | Aggregate Category List').first().json.category;\nif (!Array.isArray(items)) {\n throw new Error('Expected category to be an array');\n}\n\nconst buttons = items.map(item => ([{\n text: String(item),\n callback_data: String(item)\n}]));\n\nreturn [{\n json: {\n inline_keyboard: buttons,\n resumeUrl: $execution.resumeUrl\n }\n}];"
},
"typeVersion": 2
},
{
"id": "e7a92e17-a097-4e67-8ba0-aa2073aeefb1",
"name": "WAIT | Wait for Category Selection",
"type": "n8n-nodes-base.wait",
"position": [
7216,
176
],
"parameters": {
"resume": "webhook",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 1.1
},
{
"id": "d5c9afc3-2245-4a93-9c66-38edc4c0f4a2",
"name": "HTTP | Send Category Selection Message",
"type": "n8n-nodes-base.httpRequest",
"position": [
6960,
176
],
"parameters": {
"url": "https://api.telegram.org/bot{{YOUR_BOT_TOKEN}}/sendMessage",
"method": "POST",
"options": {},
"jsonBody": "={\n \"chat_id\": \"{{ $('MSG | Telegram Inbound').item.json.message.chat.id }}\",\n \"text\": \"Bitte w\u00e4hle eine Kategorie:<a href='{{ $json.resumeUrl }}'>\u200b</a>\",\n \"parse_mode\": \"HTML\",\n \"reply_markup\": {{ JSON.stringify({ \"inline_keyboard\": $json.inline_keyboard }) }}\n}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.3
},
{
"id": "33dba25d-9b17-4da1-8463-92dbad2c0ff3",
"name": "SET | Read Callback Body (Cat.)",
"type": "n8n-nodes-base.set",
"position": [
7472,
176
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "={{ $json.body}}"
},
"typeVersion": 3.4
},
{
"id": "e11d27e9-66ea-4cde-a108-3c38b5fea75b",
"name": "SET | Set New+Old Category (Selection)",
"type": "n8n-nodes-base.set",
"position": [
7472,
368
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e34ab1c9-3985-432b-9637-6a695fbf5931",
"name": "new",
"type": "string",
"value": "={{ $json.category }}"
},
{
"id": "b3f6ce98-c6bc-4fea-a581-d1abcd087df4",
"name": "old",
"type": "string",
"value": "={{ $('LOOP | Iterate Categories').item.json.category_new }}"
}
]
}
},
"typeVersion": 3.4,
"alwaysOutputData": true
},
{
"id": "e765fbf0-81a6-4117-9fbf-8c5c4d0f6c90",
"name": "MERGE | Combine Category Mapping Entries",
"type": "n8n-nodes-base.merge",
"position": [
7712,
368
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "dada58e6-2abc-4cec-84c2-4bd92994e796",
"name": "HTTP | Forward Category Selection",
"type": "n8n-nodes-base.httpRequest",
"position": [
3312,
496
],
"parameters": {
"url": "={{ $json.resumeUrl }}",
"method": "POST",
"options": {},
"jsonBody": "={\n \"category\": \"{{ $json.category }}\"\n}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.3
},
{
"id": "76197f7e-8f0f-46d5-84b4-c9c6bb735668",
"name": "TG | Confirm Category Selection",
"type": "n8n-nodes-base.telegram",
"position": [
3504,
496
],
"parameters": {
"text": "=Du hast \" {{ $('MSG | Telegram Inbound').item.json.callback_query.data }}\" ausgew\u00e4hlt\"",
"chatId": "={{ $('MSG | Telegram Inbound').item.json.callback_query.message.chat.id }}",
"additionalFields": {
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "883bf86d-3fac-461a-a9b7-dcc0018eb20f",
"name": "JS | Read Category Callback",
"type": "n8n-nodes-base.code",
"position": [
3104,
496
],
"parameters": {
"jsCode": "const callbackData = $json.callback_query.data;\nconst text = $json.callback_query.message.text;\nconst entities = $json.callback_query.message.entities;\n\n// URL aus den message entities auslesen\nconst urlEntity = entities.find(e => e.type === 'text_link');\nconst resumeUrl = urlEntity.url;\n\nreturn [{\n json: {\n category: callbackData,\n resumeUrl: resumeUrl\n }\n}];"
},
"typeVersion": 2
},
{
"id": "0e0c0fc1-c577-4e31-a62e-2bef9ef22815",
"name": "IF | Message or Callback?",
"type": "n8n-nodes-base.if",
"position": [
2352,
1088
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "69655a65-f86e-4666-8f8f-d9cc2fda74b2",
"operator": {
"type": "object",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.message }}",
"rightValue": "="
}
]
}
},
"typeVersion": 2.3
},
{
"id": "4233a9ba-e3a9-49c7-8f07-1d36fa3fec81",
"name": "IF | Category Present?",
"type": "n8n-nodes-base.if",
"position": [
3824,
976
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "b875ecb6-7b43-4083-8591-bec82fb6a22b",
"operator": {
"type": "string",
"operation": "notExists",
"singleValue": true
},
"leftValue": "={{ $json.category[0] }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "449ced93-3de6-4bf4-8e71-cb0acb48a43e",
"name": "MERGE | Combine Category + Person",
"type": "n8n-nodes-base.merge",
"position": [
8832,
1248
],
"parameters": {
"numberInputs": 3
},
"typeVersion": 3.2
},
{
"id": "f98b58b7-483e-495b-95c1-aa5a9fa457c4",
"name": "AGG | Aggregate Resolved Categories",
"type": "n8n-nodes-base.aggregate",
"position": [
6224,
768
],
"parameters": {
"options": {},
"fieldsToAggregate": {
"fieldToAggregate": [
{
"fieldToAggregate": "category_new"
}
]
}
},
"typeVersion": 1
},
{
"id": "50f4587d-754a-4bb3-888b-b6e084ee6237",
"name": "IF | Person Present?",
"type": "n8n-nodes-base.if",
"position": [
3760,
2096
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "b875ecb6-7b43-4083-8591-bec82fb6a22b",
"operator": {
"type": "string",
"operation": "notExists",
"singleValue": true
},
"leftValue": "={{ $json.person }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "85f203b8-bdf7-4346-aceb-30a8388dc4cf",
"name": "SPLIT | Split Persons",
"type": "n8n-nodes-base.splitOut",
"position": [
3968,
2064
],
"parameters": {
"options": {},
"fieldToSplitOut": "person"
},
"typeVersion": 1
},
{
"id": "7657a339-c559-4327-bc25-5fda098783f3",
"name": "MERGE | Join Persons with Mapping",
"type": "n8n-nodes-base.merge",
"position": [
4224,
2064
],
"parameters": {
"mode": "combineBySql",
"query": "SELECT * FROM input1 LEFT JOIN input2 ON input1.category = input2.find",
"options": {}
},
"typeVersion": 3.2
},
{
"id": "286c2311-d948-4dd0-92bb-81231cb895ea",
"name": "SET | Normalize Person",
"type": "n8n-nodes-base.set",
"position": [
4416,
2064
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "8f458013-1006-4634-a1c8-aedff820a48f",
"name": "person_new",
"type": "string",
"value": "={{ $json.replace ?? $json.person }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "fa32c88e-fb58-483a-8159-bc68edbeebcd",
"name": "MERGE | Check Person against Allowed",
"type": "n8n-nodes-base.merge",
"position": [
4704,
2048
],
"parameters": {
"mode": "combineBySql",
"query": "SELECT * FROM input1\n \nLEFT JOIN input2 ON input1.person_new = input2.person",
"options": {}
},
"typeVersion": 3.2,
"alwaysOutputData": false
},
{
"id": "8ed005b0-aa2a-4052-868a-424d6df8d83e",
"name": "IF | Person Known?",
"type": "n8n-nodes-base.if",
"position": [
4944,
2048
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "e329be08-8c46-4898-9480-5946735ad54d",
"operator": {
"type": "string",
"operation": "notExists",
"singleValue": true
},
"leftValue": "={{ $json.description }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3,
"alwaysOutputData": false
},
{
"id": "2f3cf76a-d852-4479-a91a-4d93bdcb8e28",
"name": "MERGE | Combine Persons with List",
"type": "n8n-nodes-base.merge",
"position": [
5200,
2048
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineAll"
},
"typeVersion": 3.2
},
{
"id": "9db7a679-849e-46a4-8c12-9c9296e00d4c",
"name": "AGG | Aggregate Person List",
"type": "n8n-nodes-base.aggregate",
"position": [
4912,
2352
],
"parameters": {
"options": {},
"fieldsToAggregate": {
"fieldToAggregate": [
{
"fieldToAggregate": "person"
}
]
}
},
"typeVersion": 1
},
{
"id": "82d59267-2594-498f-863b-f35116802032",
"name": "GS | Read Person Mapping",
"type": "n8n-nodes-base.googleSheets",
"position": [
3968,
2368
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_SPREADSHEET_ID",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "person_mapping"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "16747ce4-9d18-438d-a8df-df00508908c1",
"name": "GS | Read Allowed Persons",
"type": "n8n-nodes-base.googleSheets",
"position": [
4416,
2368
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1157427829,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "list_persons"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_SPREADSHEET_ID",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "person_mapping"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "fa8b6b35-8747-40b8-9639-5131f279c0cb",
"name": "TG | Confirm Person Suggestion",
"type": "n8n-nodes-base.telegram",
"position": [
6208,
2448
],
"parameters": {
"chatId": "={{ $('MSG | Telegram Inbound').item.json.message.chat.id }}",
"message": "=I couldn\u2019t find the person \"{{ $('IF | Person Known?').item.json.person_new }}\".\n\nDid you mean \"{{ $json.content }}\"?\n\n\u2705 Yes \n\u274c No",
"options": {
"appendAttribution": false
},
"operation": "sendAndWait",
"approvalOptions": {
"values": {
"approvalType": "double",
"approveLabel": "\u2705 Yes",
"disapproveLabel": "\u274c No"
}
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "fa9fc8ee-c15e-4f4f-9af2-8a5dd626d788",
"name": "LLM | Classify Person",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
5696,
2448
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-nano",
"cachedResultName": "GPT-4.1-NANO"
},
"options": {},
"messages": {
"values": [
{
"role": "system",
"content": "=You are a strict person classification system.\n\nYour task:\nMap the given input name to exactly ONE person from the allowed person list.\n\nRules:\n\n* You MUST select one person from the allowed list.\n* Do NOT invent new persons.\n* Do NOT return explanations.\n* Do NOT return multiple persons.\n* Output ONLY the exact person name from the allowed list.\n* Matching must be case-insensitive.\n* Choose the closest semantic match if there is no exact match.\n\nInput name:\n{{ $('IF | Person Known?').item.json.person_new }}\n\nAllowed persons:\n{{ $json.person }}\n"
}
]
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.4
},
{
"id": "ef710337-260f-495c-97aa-7ccb5420b7dc",
"name": "SET | Extract LLM Person",
"type": "n8n-nodes-base.set",
"position": [
5952,
2448
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "={{ $json.message }}"
},
"typeVersion": 3.4
},
{
"id": "fd5713f9-9501-420f-9d12-bb8b84fda620",
"name": "IF | Person Suggestion Accepted?",
"type": "n8n-nodes-base.if",
"position": [
6448,
2448
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4fb343c7-9eb2-4491-b017-3637fb543564",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.data.approved }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "262227e8-5564-4caf-a833-edc83bd36894",
"name": "GS | Save Person Mapping",
"type": "n8n-nodes-base.googleSheets",
"position": [
8176,
1904
],
"parameters": {
"columns": {
"value": {
"find": "={{ $json.old }}",
"replace": "={{ $json.new }}"
},
"schema": [
{
"id": "find",
"type": "string",
"display": true,
"required": false,
"displayName": "find",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "replace",
"type": "string",
"display": true,
"required": false,
"displayName": "replace",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_SPREADSHEET_ID",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID",
"cachedResultName": "person_mapping"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "e6850bcd-ec85-4a14-ae06-73878206a278",
"name": "LOOP | Iterate Persons",
"type": "n8n-nodes-base.splitInBatches",
"position": [
5456,
2048
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "741e158c-14ed-4ff1-aba3-27b0f648ab20",
"name": "SET | Set New+Old Person (LLM)",
"type": "n8n-nodes-base.set",
"position": [
7664,
2304
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "5f52894c-c285-46f0-822d-44665a58555f",
"name": "new",
"type": "string",
"value": "={{ $('SET | Extract LLM Person').item.json.content }}"
},
{
"id": "e02f6b7f-c27d-4620-84e4-346dcb5f26f0",
"name": "old",
"type": "string",
"value": "={{ $('LOOP | Iterate Persons').item.json.person_new }}"
}
]
}
},
"typeVersion": 3.4,
"alwaysOutputData": true
},
{
"id": "41280f1e-9534-4947-908a-41a7caef3397",
"name": "JS | Build Person Inline Buttons",
"type": "n8n-nodes-base.code",
"position": [
6672,
1712
],
"parameters": {
"jsCode": "const items = $('AGG | Aggregate Person List').first().json.person;\n\nif (!Array.isArray(items)) {\n throw new Error('Expected person to be an array');\n}\n\nconst buttons = items.map(item => ([{\n text: String(item),\n callback_data: String(item)\n}]));\n\nreturn [{\n json: {\n inline_keyboard: buttons,\n resumeUrl: $execution.resumeUrl\n }\n}];"
},
"typeVersion": 2
},
{
"id": "5f9c0563-ee99-4147-9081-f499af2bb507",
"name": "WAIT | Wait for Person Selection",
"type": "n8n-nodes-base.wait",
"position": [
7168,
1712
],
"parameters": {
"resume": "webhook",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 1.1
},
{
"id": "5b86154e-57b0-4b2d-a7d7-1e11cc878b67",
"name": "HTTP | Send Person Selection Message",
"type": "n8n-nodes-base.httpRequest",
"position": [
6912,
1712
],
"parameters": {
"url": "https://api.telegram.org/bot{{YOUR_BOT_TOKEN}}/sendMessage",
"method": "POST",
"options": {},
"jsonBody": "={\n \"chat_id\": \"{{ $('MSG | Telegram Inbound').item.json.message.chat.id }}\",\n \"text\": \"Bitte w\u00e4hle eine Person:<a href='{{ $json.resumeUrl }}'>\u200b</a>\",\n \"parse_mode\": \"HTML\",\n \"reply_markup\": {{ JSON.stringify({ \"inline_keyboard\": $json.inline_keyboard }) }}\n}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.3
},
{
"id": "b879f1c5-ffb3-45b6-b7f5-1b772b431643",
"name": "SET | Read Callback Body (Person)",
"type": "n8n-nodes-base.set",
"position": [
7424,
1712
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "={{ $json.body}}"
},
"typeVersion": 3.4
},
{
"id": "1a9aa8de-b20f-4cac-9fde-9d889caee19e",
"name": "SET | Set New+Old Person (Selection)",
"type": "n8n-nodes-base.set",
"position": [
7664,
1712
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e34ab1c9-3985-432b-9637-6a695fbf5931",
"name": "new",
"type": "string",
"value": "={{ $json.person }}"
},
{
"id": "b3f6ce98-c6bc-4fea-a581-d1abcd087df4",
"name": "old",
"type": "string",
"value": "={{ $('LOOP | Iterate Persons').item.json.person_new }}"
}
]
}
},
"typeVersion": 3.4,
"alwaysOutputData": true
},
{
"id": "205a6d3a-9108-4b8b-9698-5ba8db844f88",
"name": "MERGE | Combine Person Mapping Entries",
"type": "n8n-nodes-base.merge",
"position": [
7920,
1904
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "eaee6930-3da6-40a9-ba0f-84ab8543b451",
"name": "MERGE | Loop Entry (Person)",
"type": "n8n-nodes-base.merge",
"position": [
5696,
2256
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "e2241bb1-eae6-4311-909c-a67bb2bd52fe",
"name": "SET | Set Resolved Person",
"type": "n8n-nodes-base.set",
"position": [
6208,
2256
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "47a00b45-f580-4568-9913-b2359e1c1d8c",
"name": "person_new",
"type": "string",
"value": "={{ $json.replace ?? $('IF | Person Known?').item.json.person_new}}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "9a37179d-2d3c-48e3-9ce1-1624b626b6fd",
"name": "MERGE | Combine Person Mapping",
"type": "n8n-nodes-base.merge",
"position": [
6448,
2048
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "183d2e80-d18b-403e-a3f0-86d41fd0c0b1",
"name": "IF | Category or Person Callback?",
"type": "n8n-nodes-base.if",
"position": [
2816,
576
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "a6650d78-63a0-4bc4-ac46-e076eba14def",
"operator": {
"type": "string",
"operation": "contains"
},
"leftValue": "{{json.message.text}}",
"rightValue": "Kategorie"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "0ac9e7b3-a794-4a42-b48b-1d42ba317bbc",
"name": "HTTP | Forward Person Selection",
"type": "n8n-nodes-base.httpRequest",
"position": [
3312,
688
],
"parameters": {
"url": "={{ $json.resumeUrl }}",
"method": "POST",
"options": {},
"jsonBody": "={\n \"person\": \"{{ $json.person }}\"\n}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.3
},
{
"id": "36db9da8-b32c-4b48-b7d4-6871eed49cb0",
"name": "TG | Confirm Person Selection",
"type": "n8n-nodes-base.telegram",
"position": [
3504,
688
],
"parameters"
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.
googleSheetsOAuth2ApiopenAiApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
How this works
This workflow lets you ask natural-language questions about your spending and receive clear answers straight in Telegram. It pulls live data from Google Sheets, uses GPT-4.1 to interpret the query and decide which rows to analyse, then returns a concise summary or breakdown. The core step is the intent-parsing call to OpenAI that turns your message into structured instructions for the subsequent filtering and aggregation.
Use it for quick personal or small-team checks on ad-hoc queries rather than scheduled reporting. Skip it if you need audited totals or complex multi-sheet joins, as those are better handled in dedicated dashboards. A common variation replaces the Sheets source with another ledger while keeping the same Telegram front end.
About this workflow
Ask questions like “How much did I spend on food last month?” and get instant answers from your financial data — directly in Telegram.
Source: https://n8n.io/workflows/14051/ — 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.
> ⚠️ Disclaimer: This workflow uses Community Nodes and must be run on a self-hosted instance of n8n.
Viral Tik Tok Clone Finder. Uses httpRequest, telegramTrigger, openAi, googleSheets. Event-driven trigger; 41 nodes.
This workflow is designed for content creators, agencies, influencers, and automation builders who want to transform viral videos into personalized avatar-based edits — and automatically publish them
Send a target niche and location via Telegram message Workflow discovers businesses via Google Maps API AI enriches contacts with email and LinkedIn data via Serper GPT-4o scores and qualifies each le
💥 Automate YouTube thumbnail creation from video links -vide. Uses telegramTrigger, httpRequest, googleDrive, gmail. Event-driven trigger; 25 nodes.