This workflow corresponds to n8n.io template #16166 — we link there as the canonical source.
This workflow follows the Execute Workflow Trigger → 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": "XEGDrL6Z7dsNqGA6",
"meta": {
"builderVariant": "mcp",
"aiBuilderAssisted": true
},
"name": "Log receipts from Telegram to Google Sheets and query spending with AI",
"tags": [],
"nodes": [
{
"id": "264f5f07-9a30-40d9-8a23-609b9cbe8266",
"name": "MCP Server: Personal Finance",
"type": "@n8n/n8n-nodes-langchain.mcpTrigger",
"position": [
80,
240
],
"parameters": {
"path": "personal-finance"
},
"typeVersion": 1.1
},
{
"id": "71c308f1-1de8-43ed-8bf2-830745be71aa",
"name": "Query Spending",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"position": [
320,
512
],
"parameters": {
"workflowId": {
"__rl": true,
"mode": "id",
"value": "KPC0lhvFc8nNpp6J",
"cachedResultName": "Personal Finance Query Spending (MCP Tool)"
},
"description": "Query the logged expenses to answer questions about spending. Returns the total amount, a per-category breakdown, and the matching transactions for the requested category and month.",
"workflowInputs": {
"value": {
"month": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('month', 'Month to filter by in YYYY-MM format, for example 2026-06. Pass \"all\" to include all time periods.') }}",
"category": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('category', 'Expense category to filter by. Must be one of: Groceries, Dining, Transport, Utilities, Shopping, Health, Entertainment, Travel, Other. Pass \"all\" to include every category.') }}"
},
"schema": [
{
"id": "category",
"type": "string",
"display": true,
"required": true,
"displayName": "category",
"canBeUsedToMatch": false
},
{
"id": "month",
"type": "string",
"display": true,
"required": true,
"displayName": "month",
"canBeUsedToMatch": false
}
],
"mappingMode": "defineBelow"
}
},
"typeVersion": 2.2
},
{
"id": "90543223-b19d-4c66-bc2c-5381b4cd91ef",
"name": "Sticky Note 1024f92c",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
-32
],
"parameters": {
"color": 4,
"width": 520,
"height": 252,
"content": "## PERSONAL FINANCE MCP SERVER\nThis workflow turns your expense log into a **server that Claude and ChatGPT can query in plain language**.\n\nConnect an MCP client (Claude Desktop, ChatGPT, Cursor) and ask things like:\n- \"How much did I spend on dining in May 2026?\"\n- \"What is my total spending this month?\"\n\nThe AI calls the **Query Spending** tool, which runs the companion sub-workflow and returns the numbers."
},
"typeVersion": 1
},
{
"id": "c8d57810-d670-4faa-bd5b-85bbb34069b2",
"name": "Sticky Note e32fcb47",
"type": "n8n-nodes-base.stickyNote",
"position": [
560,
-32
],
"parameters": {
"color": 7,
"width": 420,
"height": 248,
"content": "## SETUP ORDER\n1. After importing, OPEN the **Query Spending** toolWorkflow node and RE-LINK it to your own copy of **Personal Finance Query Spending (MCP Tool)**. The saved ID points to the original author instance and will not resolve on yours.\n2. Make sure that companion sub-workflow is SAVED. It runs on demand when called, so it does NOT need to be activated.\n3. Publish (activate) THIS workflow to turn on the MCP endpoint and reveal the Production URL.\n4. Connect your MCP client (see the blue note)."
},
"typeVersion": 1
},
{
"id": "ce964e8f-7283-43f6-848e-e26a83880a0e",
"name": "Sticky Note bec4f349",
"type": "n8n-nodes-base.stickyNote",
"position": [
-208,
448
],
"parameters": {
"color": 6,
"width": 460,
"height": 200,
"content": "## SECURITY: READ THIS\nThis server is set to **No Authentication** so it imports and runs out of the box, and n8n exposes trigger URLs publicly.\n\nThis endpoint can read your spending data. Before sharing it or going to production, open the **MCP Server Trigger** node, switch **Authentication** to **Bearer Auth**, and add the same token in your MCP client."
},
"typeVersion": 1
},
{
"id": "12a104db-5efa-48c3-99a2-ff40f67d88ae",
"name": "Sticky Note 08915f35",
"type": "n8n-nodes-base.stickyNote",
"position": [
512,
416
],
"parameters": {
"color": 3,
"width": 460,
"height": 268,
"content": "## CONNECT CLAUDE DESKTOP\nClaude Desktop uses stdio while this server uses SSE/HTTP, so bridge them with mcp-remote.\n\nIn claude_desktop_config.json add an mcpServers entry named \"personal-finance\":\n- command: npx\n- args: mcp-remote, then your Production URL\n\nThe Production URL appears on the MCP Server Trigger node after you publish. For Bearer auth, add an arg: --header Authorization: Bearer YOUR_TOKEN.\nDocs: see the MCP Server Trigger node page on docs.n8n.io."
},
"typeVersion": 1
},
{
"id": "5fb41d8e-a74e-4bb1-a5ac-2192830fdade",
"name": "Receive Query Parameters",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
1440,
272
],
"parameters": {
"workflowInputs": {
"values": [
{
"name": "category"
},
{
"name": "month"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "5dc5dccb-fde1-45c3-9386-7fe0222a43d3",
"name": "Read Expenses",
"type": "n8n-nodes-base.googleSheets",
"position": [
1664,
272
],
"parameters": {
"sheetName": {
"__rl": true,
"mode": "list",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"typeVersion": 4.7
},
{
"id": "9a3943ee-5413-4c4d-92aa-3518a4c7f2b4",
"name": "Filter and Summarize",
"type": "n8n-nodes-base.code",
"position": [
1888,
272
],
"parameters": {
"jsCode": "const q = $('Receive Query Parameters').first().json;\nconst category = (q.category || 'all').toString().trim().toLowerCase();\nconst month = (q.month || 'all').toString().trim();\nconst rows = $input.all().map(i => i.json);\nconst filtered = rows.filter(r => {\n const rCat = (r.Category || '').toString().trim().toLowerCase();\n const rDate = (r.Date || '').toString().trim();\n const catOk = category === 'all' || rCat === category;\n const monthOk = month === 'all' || rDate.indexOf(month) === 0;\n return catOk && monthOk;\n});\nconst total = filtered.reduce((s, r) => s + (parseFloat(r.Amount) || 0), 0);\nconst byCat = {};\nfor (const r of filtered) {\n const c = (r.Category || 'Other').toString();\n byCat[c] = (byCat[c] || 0) + (parseFloat(r.Amount) || 0);\n}\nconst currency = filtered.length ? (filtered[0].Currency || 'EUR') : 'EUR';\nreturn [{ json: {\n query_category: category,\n query_month: month,\n matched_transactions: filtered.length,\n total_spent: Math.round(total * 100) / 100,\n currency: currency,\n breakdown_by_category: byCat,\n transactions: filtered.map(r => ({ date: r.Date, amount: r.Amount, vendor: r.Vendor, category: r.Category }))\n} }];"
},
"typeVersion": 2
},
{
"id": "f0b41584-14ac-4646-9b5c-063368018a30",
"name": "Sticky Note b5714413",
"type": "n8n-nodes-base.stickyNote",
"position": [
1472,
-32
],
"parameters": {
"color": 4,
"width": 460,
"height": 200,
"content": "## MCP TOOL: Query Spending\nThis sub-workflow is the **tool** called by the *Personal Finance MCP Server*. When you ask Claude or ChatGPT something like \"how much did I spend on groceries in June?\", the MCP hub calls this workflow with two inputs and receives a total plus a breakdown.\n\n**Inputs:** category, month\n**Output:** total_spent, breakdown_by_category, matching transactions"
},
"typeVersion": 1
},
{
"id": "446f8bfd-ed98-4d2b-a75d-70e1e264731b",
"name": "Sticky Note 4dc72d23",
"type": "n8n-nodes-base.stickyNote",
"position": [
1360,
448
],
"parameters": {
"color": 7,
"width": 420,
"height": 220,
"content": "## How the inputs work\n- **category**: one of Groceries, Dining, Transport, Utilities, Shopping, Health, Entertainment, Travel, Other, or `all` for every category.\n- **month**: YYYY-MM (for example 2026-06) or `all` for all time.\n\nThe AI passes `all` automatically when the user does not specify one. Using `all` instead of empty values avoids the known n8n schema error with null tool inputs."
},
"typeVersion": 1
},
{
"id": "b4af931b-4bce-4353-bb5e-af3fd0d4bfbf",
"name": "Sticky Note 7d4d6f62",
"type": "n8n-nodes-base.stickyNote",
"position": [
1808,
448
],
"parameters": {
"color": 6,
"width": 440,
"height": 220,
"content": "## BEFORE YOU GO LIVE\n1. Point **Read Expenses** at the **SAME** Google Sheet used by the ingestion workflow (Personal Finance Receipt Logger).\n2. This sub-workflow runs on demand when the MCP hub calls it, so it does NOT need to be activated. Keep it saved; only the MCP Server workflow must be active.\n3. Expected sheet columns: Date, Amount, Currency, Vendor, Category, Note, LoggedAt."
},
"typeVersion": 1
},
{
"id": "3ba52de6-3ba4-431d-b3e7-fce13978ffe1",
"name": "Receive Receipt Photo",
"type": "n8n-nodes-base.telegramTrigger",
"position": [
2528,
256
],
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"typeVersion": 1.3
},
{
"id": "159b9c13-4b68-4192-8c84-e9b2881d9eac",
"name": "Download Photo",
"type": "n8n-nodes-base.telegram",
"position": [
2752,
256
],
"parameters": {
"fileId": "={{ $json.message.photo[$json.message.photo.length - 1].file_id }}",
"resource": "file",
"additionalFields": {}
},
"typeVersion": 1.2
},
{
"id": "f13274a7-aa02-4a51-b0f8-b99ef44dbd74",
"name": "Extract Receipt Data with Gemini",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
2976,
256
],
"parameters": {
"text": "You are a receipt parsing assistant. Look at the receipt image and extract the purchase details. Respond with ONLY a single JSON object and nothing else (no markdown, no code fences). Use exactly these keys: date (format YYYY-MM-DD, the purchase date; if it is missing use today), amount (a number, the grand total), currency (a 3-letter code such as EUR or USD), vendor (the store or merchant name), category (one of: Groceries, Dining, Transport, Utilities, Shopping, Health, Entertainment, Travel, Other), summary (a short 3 to 6 word note). If a value cannot be read, use an empty string for text fields and 0 for amount.",
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-2.5-flash",
"cachedResultName": "gemini-2.5-flash"
},
"options": {
"maxOutputTokens": 400
},
"resource": "image",
"inputType": "binary",
"operation": "analyze"
},
"typeVersion": 1.2
},
{
"id": "0fc10ce8-070e-4125-9a44-9b939d4fc6bf",
"name": "Parse Extracted JSON",
"type": "n8n-nodes-base.code",
"position": [
3200,
256
],
"parameters": {
"jsCode": "const item = $input.first().json;\nlet raw = '';\nif (item && typeof item === 'object') {\n raw = (item.content && item.content.parts && item.content.parts[0] && item.content.parts[0].text) || item.text || item.output || item.response || '';\n if (!raw && Array.isArray(item.candidates)) {\n const c = item.candidates[0];\n raw = (c && c.content && c.content.parts && c.content.parts[0] && c.content.parts[0].text) || '';\n }\n}\nraw = (raw || '').toString();\nlet parsed = {};\ntry {\n parsed = JSON.parse(raw);\n} catch (e) {\n const start = raw.indexOf('{');\n const end = raw.lastIndexOf('}');\n if (start !== -1 && end !== -1 && end > start) {\n try { parsed = JSON.parse(raw.slice(start, end + 1)); } catch (e2) { parsed = {}; }\n }\n}\nconst today = new Date().toISOString().slice(0, 10);\nconst amt = (parsed.amount !== undefined && parsed.amount !== null && parsed.amount !== '') ? parsed.amount : 0;\nreturn [{ json: {\n Date: parsed.date || today,\n Amount: amt,\n Currency: parsed.currency || 'EUR',\n Vendor: parsed.vendor || '',\n Category: parsed.category || 'Other',\n Note: parsed.summary || parsed.note || '',\n LoggedAt: new Date().toISOString()\n} }];"
},
"typeVersion": 2
},
{
"id": "c26942f5-b641-435d-8de2-699100f22f42",
"name": "Append to Expense Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
3424,
256
],
"parameters": {
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"typeVersion": 4.7
},
{
"id": "490b2ab2-1e7a-4fcf-95dc-c64cbb838d5e",
"name": "Confirm in Telegram",
"type": "n8n-nodes-base.telegram",
"position": [
3648,
256
],
"parameters": {
"text": "=Logged: {{ $('Parse Extracted JSON').item.json.Amount }} {{ $('Parse Extracted JSON').item.json.Currency }} at {{ $('Parse Extracted JSON').item.json.Vendor }} ({{ $('Parse Extracted JSON').item.json.Category }}) on {{ $('Parse Extracted JSON').item.json.Date }}",
"chatId": "={{ $('Receive Receipt Photo').item.json.message.chat.id }}",
"additionalFields": {
"appendAttribution": false
}
},
"typeVersion": 1.2
},
{
"id": "61a9855c-07fd-4815-8a75-4af9db405daa",
"name": "Sticky Note 045354c6",
"type": "n8n-nodes-base.stickyNote",
"position": [
2512,
0
],
"parameters": {
"color": 4,
"width": 700,
"height": 180,
"content": "## PERSONAL FINANCE RECEIPT LOGGER\nSnap a photo of any receipt, send it to your Telegram bot, and this workflow reads it with Google Gemini Vision and logs it to Google Sheets, with no typing.\n\n**Flow:** Telegram photo, then download the image, then Gemini extracts amount/date/vendor/category as JSON, then append a row to your sheet, then a confirmation message.\n\n**Free stack:** Telegram (free), Gemini 2.5 Flash (free tier), Google Sheets (free)."
},
"typeVersion": 1
},
{
"id": "86e738a2-6984-4364-a3e7-8279fe4c2fe6",
"name": "Sticky Note 7f8b9c55",
"type": "n8n-nodes-base.stickyNote",
"position": [
3248,
-48
],
"parameters": {
"color": 3,
"width": 480,
"height": 180,
"content": "## SWAP / EXTEND\n- Prefer a database? Replace Google Sheets with Supabase for stronger filtering.\n- Higher OCR accuracy on crumpled receipts? Add OCR.space (free 25k/month) before Gemini, or switch the model to gemini-2.5-pro.\n- Pair this with the **Personal Finance MCP Server** workflow to ask Claude about your spending.\nDocs: see Telegram Trigger, Google Gemini, and Google Sheets on docs.n8n.io."
},
"typeVersion": 1
},
{
"id": "c82f3b49-c8bc-497e-8527-c5c3ed3b50a4",
"name": "Sticky Note c1b7bdd3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2512,
464
],
"parameters": {
"color": 7,
"width": 420,
"height": 200,
"content": "## 1) TELEGRAM SETUP\n- Create a bot with @BotFather and paste the token into the Telegram credential (shared by all Telegram nodes here).\n- Send a photo of a receipt to your bot to start the flow.\n- **Download Photo** grabs the highest resolution image from the message and passes it on as binary (field: data)."
},
"typeVersion": 1
},
{
"id": "7f8778ce-717f-48eb-8219-52c31b09bdea",
"name": "Sticky Note 2c2dbff3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2960,
464
],
"parameters": {
"color": 7,
"width": 440,
"height": 200,
"content": "## 2) GEMINI EXTRACTION\n- **Extract Receipt Data with Gemini** sends the image to gemini-2.5-flash and asks for a strict JSON object.\n- **Parse Extracted JSON** safely parses that JSON (it recovers even if the model adds extra text) and normalizes the columns.\n- Categories: Groceries, Dining, Transport, Utilities, Shopping, Health, Entertainment, Travel, Other."
},
"typeVersion": 1
},
{
"id": "5fac2149-1e43-4f2e-ad32-f6873a178e47",
"name": "Sticky Note 5cb9151a",
"type": "n8n-nodes-base.stickyNote",
"position": [
3408,
464
],
"parameters": {
"color": 6,
"width": 480,
"height": 220,
"content": "## 3) SHEET + PRIVACY\n- Create a Google Sheet with these headers in row 1: Date, Amount, Currency, Vendor, Category, Note, LoggedAt.\n- Point **Append to Expense Sheet** at that sheet. The companion **Query Spending** sub-workflow must read the SAME sheet.\n- **Privacy:** on the Gemini free tier, Google may use your images to improve models. Use a paid Gemini key if your receipts are sensitive."
},
"typeVersion": 1
},
{
"id": "b32607fa-a0e6-431b-a4a4-012e2bfef332",
"name": "Template Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
1184,
-720
],
"parameters": {
"width": 1232,
"height": 604,
"content": "## Personal Finance MCP\n\nA two-part personal finance system: log receipts automatically from Telegram photos into Google Sheets, then ask Claude or ChatGPT about your spending in plain language through an MCP server. Read this note first.\n\n## Who it is for\nAnyone who wants effortless expense tracking with zero typing plus natural-language reporting. Snap a receipt to log it; ask an AI client for totals and breakdowns.\n\n## How it works (3 boxed areas on this canvas)\n1. RECEIPT LOGGER (bottom): send a receipt photo to your Telegram bot, Gemini Vision extracts amount/date/vendor/category as JSON, a row is appended to your Google Sheet, you get a confirmation.\n2. MCP SERVER (top-left): exposes a Query Spending tool to MCP clients (Claude Desktop, ChatGPT, Cursor).\n3. QUERY SPENDING (tool): reads the same sheet and returns the total, a per-category breakdown, and the matching transactions.\n\n## Setup\n1. Create a Google Sheet with row-1 headers: Date, Amount, Currency, Vendor, Category, Note, LoggedAt.\n2. Point BOTH Google Sheets nodes (Read Expenses, Append to Expense Sheet) at THAT sheet. Both halves must use the same sheet.\n3. Create a Telegram bot with @BotFather and add the token to the Telegram credential.\n4. Add a Google Gemini credential.\n5. After import, RE-LINK the Query Spending toolWorkflow node to your own copy of the tool sub-workflow, then save it.\n6. Publish (activate) only the MCP Server workflow, then connect your MCP client. For production, switch the MCP trigger to Bearer Auth.\n\n## Requirements\nn8n. Accounts: Telegram, Google (Sheets + Gemini, free tier OK). An MCP client for queries. No community nodes.\n\n## Privacy\nOn the Gemini free tier, Google may use your images to improve models. Use a paid Gemini key for sensitive receipts."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": true,
"executionOrder": "v1"
},
"versionId": "aeda85ba-c2af-4ebf-968c-84273e011c38",
"nodeGroups": [],
"connections": {
"Read Expenses": {
"main": [
[
{
"node": "Filter and Summarize",
"type": "main",
"index": 0
}
]
]
},
"Download Photo": {
"main": [
[
{
"node": "Extract Receipt Data with Gemini",
"type": "main",
"index": 0
}
]
]
},
"Query Spending": {
"ai_tool": [
[
{
"node": "MCP Server: Personal Finance",
"type": "ai_tool",
"index": 0
}
]
]
},
"Parse Extracted JSON": {
"main": [
[
{
"node": "Append to Expense Sheet",
"type": "main",
"index": 0
}
]
]
},
"Receive Receipt Photo": {
"main": [
[
{
"node": "Download Photo",
"type": "main",
"index": 0
}
]
]
},
"Append to Expense Sheet": {
"main": [
[
{
"node": "Confirm in Telegram",
"type": "main",
"index": 0
}
]
]
},
"Receive Query Parameters": {
"main": [
[
{
"node": "Read Expenses",
"type": "main",
"index": 0
}
]
]
},
"Extract Receipt Data with Gemini": {
"main": [
[
{
"node": "Parse Extracted JSON",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow logs receipt photos from Telegram into Google Sheets using Google Gemini for data extraction, and exposes an MCP server tool that lets clients like Claude or ChatGPT query your spending by month and category. Triggers when you send a receipt photo to your Telegram…
Source: https://n8n.io/workflows/16166/ — 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.
> AI-powered nutrition assistant for Telegram — log meals, set goals, and get personalized daily reports with Google Sheets integration.
This project is a template for building a complete academic virtual assistant using n8n. It connects to Telegram, answers frequently asked questions by querying MongoDB, keeps the community informed a
Arvifund - Supabase. Uses httpRequest, telegram, googleSheets, telegramTrigger. Event-driven trigger; 90 nodes.
Arvifund - Supabase (Fixed v2). Uses httpRequest, telegram, googleSheets, telegramTrigger. Event-driven trigger; 90 nodes.
Arvifund - Supabase (Fixed v4). Uses httpRequest, telegram, googleSheets, telegramTrigger. Event-driven trigger; 90 nodes.