This workflow corresponds to n8n.io template #12006 — we link there as the canonical source.
This workflow follows the Googlegemini → Google Sheets 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": "P7Tc134xq0XZFjz7",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "AI Multimodal Expense Tracker_Final_v2.2",
"tags": [],
"nodes": [
{
"id": "789a3e35-547d-47ff-9fce-4287f1a51ee0",
"name": "Google Sheets: Get Rows (Dedup lookup)",
"type": "n8n-nodes-base.googleSheets",
"maxTries": 2,
"position": [
560,
-64
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $json.update_id }}",
"lookupColumn": "Update_ID"
}
]
},
"sheetName": {
"__rl": true,
"mode": "id",
"value": "={{ $('CONFIG - User Settings').item.json.sheet_gid_log }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('CONFIG - User Settings').item.json.spreadsheet_id }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"executeOnce": false,
"retryOnFail": false,
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "81ffc0d2-926c-4ac4-ba23-04c81652b99e",
"name": "IF (Is Duplicate?)",
"type": "n8n-nodes-base.if",
"position": [
736,
-64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "2707e1b7-5a59-47e0-9bf5-a24868021e3f",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.Message_ID }}",
"rightValue": "="
}
]
},
"looseTypeValidation": true
},
"typeVersion": 2.3
},
{
"id": "e0461045-0fc1-4a8f-a8f2-81189e44917f",
"name": "Switch (Voice/Photo/Text)",
"type": "n8n-nodes-base.switch",
"position": [
1104,
-288
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Voice",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "8505ca3e-24b9-4ce6-b803-3f844f5e07f5",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{$node[\"Telegram Trigger\"].json.message.voice}}",
"rightValue": "true"
}
]
},
"renameOutput": true
},
{
"outputKey": "Photo",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "dfefc516-d1e3-4932-a4b4-d2f3dd2c84ae",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{$node[\"Telegram Trigger\"].json.message.photo}}",
"rightValue": "true"
}
]
},
"renameOutput": true
},
{
"outputKey": "Text",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "3e137d35-f896-4ecf-bc4e-a481240c7acd",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{$node[\"Telegram Trigger\"].json.message.text}}",
"rightValue": "true"
}
]
},
"renameOutput": true
}
]
},
"options": {},
"looseTypeValidation": true
},
"typeVersion": 3.4
},
{
"id": "2881f5cc-61ba-4204-acaa-f0cfb7615a1b",
"name": "Code (Restore Telegram Payload)",
"type": "n8n-nodes-base.code",
"position": [
944,
-48
],
"parameters": {
"jsCode": "return [{ json: $node[\"Telegram Trigger\"].json }];"
},
"typeVersion": 2
},
{
"id": "6408da1b-ca88-48ad-a351-ea89b565d527",
"name": "Set (Text Context)",
"type": "n8n-nodes-base.set",
"position": [
1408,
336
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "d5fe1c74-66db-4a76-ae1e-f8c8043f8309",
"name": "raw_input",
"type": "string",
"value": "={{$json.message.text}}"
},
{
"id": "45bc27fc-b351-4429-a7cb-45c6138a7f7c",
"name": "message_id",
"type": "string",
"value": "={{$json.message.message_id}}"
},
{
"id": "06f119fc-1fb7-4033-a1f3-68f292f294e4",
"name": "chat_id",
"type": "string",
"value": "={{$json.message.chat.id}}"
},
{
"id": "98509cc8-b1b5-4384-899e-19d7a2ffe14d",
"name": "source_type",
"type": "string",
"value": "text"
},
{
"id": "c25b1e8b-91e8-45ab-b0c5-189c8184223f",
"name": "now",
"type": "string",
"value": "={{ $now.setZone('Asia/Ho_Chi_Minh').toFormat('yyyy-LL-dd HH:mm:ss') }}"
},
{
"id": "ba343e09-509b-4031-958f-b41ab59505fc",
"name": "update_id",
"type": "string",
"value": "={{ $json.update_id }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "10079662-d912-4409-91e5-09fa347111ce",
"name": "Google Gemini Chat (Text \u2192 JSON)",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
1648,
336
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-2.5-flash",
"cachedResultName": "models/gemini-2.5-flash"
},
"options": {},
"messages": {
"values": [
{
"content": "=You are an advanced Expense Tracker Assistant.\nYour goal is to extract expense data from the user input into structured JSON.\n\nCURRENT CONTEXT:\n- Date: {{ $json.now }}\n- Default Currency: {{ $('CONFIG - User Settings').item.json.currency_code }}\n\nRULES:\n1. Extract ALL distinct items.\n2. Category MUST be one of: [Food, Transport, Bills, Shopping, Entertainment, Other].\n3. Payment Method: Infer if possible (e.g., \"transfer\" -> Transfer), else default to \"Cash\".\n4. AMOUNT NORMALIZATION:\n - Convert \"k\" or \"grand\" to 1,000 (e.g., \"50k\" -> 50000).\n - Convert \"m\", \"mil\", or \"million\" to 1,000,000.\n - Return pure integers (no decimals).\n5. If currency is missing, use Default Currency from context.\n\nOUTPUT FORMAT (Strict JSON, No Markdown):\n{\n \"expenses\": [\n {\n \"item\": \"string (short description)\",\n \"amount\": number,\n \"currency\": \"string\",\n \"category\": \"string\",\n \"payment_method\": \"string\",\n \"date\": \"YYYY-MM-DD HH:mm:ss\"\n }\n ],\n \"summary_text\": \"string (A concise confirmation message in English)\"\n}"
},
{
"content": "={{$json.raw_input}}"
}
]
}
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "b0accb8d-9078-4361-bc9b-609e39fc6fd1",
"name": "Code (Parse Gemini JSON)",
"type": "n8n-nodes-base.code",
"position": [
2496,
-64
],
"parameters": {
"jsCode": "function extractJsonObject(text) {\n if (!text || typeof text !== 'string') return null;\n\n // Remove ```json ... ``` fences\n let cleaned = text\n .replace(/```json/gi, '```')\n .replace(/```/g, '')\n .trim();\n\n // Try direct parse\n try {\n return { obj: JSON.parse(cleaned), cleaned };\n } catch (e) {}\n\n // Fallback: extract first {...} block\n const match = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (!match) return null;\n\n try {\n return { obj: JSON.parse(match[0]), cleaned: match[0] };\n } catch (e) {\n return null;\n }\n}\n\nconst aiText = $json?.content?.parts?.[0]?.text ?? '';\nconst parsed = extractJsonObject(aiText);\n\nif (!parsed) {\n throw new Error(`Gemini output is not valid JSON. Raw:\\n${aiText}`);\n}\n\nconst data = parsed.obj;\n\n// Basic validation\nif (!Array.isArray(data.expenses)) data.expenses = [];\nif (typeof data.summary_text !== 'string') data.summary_text = '';\n\n// Context: Prioritize using the current item (to be used for Text + Photo + Voice)\nconst updateIdFromTrigger = $node[\"Telegram Trigger\"]?.json?.update_id;\n\nconst ctx = {\n update_id: $json.update_id ?? updateIdFromTrigger ?? null,\n raw_input: $json.raw_input ?? null,\n message_id: $json.message_id ?? null,\n chat_id: $json.chat_id ?? null,\n source_type: $json.source_type ?? null,\n // It supports both the \"now\" field (in case of accidentally setting the wrong key with a space) and the $now field.\n now: $json.now ?? $json[\"now \"] ?? $now,\n};\n\nreturn [{\n json: {\n ...data,\n _ai_raw_text: aiText,\n _ai_clean_json_text: parsed.cleaned,\n ...ctx,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "c47bab6f-6c9f-412f-a717-262277657d14",
"name": "Code (Split expenses to items)",
"type": "n8n-nodes-base.code",
"position": [
2880,
-80
],
"parameters": {
"jsCode": "// Get the configuration from the User Settings node.\nconst config = $('CONFIG - User Settings').item.json;\nconst timeZone = config.timezone || 'Asia/Ho_Chi_Minh';\nconst defaultCurrency = config.currency_code || 'USD';\n\nfunction formatDateTimeInTZ(dateInput, tz) {\n const d = dateInput ? new Date(dateInput) : new Date();\n if (Number.isNaN(d.getTime())) return '';\n\n const parts = new Intl.DateTimeFormat('en-GB', {\n timeZone: tz,\n year: 'numeric', month: '2-digit', day: '2-digit',\n hour: '2-digit', minute: '2-digit', second: '2-digit',\n hour12: false,\n }).formatToParts(d);\n\n const map = Object.fromEntries(parts.map(p => [p.type, p.value]));\n return `${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}:${map.second}`;\n}\n\nconst expenses = $json.expenses ?? [];\nconst updateId = $node[\"Telegram Trigger\"].json.update_id;\nconst fallbackDate = formatDateTimeInTZ($json.now ?? new Date(), timeZone);\n\nreturn expenses.map((e) => {\n const dateValue = (typeof e?.date === 'string' && e.date.trim())\n ? e.date.trim()\n : fallbackDate;\n\n return {\n json: {\n update_id: updateId,\n message_id: $json.message_id,\n chat_id: $json.chat_id,\n source_type: $json.source_type,\n raw_input: $json.raw_input,\n\n item: e.item ?? '',\n amount: Number(e.amount ?? 0),\n // Use the default currency from the Config if the AI doesn't return it.\n currency: e.currency ?? defaultCurrency,\n category: e.category ?? 'Other',\n payment_method: e.payment_method ?? 'Cash',\n\n date: dateValue,\n summary_text: $json.summary_text,\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "1cd58e0a-904a-4ac6-88dc-e35b6f3cbfc2",
"name": "Google Sheets \u2192 Append row(s)",
"type": "n8n-nodes-base.googleSheets",
"position": [
3072,
-80
],
"parameters": {
"columns": {
"value": {
"Date": "={{ $json.date }}",
"Item": "={{ $json.item }}",
"Amount": "={{ $json.amount }}",
"Category": "={{ $json.category }}",
"Currency": "={{ $json.currency }}",
"Raw_Input": "={{ $json.raw_input }}",
"Update_ID": "={{ $json.update_id }}",
"Message_ID": "={{ $json.message_id }}",
"Payment_Method": "={{ $json.payment_method }}"
},
"schema": [
{
"id": "Update_ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Update_ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Message_ID",
"type": "string",
"display": true,
"required": false,
"displayName": "Message_ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Item",
"type": "string",
"display": true,
"required": false,
"displayName": "Item",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Amount",
"type": "string",
"display": true,
"required": false,
"displayName": "Amount",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Payment_Method",
"type": "string",
"display": true,
"required": false,
"displayName": "Payment_Method",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Category",
"type": "string",
"display": true,
"required": false,
"displayName": "Category",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Currency",
"type": "string",
"display": true,
"required": false,
"displayName": "Currency",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Raw_Input",
"type": "string",
"display": true,
"required": false,
"displayName": "Raw_Input",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "id",
"value": "={{ $('CONFIG - User Settings').item.json.sheet_gid_log }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('CONFIG - User Settings').item.json.spreadsheet_id }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"typeVersion": 4.7
},
{
"id": "d57bc019-f640-48d0-9628-4e3c9c1f5d57",
"name": "IF (Has expenses?)",
"type": "n8n-nodes-base.if",
"position": [
2672,
-64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "8d8e2ec2-b28e-47f8-8066-69c3f036fa21",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ ($json.expenses || []).length > 0 }}",
"rightValue": ""
}
]
},
"looseTypeValidation": true
},
"typeVersion": 2.3
},
{
"id": "ee625a24-d767-4ffe-88ab-0b174e656a3c",
"name": "Set (Photo Context)",
"type": "n8n-nodes-base.set",
"position": [
1408,
-64
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "03df0e04-9fbe-49d6-8018-52f43f252b96",
"name": "update_id",
"type": "string",
"value": "={{$json.update_id}}"
},
{
"id": "12fc48a7-aacf-460a-95d5-93e3e4dd1b4c",
"name": "message_id",
"type": "string",
"value": "={{$json.message.message_id}}"
},
{
"id": "93ec3b26-05aa-45d6-99e6-52e12d3cdfbe",
"name": "chat_id",
"type": "string",
"value": "={{$json.message.chat.id}}"
},
{
"id": "9955eb19-7634-4cf9-8cf8-3a868aaacad2",
"name": "source_type",
"type": "string",
"value": "photo"
},
{
"id": "8a7b2fc9-350c-499e-a5aa-d28cc37701b7",
"name": "caption",
"type": "string",
"value": "={{$json.message.caption || \"[photo]\"}}"
},
{
"id": "5c70c114-304e-446f-8ced-30d935ea46f1",
"name": "raw_input",
"type": "string",
"value": "={{$json.message.caption || \"[photo]\"}}"
},
{
"id": "e7e5b2d3-cbe6-4dec-a0b2-3ee3e5906fcc",
"name": "now",
"type": "string",
"value": "={{ $now.setZone('Asia/Ho_Chi_Minh').toFormat('yyyy-LL-dd HH:mm:ss') }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "4da8ecb9-2306-457f-97d2-de9a6200bd5a",
"name": "Code (Pick Best Photo)",
"type": "n8n-nodes-base.code",
"position": [
1616,
-64
],
"parameters": {
"jsCode": "const photos = $json?.message?.photo ?? [];\nif (!photos.length) throw new Error('No message.photo found');\n\nconst best = [...photos].sort((a, b) => (a.file_size ?? 0) - (b.file_size ?? 0)).pop();\n\nreturn [{\n json: {\n ...$json,\n file_id: best.file_id,\n photo_width: best.width,\n photo_height: best.height,\n photo_file_size: best.file_size ?? null,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "9318dbfe-62f6-4136-a880-05a6e8b32f52",
"name": "Code (Normalize Gemini Image Output)",
"type": "n8n-nodes-base.code",
"position": [
2224,
-64
],
"parameters": {
"jsCode": "// 1) Get text output from Gemini Analyze Image\nconst aiText =\n $json?.content?.parts?.[0]?.text ??\n $json?.text ??\n '';\n\n// 2) Get context from Set (Photo Context)\nconst ctx = $node[\"Set (Photo Context)\"].json;\n\nreturn [{\n json: {\n // Standardize to match the format Parse is reading.\n content: { parts: [{ text: aiText }] },\n\n // Context for downstream to share\n update_id: ctx.update_id,\n message_id: ctx.message_id,\n chat_id: ctx.chat_id,\n source_type: ctx.source_type ?? 'photo',\n\n // raw_input: prioritize raw_input if you have it, fallback caption\n raw_input: ctx.raw_input ?? ctx.caption ?? '[photo]',\n\n now: ctx.now ?? $now,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "ea8bb0d4-9ea0-482c-b2d2-3e035f810993",
"name": "Telegram \u2192 Send Error Message and wait for response",
"type": "n8n-nodes-base.telegram",
"position": [
2880,
128
],
"parameters": {
"text": "=\u26a0\ufe0f Could not understand expenses in: \"{{$json.raw_input}}\"\nPlease try format: \"Lunch 10k\" or \"Taxi 50k\".",
"chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
"additionalFields": {
"reply_to_message_id": "={{ $json.message_id }}"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "dd199ba1-0778-49f7-9fb6-a932092fd5f4",
"name": "Telegram \u2192 Send Final Message",
"type": "n8n-nodes-base.telegram",
"position": [
2880,
-272
],
"parameters": {
"text": "={{ $json.summary_text }}",
"chatId": "={{ $json.chat_id }}",
"additionalFields": {
"reply_to_message_id": "={{ $json.message_id }}"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "64d0da46-7a55-460e-914a-f3f89fc53d78",
"name": "Google Gemini (Analyze Image)",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
2016,
-64
],
"parameters": {
"text": "Analyze this receipt image and extract expense items.\nReturn ONLY raw JSON (no markdown, no ```).\n\nCONTEXT:\n- Default Currency: {{ $('CONFIG - User Settings').item.json.currency_code }}\n\nOutput schema:\n{\n \"expenses\": [\n {\n \"item\": \"string\",\n \"amount\": number,\n \"currency\": \"string\",\n \"category\": \"Food|Shopping|Transport|Bills|Entertainment|Other\",\n \"payment_method\": \"Cash|Card|Transfer\",\n \"date\": \"YYYY-MM-DD HH:mm:ss\"\n }\n ],\n \"summary_text\": \"string (Concise summary in English, e.g. 'Receipt processed: Item A, Item B')\"\n}",
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-2.5-flash",
"cachedResultName": "models/gemini-2.5-flash"
},
"options": {},
"resource": "image",
"inputType": "binary",
"operation": "analyze"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "7af32a67-6212-439d-8189-b27b94a8261f",
"name": "Set (Voice Context)",
"type": "n8n-nodes-base.set",
"position": [
1408,
-464
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "c84c376e-40f2-4026-9875-9e36880714eb",
"name": "update_id",
"type": "string",
"value": "={{ $json.update_id }}"
},
{
"id": "55101232-245a-43e9-a903-bdf36cffb71d",
"name": "message_id",
"type": "string",
"value": "={{$json.message.message_id}}"
},
{
"id": "4f342238-d326-498c-9570-fd1f5a5fa40f",
"name": "chat_id",
"type": "string",
"value": "={{$json.message.chat.id}}"
},
{
"id": "72c7c3ab-cd23-49e7-b2e9-52d78317a8d2",
"name": "source_type",
"type": "string",
"value": "voice"
},
{
"id": "229a19c4-9e34-4f76-b1e6-0ba16b0fbc45",
"name": "file_id",
"type": "string",
"value": "={{$json.message.voice.file_id}}"
},
{
"id": "e953e5e2-356a-434c-923a-d157c7f8dea8",
"name": "raw_input",
"type": "string",
"value": "[voice]"
},
{
"id": "15a7e226-39c0-42d3-8e08-d904eb8c6758",
"name": "now",
"type": "string",
"value": "={{ $now.setZone('Asia/Ho_Chi_Minh').toFormat('yyyy-LL-dd HH:mm:ss') }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "fc1d557e-30b4-4f28-852a-f19af6ddcb76",
"name": "Telegram \u2192 Get Voice File",
"type": "n8n-nodes-base.telegram",
"position": [
1600,
-464
],
"parameters": {
"fileId": "={{$json.file_id}}",
"resource": "file",
"additionalFields": {
"mimeType": "audio/ogg"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "022846d3-718f-41ad-b6c2-89d990e95629",
"name": "Telegram \u2192 Get Image File",
"type": "n8n-nodes-base.telegram",
"position": [
1808,
-64
],
"parameters": {
"fileId": "={{$json.file_id}}",
"resource": "file",
"additionalFields": {
"mimeType": "image/jpeg"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "3c664eb1-856f-4b6b-8408-351c5f2653dc",
"name": "Google Gemini (Analyze Audio)",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
1808,
-464
],
"parameters": {
"text": "You are an advanced Expense Tracker Assistant.\nInput is an AUDIO message (voice note).\n\nTASKS:\n1. Transcribe the audio accurately to text (Detect language automatically).\n2. Extract expense data from the transcript.\n\nCONTEXT:\n- Default Currency: {{ $('CONFIG - User Settings').item.json.currency_code }}\n\nOutput schema (Strict JSON, No Markdown):\n{\n \"expenses\": [\n {\n \"item\": \"string\",\n \"amount\": number,\n \"currency\": \"string\",\n \"category\": \"Food|Shopping|Transport|Bills|Entertainment|Other\",\n \"payment_method\": \"Cash|Card|Transfer\",\n \"date\": \"YYYY-MM-DD HH:mm:ss\"\n }\n ],\n \"summary_text\": \"string (Concise summary in English)\"\n}",
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-2.5-flash",
"cachedResultName": "models/gemini-2.5-flash"
},
"options": {},
"resource": "audio",
"inputType": "binary",
"operation": "analyze"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "5a718e98-34de-407f-ab47-e975d3249ebe",
"name": "Code (Normalize Gemini Audio Output)",
"type": "n8n-nodes-base.code",
"position": [
2016,
-464
],
"parameters": {
"jsCode": "// 1) Get text output from Gemini Analyze Audio\nconst aiText =\n $json?.content?.parts?.[0]?.text ??\n $json?.text ??\n '';\n\n// 2) Get context from Set (Voice Context)\nconst ctx = $node[\"Set (Voice Context)\"].json;\n\nreturn [{\n json: {\n // Standardize to match the format Parse is reading.\n content: { parts: [{ text: aiText }] },\n\n // Context for downstream to share\n update_id: ctx.update_id,\n message_id: ctx.message_id,\n chat_id: ctx.chat_id,\n source_type: ctx.source_type ?? 'voice',\n\n // raw_input: retain the [voice] marker or caption\n raw_input: ctx.raw_input ?? '[voice]',\n\n now: ctx.now ?? $now,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "0f04fa94-5c7b-48ec-9ad3-558a7cb24316",
"name": "Code (Normalize Gemini Text Output)",
"type": "n8n-nodes-base.code",
"position": [
2016,
336
],
"parameters": {
"jsCode": "// 1) Get text output from Gemini Chat (Text)\nconst aiText =\n $json?.content?.parts?.[0]?.text ??\n $json?.text ??\n '';\n\n// 2) Get context from Set (Text Context)\nconst ctx = $node[\"Set (Text Context)\"].json;\n\nreturn [{\n json: {\n // Standardize to match the format Parse is reading.\n content: { parts: [{ text: aiText }] },\n\n // Context for downstream to share\n update_id: ctx.update_id,\n message_id: ctx.message_id,\n chat_id: ctx.chat_id,\n source_type: ctx.source_type ?? 'text',\n raw_input: ctx.raw_input ?? '[text]',\n\n now: ctx.now ?? $now,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "e8c72bf3-485e-4a10-bfb2-2c98a966e9bb",
"name": "Switch (Command Router)",
"type": "n8n-nodes-base.switch",
"position": [
416,
-272
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "AddBudget",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c48f3c1f-65ec-4d06-9464-7bc8efa70bdf",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ /^\\/add(?:@\\w+)?\\s+budget\\b/i.test(($json.message?.text || '').trim()) }}",
"rightValue": true
}
]
},
"renameOutput": true
},
{
"outputKey": "Default",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "58c02279-ceef-4ab7-bd74-4223c5cf8abb",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ !/^\\/add(?:@\\w+)?\\s+budget\\b/i.test(($json.message?.text || '').trim()) }}",
"rightValue": true
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.4
},
{
"id": "bed08bc1-2a8d-4e18-90c0-3a9a145169bc",
"name": "Code (Parse Budget Amount)",
"type": "n8n-nodes-base.code",
"position": [
752,
-928
],
"parameters": {
"jsCode": "function parseAmount(text) {\n if (!text) return null;\n\n // Remove command prefix\n let s = text.trim().replace(/^\\/add(?:@\\w+)?\\s+budget\\b/i, '').trim();\n\n // Normalize: Remove commas (1,000 -> 1000)\n s = s.replace(/,/g, '').replace(/\\s+/g, ' ').trim();\n const lower = s.toLowerCase();\n\n // Find number\n const m = lower.match(/(\\d+(?:\\.\\d+)?)/);\n if (!m) return null;\n\n const num = Number(m[1]);\n if (!Number.isFinite(num)) return null;\n\n let multiplier = 1;\n // Global suffixes: k = 1000, m = 1,000,000\n if (/(k|grand)\\b/i.test(lower)) multiplier = 1000;\n if (/(m|mil|million)\\b/i.test(lower)) multiplier = 1000000;\n \n // support Vietnamese currency (Hybrid)\n if (/(tr|tri\u1ec7u|trieu)\\b/i.test(lower)) multiplier = 1000000; \n\n return Math.round(num * multiplier);\n}\n\nconst text = $json.message?.text ?? '';\nconst amount = parseAmount(text);\n// Get the configuration to reformat the display for the user\nconst config = $('CONFIG - User Settings').item.json;\nconst locale = config.locale || 'en-US';\nconst symbol = config.currency_symbol || '$';\n\nreturn [{\n json: {\n ok: !!(amount && amount > 0),\n amount: amount || 0,\n chat_id: $json.message?.chat?.id,\n message_id: $json.message?.message_id,\n raw_text: text,\n formatted_amount: (amount || 0).toLocaleString(locale) + ' ' + symbol\n }\n}];"
},
"typeVersion": 2
},
{
"id": "bef10b08-afb6-492e-8581-f859ee5084b7",
"name": "IF (Budget ok?)",
"type": "n8n-nodes-base.if",
"position": [
944,
-928
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4c555183-5715-4b0b-91ab-b2f14498a9e5",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.ok }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "d7a70e37-8fab-4c8e-b040-22897d66b88a",
"name": "Telegram \u2192 Budget Error",
"type": "n8n-nodes-base.telegram",
"position": [
1184,
-736
],
"parameters": {
"text": "=\u26a0\ufe0f Invalid format.\nUsage: /add budget 500k or /add budget 10m.\n(Input: {{$json.raw_text}})",
"chatId": "={{ $json.chat_id }}",
"additionalFields": {
"reply_to_message_id": "={{ $json.message_id }}"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "9551f85a-3dd3-413c-a5b9-155a3065611e",
"name": "Google Sheets \u2192 Append or update row in sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
1168,
-944
],
"parameters": {
"columns": {
"value": {
"ts": "={{ new Date().toLocaleString('vi-VN', { timeZone: 'Asia/Ho_Chi_Minh' }) }}",
"amount": "={{ $json.amount }}",
"raw_text": "={{ $json.raw_text }}"
},
"schema": [
{
"id": "ts",
"type": "string",
"display": true,
"required": false,
"displayName": "ts",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "amount",
"type": "string",
"display": true,
"required": false,
"displayName": "amount",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "raw_text",
"type": "string",
"display": true,
"required": false,
"displayName": "raw_text",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "id",
"value": "={{ Number($('CONFIG - User Settings').item.json.sheet_gid_budget) }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('CONFIG - User Settings').item.json.spreadsheet_id }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "af27370d-b54a-4b35-b31d-679a1889ebe8",
"name": "Telegram \u2192 Budget Updated",
"type": "n8n-nodes-base.telegram",
"position": [
1360,
-944
],
"parameters": {
"text": "=\u2705 Budget updated to: {{$json.formatted_amount}}",
"chatId": "={{ $('IF (Budget ok?)').item.json.chat_id }}",
"additionalFields": {
"reply_to_message_id": "={{ $('IF (Budget ok?)').item.json.message_id }}"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "179743fe-7387-48b4-a2e0-50a5b9479ad4",
"name": "GS - Get Daily Report Range",
"type": "n8n-nodes-base.googleSheets",
"position": [
4512,
-80
],
"parameters": {
"options": {
"dataLocationOnSheet": {
"values": {
"range": "D1:I2",
"rangeDefinition": "specifyRangeA1"
}
}
},
"sheetName": {
"__rl": true,
"mode": "id",
"value": "={{ Number($('CONFIG - User Settings').item.json.sheet_gid_dashboard) }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('CONFIG - User Settings').item.json.spreadsheet_id }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"notesInFlow": false,
"typeVersion": 4.7
},
{
"id": "9852ebb9-18c4-4420-9281-048c03995433",
"name": "Code - Build Daily Report",
"type": "n8n-nodes-base.code",
"position": [
4704,
-80
],
"parameters": {
"jsCode": "const r = $json;\nconst config = $('CONFIG - User Settings').item.json;\nconst locale = config.locale || 'en-US';\nconst symbol = config.currency_symbol || '$';\n\nfunction num(v){ return Number(String(v ?? '0').replace(/[^\\d.-]/g,'')) || 0; }\nfunction fmt(n){ return num(n).toLocaleString(locale) + ' ' + symbol; }\n\nconst note = String(r.note ?? '').trim();\n\nconst text =\n`\ud83d\udcca *Daily Report* (${r.date})\n\n\ud83d\udcb0 Total budget: *${fmt(r.total_budget)}*\n\ud83d\udcb8 Total spent: *${fmt(r.total_spent)}*\n\ud83c\udfe6 Remaining: *${fmt(r.remaining)}*\n\ud83d\udcc9 Daily Avg: *${fmt(r.monthly_rate)}*${note ? `\\n\\n\ud83d\udcdd Note: ${note}` : ''}`;\n\nreturn [{ json: { text } }];"
},
"typeVersion": 2
},
{
"id": "8a1698e4-c3df-4ca8-be1a-c0c595d70058",
"name": "TG - Send Daily Report",
"type": "n8n-nodes-base.telegram",
"position": [
4912,
-80
],
"parameters": {
"text": "={{ $json.text }}",
"chatId": "={{ $node[\"Telegram Trigger\"].json.message.chat.id }}",
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "aeaf6ba9-378d-4065-b020-72d63f68bf34",
"name": "Code (Schedule Report Token)",
"type": "n8n-nodes-base.code",
"position": [
3248,
-80
],
"parameters": {
"jsCode": "const chatId = $node[\"Telegram Trigger\"].json.message?.chat?.id;\nif (!chatId) throw new Error(\"Missing chat_id from Telegram Trigger\");\n\nconst token = `${Date.now()}_${Math.random().toString(16).slice(2)}`;\n\nreturn [{\n json: {\n chat_id: String(chatId),\n report_token: token,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "d8055f37-ddb8-4b09-97c9-733fd69d5259",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"position": [
0,
-272
],
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "a6ee6c21-fa8e-44eb-b200-d42766bcb7ad",
"name": "Code - Check Latest Token",
"type": "n8n-nodes-base.code",
"position": [
3920,
-80
],
"parameters": {
"jsCode": "const scheduledToken = $node[\"Wait\"].json.report_token;\n\n// Data table Get row(s): each row is an item => must be retrieved via $input.all()\nconst items = $input.all().map(i => i.json);\n\nif (!items.length) {\n return [{ json: { shouldSend: false, reason: 'No row found', itemsCount: 0 } }];\n}\n\n// If the Upgrade is correct, there is usually only 1 item.\n// But to be safe, use the newest one based on updatedAt (fallback is based on ID).\nitems.sort((a, b) => {\n const ta = new Date(a.updatedAt || a.createdAt || 0).getTime();\n const tb = new Date(b.updatedAt || b.createdAt || 0).getTime();\n if (ta !== tb) return tb - ta;\n return (b.id || 0) - (a.id || 0);\n});\n\nconst latest = items[0];\nconst latestToken = latest?.report_token ?? null;\n\nreturn [{\n json: {\n shouldSend: String(latestToken ?? '') === String(scheduledToken ?? ''),\n debug_latestToken: latestToken,\n debug_scheduledToken: scheduledToken,\n debug_itemsCount: items.length,\n debug_latestRowId: latest?.id ?? null,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "f5ab1340-25b8-4573-9225-8aa67e1e434a",
"name": "If",
"type": "n8n-nodes-base.if",
"position": [
4096,
-64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "b4555b5d-0140-41df-bc19-01adab9af376",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.shouldSend }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "b4027f12-8ec6-4f80-83a5-678229609c19",
"name": "ReportTokens",
"type": "n8n-nodes-base.dataTable",
"position": [
3424,
-80
],
"parameters": {
"columns": {
"value": {
"chat_id": "={{ $json.chat_id }}",
"updated_at": "={{ $now.setZone('Asia/Ho_Chi_Minh').toFormat('yyyy-LL-dd HH:mm:ss') }}",
"report_token": "={{ $json.report_token }}"
},
"schema": [
{
"id": "chat_id",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "chat_id",
"defaultMatch": false
},
{
"id": "report_token",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "report_token",
"defaultMatch": false
},
{
"id": "updated_at",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "updated_at",
"defaultMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"filters": {
"conditions": [
{
"keyName": "chat_id",
"keyValue": "={{ $json.chat_id }}"
}
]
},
"options": {},
"operation": "upsert",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "QvSW24TWoTE0N3cH",
"cachedResultUrl": "/projects/abUHa4TKmFzPJngM/datatables/QvSW24TWoTE0N3cH",
"cachedResultName": "ReportTokens"
}
},
"typeVersion": 1
},
{
"id": "9e10464d-c414-4091-a991-089403a7ae39",
"name": "Data table \u2192 Get row(s)",
"type": "n8n-nodes-base.dataTable",
"position": [
3744,
-80
],
"parameters": {
"filters": {
"conditions": [
{
"keyName": "chat_id",
"keyValue": "={{ $node[\"Wait\"].json.chat_id }}"
}
]
},
"operation": "get",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "QvSW24TWoTE0N3cH",
"cachedResultUrl": "/projects/abUHa4TKmFzPJngM/datatables/QvSW24TWoTE0N3cH",
"cachedResultName": "ReportTokens"
}
},
"typeVersion": 1
},
{
"id": "fa1ae335-6574-45fc-aa5a-a84fdfd1ba2f",
"name": "Data table \u2192 Delete row(s)",
"type": "n8n-nodes-base.dataTable",
"position": [
4304,
-80
],
"parameters": {
"filters": {
"conditions": [
{
"keyName": "chat_id",
"keyValue": "={{ $node[\"Wait\"].json.chat_id }}"
}
]
},
"options": {},
"operation": "deleteRows",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "QvSW24TWoTE0N3cH",
"cachedResultUrl": "/projects/abUHa4TKmFzPJngM/datatables/QvSW24TWoTE0N3cH",
"cachedResultName": "ReportTokens"
}
},
"typeVersion": 1
},
{
"id": "fa2618f9-6914-41ab-a274-0f90dbebb441",
"name": "Wait",
"type": "n8n-nodes-base.wait",
"position": [
3584,
-80
],
"parameters": {
"unit": "minutes",
"amount": 30
},
"typeVersion": 1.1
},
{
"id": "52f3a9a9-5cc4-4c5a-9cf1-fdb42663c306",
"name": "CONFIG - User Settings",
"type": "n8n-nodes-base.set",
"position": [
208,
-272
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "2696366a-a0ac-4911-8f0b-3c1e4cb4efab",
"name": "spreadsheet_id",
"type": "string",
"value": "Input your Spread Sheet ID here"
},
{
"id": "393db088-bfe0-493e-9fdb-61eb49d62fd1",
"name": "sheet_gid_log",
"type": "string",
"value": "gid=0"
},
{
"id": "719092f5-e6b7-416c-9b73-4b7346bdf6df",
"name": "sheet_gid_dashboard",
"type": "string",
"value": "Input your Sheet \"Dashboard\" ID here"
},
{
"id": "56c12832-6f97-47b0-93de-986dcaa1357f",
"name": "sheet_gid_budget",
"type": "string",
"value": "Input your Sheet \"Budget Topups\" ID here"
},
{
"id": "4de77bb6-8952-4df1-a34d-16b4c9393b8a",
"name": "currency_code",
"type": "string",
"value": "USD"
},
{
"id": "13626830-f7c6-44b3-bd31-3b0ac81b56e3",
"name": "currency_symbol",
"type": "string",
"value": "$"
},
{
"id": "8a3aba9b-1e2d-4608-89c0-4713c594e657",
"name": "locale",
"type": "string",
"value": "en-US"
},
{
"id": "39a94c66-e67f-4e3a-8f01-2a5e76040ebe",
"name": "timezone",
"type": "string",
"value": "Input your timezone"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "6bb87bf8-368b-4e9a-8ef2-77e0e610d5ae",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-32,
-1024
],
"parameters": {
"width": 592,
"height": 576,
"content": "# AI Multimodal Expense Tracker\n**Global, Multi-Currency Expense Logger via Telegram (Text, Voice, Photo).**\n\nThis workflow turns Telegram into a frictionless finance assistant using Gemini AI. It supports receipt scanning, voice logging, budget management, and smart daily reporting.\n\n### How it works\n1. **Router:** Routes incoming messages (Text, Audio, Photo) to the specific Gemini AI agent.\n2. **AI Extraction:** Gemini analyzes the input to extract structured data (Item, Amount, Category).\n3. **Normalization:** Javascript logic normalizes numbers (k/m suffixes) and handles global currency formatting based on your Locale config.\n4. **Logging:** Data is appended to Google Sheets.\n5. **Smart Report:** A \"debounce\" logic waits for 30 minutes of inactivity before sending a daily summary to avoid spamming.\n\n### Setup steps\n1. **Prerequisites:** You **MUST** copy the provided Google Sheet Template and create a \"ReportTokens\" Data Table in n8n (See Template Description).\n2. **Config:** Open the `CONFIG - User Settings` node to set your Sheet ID, Currency (USD/VND), and Locale.\n3. **Credentials:** Connect Telegram, Google Sheets, and Google Gemini (PaLM) accounts."
},
"typeVersion": 1
},
{
"id": "6d5e677d-c3f8-4e4d-932b-f03ff87e6b22",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-32,
-368
],
"parameters": {
"color": 7,
"width": 1280,
"height": 480,
"content": "## 1. Input & Routing\nInitializes configuration, handles credentials, and routes inputs to the appropriate processing chain (Command, Voice, Photo, or Text)."
},
"typeVersion": 1
},
{
"id": "4489dbec-b060-4b53-a509-1dd6f053cf02",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1280,
-544
],
"parameters": {
"color": 7,
"width": 1104,
"height": 1072,
"content": "## 2. AI Extraction & Normalization\nGemini AI analyzes multimodal inputs to extract structured JSON. Code nodes normalize currency formats and parse the AI response."
},
"typeVersion": 1
},
{
"id": "5836aead-4611-4f97-9066-84e1b4531ce1",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
3216,
-176
],
"parameters": {
"color": 7,
"width": 1856,
"height": 288,
"content": "## 5. Smart Debounced Reporting (optional)\nUses n8n Data Table to wait for 30 minutes of inactivity before generating and sending a daily financial summary."
},
"typeVersion": 1
},
{
"id": "7d325593-4132-4125-8f8b-fd167afaecf6",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
704,
-1024
],
"parameters": {
"color": 7,
"width": 784,
"height": 432,
"content": "## 4. Logging & Budget Logic\nSplits multiple items into separate rows, handles \"/add budget\" commands, and writes clean data to Google Sheets."
},
"typeVersion": 1
},
{
"id": "f9de232a-e13f-423d-9907-0f5516b0a9a9",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
112,
-112
],
"parameters": {
"color": 3,
"width": 304,
"height": 176,
"content": "### \u26a0\ufe0f CRITICAL SETUP REQUIRED\n1. **Google Sheet:** You must use the Template provided in the description.\n2. **Data Table (optional):** You must create a `ReportTokens` table in n8n for the reporting feature to work. \nCheck the Template Description for the setup guide!"
},
"typeVersion": 1
},
{
"id": "2dc28159-a189-4b28-8982-44cd7558d18b",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
2416,
-384
],
"parameters": {
"color": 7,
"width": 784,
"height": 688,
"content": "## 3. Logging & Feedback\nValidates AI output, splits multiple items, logs to Google Sheets, and sends success/error feedback to Telegram."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"timezone": "Asia/Ho_Chi_Minh",
"callerPolicy": "workflowsFromSameOwner",
"timeSavedMode": "fixed",
"availableInMCP": false,
"executionOrder": "v1",
"executionTimeout": -1,
"saveExecutionProgress": true
},
"versionId": "f0b24ec4-2588-4ea3-b4bb-c6747d837dbe",
"connections": {
"If": {
"main": [
[
{
"node": "Data table \u2192 Delete row(s)",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [
[
{
"node": "Data table \u2192 Get row(s)",
"type": "main",
"index": 0
}
]
]
},
"ReportTokens": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"IF (Budget ok?)": {
"main": [
[
{
"node": "Google Sheets \u2192 Append or update row in sheet",
"type": "main",
"index": 0
}
],
[
{
"node": "Telegram \u2192 Budget Error",
"type": "main",
"index": 0
}
]
]
},
"Telegram Trigger": {
"main": [
[
{
"node": "CONFIG - User Settings",
"type": "main",
"index": 0
}
]
]
},
"IF (Has expenses?)": {
"main": [
[
{
"node": "Code (Split expenses to items)",
"type": "main",
"index": 0
},
{
"node": "Telegram \u2192 Send Final Message",
"type": "main",
"index": 0
}
],
[
{
"node": "Telegram \u2192 Send Error Message and wait for response",
"type": "main",
"index": 0
}
]
]
},
"IF (Is Duplicate?)": {
"main": [
[],
[
{
"node": "Code (Restore Telegram Payload)",
"type": "main",
"index": 0
}
]
]
},
"Set (Text Context)": {
"main": [
[
{
"node": "Google Gemini Chat (Text \u2192 JSON)",
"type": "main",
"index": 0
}
]
]
},
"Set (Photo Context)": {
"main": [
[
{
"node": "Code (Pick Best Photo)",
"type": "main",
"index": 0
}
]
]
},
"Set (Voice Context)": {
"main": [
[
{
"node": "Telegram \u2192 Get Voice File",
"type": "main",
"index": 0
}
]
]
},
"CONFIG - User Settings": {
"main": [
[
{
"node": "Switch (Command Router)",
"type": "main",
"index": 0
}
]
]
},
"Code (Pick Best Photo)": {
"main": [
[
{
"node": "Telegram \u2192 Get Image File",
"type": "main",
"index": 0
}
]
]
},
"Switch (Command Router)": {
"main": [
[
{
"node": "Code (Parse Budget Amount)",
"type": "main",
"index": 0
}
],
[
{
"node": "Google Sheets: Get Rows (Dedup lookup)",
"type": "main",
"index": 0
}
]
]
},
"Code (Parse Gemini JSON)": {
"main": [
[
{
"node": "IF (Has expenses?)",
"type": "main",
"index": 0
}
]
]
},
"Code - Build Daily Report": {
"main": [
[
{
"node": "TG - Send Daily Report",
"type": "main",
"index": 0
}
]
]
},
"Code - Check Latest Token": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"Data table \u2192 Get row(s)": {
"main": [
[
{
"node": "Code - Check Latest Token",
"type": "main",
"index": 0
}
]
]
},
"Switch (Voice/Photo/Text)": {
"main": [
[
{
"node": "Set (Voice Context)",
"type": "main",
"index": 0
}
],
[
{
"node": "Set (Photo Context)",
"type": "main",
"index": 0
}
],
[
{
"node": "Set (Text Context)",
"type": "main",
"index": 0
}
]
]
},
"Code (Parse Budget Amount)": {
"main": [
[
{
"node": "IF (Budget ok?)",
"type": "main",
"index": 0
}
]
]
},
"GS - Get Daily Report Range": {
"main": [
[
{
"node": "Code - Build Daily Report",
"type": "main",
"index": 0
}
]
]
},
"Telegram \u2192 Get Image File": {
"main": [
[
{
"node": "Google Gemini (Analyze Image)",
"type": "main",
"index": 0
}
]
]
},
"Telegram \u2192 Get Voice File": {
"main": [
[
{
"node": "Google Gemini (Analyze Audio)",
"type": "main",
"index": 0
}
]
]
},
"Code (Schedule Report Token)": {
"main": [
[
{
"node": "ReportTokens",
"type": "main",
"index": 0
}
]
]
},
"Data table \u2192 Delete row(s)": {
"main": [
[
{
"node": "GS - Get Daily Report Range",
"type": "main",
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.
googlePalmApigoogleSheetsOAuth2ApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Most expense tracker apps (like Money Lover, Spendee, or Wallet) have a common friction point: Data Entry. You have to unlock your phone, find the app, wait for it to load, navigate menus, and manually select categories. It’s tedious, so we often forget to log small expenses.
Source: https://n8n.io/workflows/12006/ — 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 is a complete, AI-powered content engine designed to help automation experts build their personal brand on LinkedIn. It transforms a technical n8n workflow (in JSON format) into a polish
Ask questions like “How much did I spend on food last month?” and get instant answers from your financial data — directly in Telegram.
> ⚠️ 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