{
  "name": "Query expenses on Telegram with GPT-4.1 and Google Sheets",
  "nodes": [
    {
      "id": "58535881-40c5-4d75-82c6-f0faa9443e1a",
      "name": "MSG | Telegram Inbound",
      "type": "n8n-nodes-base.telegramTrigger",
      "position": [
        2144,
        1088
      ]
    },
    {
      "id": "69f61c14-bd15-4c5a-b0b8-ada463bff839",
      "name": "LLM | Parse Intent",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2880,
        1632
      ]
    },
    {
      "id": "1f1984d4-cc31-4730-8354-69c6ec425bbe",
      "name": "JS | Extract Intent JSON",
      "type": "n8n-nodes-base.code",
      "position": [
        3232,
        1632
      ]
    },
    {
      "id": "4e830963-ede6-4d56-b328-7f3119040143",
      "name": "GS | Load Expenses",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        9824,
        1440
      ]
    },
    {
      "id": "4271bf57-4b8d-4f68-8896-2d55f162161e",
      "name": "GS | Load Categories",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        9824,
        1152
      ]
    },
    {
      "id": "21d72a8f-e9cd-4fac-b832-f0804777d270",
      "name": "JS | Filter & Aggregate",
      "type": "n8n-nodes-base.code",
      "position": [
        10576,
        1264
      ]
    },
    {
      "id": "b24a37e4-d82f-43e6-b343-e91f7f838d19",
      "name": "JS | Format Response Message",
      "type": "n8n-nodes-base.code",
      "position": [
        11056,
        1264
      ]
    },
    {
      "id": "c7dc9c2c-8bc0-4b0a-a988-27a966b3f814",
      "name": "TG | Send Reply",
      "type": "n8n-nodes-base.telegram",
      "position": [
        11312,
        1264
      ]
    },
    {
      "id": "4876799b-af52-4218-80aa-7603258ff084",
      "name": "MERGE | Combine Expenses + Categories",
      "type": "n8n-nodes-base.merge",
      "position": [
        10144,
        1264
      ]
    },
    {
      "id": "db13a254-5fde-42e4-9e82-2060a04451f0",
      "name": "IF | User Authorized?",
      "type": "n8n-nodes-base.if",
      "position": [
        2528,
        1072
      ]
    },
    {
      "id": "7a76b7d6-f533-4785-b822-8d7ac464d883",
      "name": "SPLIT | Split Categories",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        4016,
        768
      ]
    },
    {
      "id": "8e963faf-3079-447d-bfd9-89eff8da207e",
      "name": "GS | Read Category Mapping",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4016,
        1072
      ]
    },
    {
      "id": "e992cd5e-ba4e-4ab5-b885-14c55546d0a3",
      "name": "MERGE | Join Categories with Mapping",
      "type": "n8n-nodes-base.merge",
      "position": [
        4272,
        768
      ]
    },
    {
      "id": "58c889ef-e918-43d4-a7a3-1a47bf890ffc",
      "name": "TG | Confirm Category Suggestion",
      "type": "n8n-nodes-base.telegram",
      "position": [
        6224,
        368
      ]
    },
    {
      "id": "fd9c3d1c-223a-44d8-afbf-4773a11d09bd",
      "name": "GS | Read Allowed Categories",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4464,
        1072
      ]
    },
    {
      "id": "2f1f1de8-cae3-4792-920c-ba490986019e",
      "name": "SET | Normalize Category",
      "type": "n8n-nodes-base.set",
      "position": [
        4464,
        768
      ]
    },
    {
      "id": "7a819214-50e5-460a-bd4d-a546eb68dc20",
      "name": "MERGE | Check Category against Allowed",
      "type": "n8n-nodes-base.merge",
      "position": [
        4720,
        768
      ]
    },
    {
      "id": "ed0de8c5-8c55-49f2-96a0-973b49b95d6a",
      "name": "IF | Category Known?",
      "type": "n8n-nodes-base.if",
      "position": [
        4960,
        768
      ]
    },
    {
      "id": "9ecd305a-5e42-43f1-91fa-21fbb0d89e53",
      "name": "LLM | Classify Category",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        5712,
        368
      ]
    },
    {
      "id": "73085ca0-da81-4f8f-a232-5680c7d88b88",
      "name": "MERGE | Combine Categories with List",
      "type": "n8n-nodes-base.merge",
      "position": [
        5216,
        672
      ]
    },
    {
      "id": "19fe45dc-435b-4e70-9b7a-cd7c0bedd41e",
      "name": "SET | Extract LLM Category",
      "type": "n8n-nodes-base.set",
      "position": [
        5968,
        368
      ]
    },
    {
      "id": "20b22d0e-35bf-476f-9aba-e2abb14f0377",
      "name": "IF | Category Suggestion Accepted?",
      "type": "n8n-nodes-base.if",
      "position": [
        6464,
        368
      ]
    },
    {
      "id": "1d692432-6601-47f1-9bdd-cafd50aeef5c",
      "name": "GS | Save Category Mapping",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        7968,
        368
      ]
    },
    {
      "id": "750a9069-a57b-4b46-8725-42367602aad2",
      "name": "MERGE | Loop Entry (Category)",
      "type": "n8n-nodes-base.merge",
      "position": [
        5712,
        768
      ]
    },
    {
      "id": "20498665-76a1-45b8-978f-e92e0fc19ed1",
      "name": "SET | Set Resolved Category",
      "type": "n8n-nodes-base.set",
      "position": [
        5968,
        768
      ]
    },
    {
      "id": "e6824c4c-a839-40d0-b334-40f19aad19d0",
      "name": "SET | Assemble Resolved Intent",
      "type": "n8n-nodes-base.set",
      "position": [
        9472,
        1264
      ]
    },
    {
      "id": "cc31446d-2ffb-4411-afd7-64e69182b448",
      "name": "AGG | Aggregate Category List",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        4720,
        1072
      ]
    },
    {
      "id": "6f2ace60-279f-40ce-8459-53a13a603748",
      "name": "LOOP | Iterate Categories",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        5472,
        768
      ]
    },
    {
      "id": "165edc36-f6bd-4231-a1e4-bd7c7cd4ecea",
      "name": "SET | Set New+Old Category (LLM)",
      "type": "n8n-nodes-base.set",
      "position": [
        7216,
        368
      ]
    },
    {
      "id": "8294d338-81df-4799-8d83-335699eb38d9",
      "name": "JS | Build Category Inline Buttons",
      "type": "n8n-nodes-base.code",
      "position": [
        6720,
        176
      ]
    },
    {
      "id": "e7a92e17-a097-4e67-8ba0-aa2073aeefb1",
      "name": "WAIT | Wait for Category Selection",
      "type": "n8n-nodes-base.wait",
      "position": [
        7216,
        176
      ]
    },
    {
      "id": "d5c9afc3-2245-4a93-9c66-38edc4c0f4a2",
      "name": "HTTP | Send Category Selection Message",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        6960,
        176
      ]
    },
    {
      "id": "33dba25d-9b17-4da1-8463-92dbad2c0ff3",
      "name": "SET | Read Callback Body (Cat.)",
      "type": "n8n-nodes-base.set",
      "position": [
        7472,
        176
      ]
    },
    {
      "id": "e11d27e9-66ea-4cde-a108-3c38b5fea75b",
      "name": "SET | Set New+Old Category (Selection)",
      "type": "n8n-nodes-base.set",
      "position": [
        7472,
        368
      ]
    },
    {
      "id": "e765fbf0-81a6-4117-9fbf-8c5c4d0f6c90",
      "name": "MERGE | Combine Category Mapping Entries",
      "type": "n8n-nodes-base.merge",
      "position": [
        7712,
        368
      ]
    },
    {
      "id": "dada58e6-2abc-4cec-84c2-4bd92994e796",
      "name": "HTTP | Forward Category Selection",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3312,
        496
      ]
    },
    {
      "id": "76197f7e-8f0f-46d5-84b4-c9c6bb735668",
      "name": "TG | Confirm Category Selection",
      "type": "n8n-nodes-base.telegram",
      "position": [
        3504,
        496
      ]
    },
    {
      "id": "883bf86d-3fac-461a-a9b7-dcc0018eb20f",
      "name": "JS | Read Category Callback",
      "type": "n8n-nodes-base.code",
      "position": [
        3104,
        496
      ]
    },
    {
      "id": "0e0c0fc1-c577-4e31-a62e-2bef9ef22815",
      "name": "IF | Message or Callback?",
      "type": "n8n-nodes-base.if",
      "position": [
        2352,
        1088
      ]
    },
    {
      "id": "4233a9ba-e3a9-49c7-8f07-1d36fa3fec81",
      "name": "IF | Category Present?",
      "type": "n8n-nodes-base.if",
      "position": [
        3824,
        976
      ]
    },
    {
      "id": "449ced93-3de6-4bf4-8e71-cb0acb48a43e",
      "name": "MERGE | Combine Category + Person",
      "type": "n8n-nodes-base.merge",
      "position": [
        8832,
        1248
      ]
    },
    {
      "id": "f98b58b7-483e-495b-95c1-aa5a9fa457c4",
      "name": "AGG | Aggregate Resolved Categories",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        6224,
        768
      ]
    },
    {
      "id": "50f4587d-754a-4bb3-888b-b6e084ee6237",
      "name": "IF | Person Present?",
      "type": "n8n-nodes-base.if",
      "position": [
        3760,
        2096
      ]
    },
    {
      "id": "85f203b8-bdf7-4346-aceb-30a8388dc4cf",
      "name": "SPLIT | Split Persons",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        3968,
        2064
      ]
    },
    {
      "id": "7657a339-c559-4327-bc25-5fda098783f3",
      "name": "MERGE | Join Persons with Mapping",
      "type": "n8n-nodes-base.merge",
      "position": [
        4224,
        2064
      ]
    },
    {
      "id": "286c2311-d948-4dd0-92bb-81231cb895ea",
      "name": "SET | Normalize Person",
      "type": "n8n-nodes-base.set",
      "position": [
        4416,
        2064
      ]
    },
    {
      "id": "fa32c88e-fb58-483a-8159-bc68edbeebcd",
      "name": "MERGE | Check Person against Allowed",
      "type": "n8n-nodes-base.merge",
      "position": [
        4704,
        2048
      ]
    },
    {
      "id": "8ed005b0-aa2a-4052-868a-424d6df8d83e",
      "name": "IF | Person Known?",
      "type": "n8n-nodes-base.if",
      "position": [
        4944,
        2048
      ]
    },
    {
      "id": "2f3cf76a-d852-4479-a91a-4d93bdcb8e28",
      "name": "MERGE | Combine Persons with List",
      "type": "n8n-nodes-base.merge",
      "position": [
        5200,
        2048
      ]
    },
    {
      "id": "9db7a679-849e-46a4-8c12-9c9296e00d4c",
      "name": "AGG | Aggregate Person List",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        4912,
        2352
      ]
    },
    {
      "id": "82d59267-2594-498f-863b-f35116802032",
      "name": "GS | Read Person Mapping",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3968,
        2368
      ]
    },
    {
      "id": "16747ce4-9d18-438d-a8df-df00508908c1",
      "name": "GS | Read Allowed Persons",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4416,
        2368
      ]
    },
    {
      "id": "fa8b6b35-8747-40b8-9639-5131f279c0cb",
      "name": "TG | Confirm Person Suggestion",
      "type": "n8n-nodes-base.telegram",
      "position": [
        6208,
        2448
      ]
    },
    {
      "id": "fa9fc8ee-c15e-4f4f-9af2-8a5dd626d788",
      "name": "LLM | Classify Person",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        5696,
        2448
      ]
    },
    {
      "id": "ef710337-260f-495c-97aa-7ccb5420b7dc",
      "name": "SET | Extract LLM Person",
      "type": "n8n-nodes-base.set",
      "position": [
        5952,
        2448
      ]
    },
    {
      "id": "fd5713f9-9501-420f-9d12-bb8b84fda620",
      "name": "IF | Person Suggestion Accepted?",
      "type": "n8n-nodes-base.if",
      "position": [
        6448,
        2448
      ]
    },
    {
      "id": "262227e8-5564-4caf-a833-edc83bd36894",
      "name": "GS | Save Person Mapping",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        8176,
        1904
      ]
    },
    {
      "id": "e6850bcd-ec85-4a14-ae06-73878206a278",
      "name": "LOOP | Iterate Persons",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        5456,
        2048
      ]
    },
    {
      "id": "741e158c-14ed-4ff1-aba3-27b0f648ab20",
      "name": "SET | Set New+Old Person (LLM)",
      "type": "n8n-nodes-base.set",
      "position": [
        7664,
        2304
      ]
    },
    {
      "id": "41280f1e-9534-4947-908a-41a7caef3397",
      "name": "JS | Build Person Inline Buttons",
      "type": "n8n-nodes-base.code",
      "position": [
        6672,
        1712
      ]
    },
    {
      "id": "5f9c0563-ee99-4147-9081-f499af2bb507",
      "name": "WAIT | Wait for Person Selection",
      "type": "n8n-nodes-base.wait",
      "position": [
        7168,
        1712
      ]
    },
    {
      "id": "5b86154e-57b0-4b2d-a7d7-1e11cc878b67",
      "name": "HTTP | Send Person Selection Message",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        6912,
        1712
      ]
    },
    {
      "id": "b879f1c5-ffb3-45b6-b7f5-1b772b431643",
      "name": "SET | Read Callback Body (Person)",
      "type": "n8n-nodes-base.set",
      "position": [
        7424,
        1712
      ]
    },
    {
      "id": "1a9aa8de-b20f-4cac-9fde-9d889caee19e",
      "name": "SET | Set New+Old Person (Selection)",
      "type": "n8n-nodes-base.set",
      "position": [
        7664,
        1712
      ]
    },
    {
      "id": "205a6d3a-9108-4b8b-9698-5ba8db844f88",
      "name": "MERGE | Combine Person Mapping Entries",
      "type": "n8n-nodes-base.merge",
      "position": [
        7920,
        1904
      ]
    },
    {
      "id": "eaee6930-3da6-40a9-ba0f-84ab8543b451",
      "name": "MERGE | Loop Entry (Person)",
      "type": "n8n-nodes-base.merge",
      "position": [
        5696,
        2256
      ]
    },
    {
      "id": "e2241bb1-eae6-4311-909c-a67bb2bd52fe",
      "name": "SET | Set Resolved Person",
      "type": "n8n-nodes-base.set",
      "position": [
        6208,
        2256
      ]
    },
    {
      "id": "9a37179d-2d3c-48e3-9ce1-1624b626b6fd",
      "name": "MERGE | Combine Person Mapping",
      "type": "n8n-nodes-base.merge",
      "position": [
        6448,
        2048
      ]
    },
    {
      "id": "183d2e80-d18b-403e-a3f0-86d41fd0c0b1",
      "name": "IF | Category or Person Callback?",
      "type": "n8n-nodes-base.if",
      "position": [
        2816,
        576
      ]
    },
    {
      "id": "0ac9e7b3-a794-4a42-b48b-1d42ba317bbc",
      "name": "HTTP | Forward Person Selection",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3312,
        688
      ]
    },
    {
      "id": "36db9da8-b32c-4b48-b7d4-6871eed49cb0",
      "name": "TG | Confirm Person Selection",
      "type": "n8n-nodes-base.telegram",
      "position": [
        3504,
        688
      ]
    },
    {
      "id": "17fceb64-ac93-42b7-af34-fc5f1d0cb3d2",
      "name": "JS | Read Person Callback",
      "type": "n8n-nodes-base.code",
      "position": [
        3104,
        688
      ]
    },
    {
      "id": "a742d127-062b-4f7d-8c4d-0b089439508e",
      "name": "JS | Merge Intent Fields",
      "type": "n8n-nodes-base.code",
      "position": [
        9168,
        1264
      ]
    },
    {
      "id": "f4353b36-c5f3-4e13-baf8-a257e81fbf43",
      "name": "StickyNote_Layer1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1968,
        0
      ],
      "parameters": {
        "width": 1740,
        "height": 1288,
        "content": "## 🟢 LAYER 1 — INPUT\nReceives incoming Telegram messages & callbacks.\nAuthorizes users and routes by message type.\n\n---\n\n⚙️ **ACTION REQUIRED**\n\n**1. Connect Telegram Credential**\nOpen the `MSG | Tele"
      }
    },
    {
      "id": "b6684ba8-b5ab-41fe-8c1c-9ec20255983d",
      "name": "StickyNote_Layer2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1968,
        1312
      ],
      "parameters": {
        "width": 1730,
        "height": 1284,
        "content": "## 🔵 LAYER 2 — INTENT PARSING\n\n---\n\n⚙️ **ACTION REQUIRED**\n\n**1. Connect OpenAI Credential**\nOpen the `LLM | Parse Intent` node and connect your OpenAI API credential."
      }
    },
    {
      "id": "c6ddda36-1b63-428b-932d-2d9c857aa12d",
      "name": "StickyNote_Layer3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3712,
        0
      ],
      "parameters": {
        "width": 4970,
        "height": 1296,
        "content": "## 🟡 LAYER 3a — ENTITY RESOLUTION: CATEGORIES\nResolves raw category strings to canonical names via mapping sheets.\nUnknown entities → LLM classification → user confirmation → alias saved.\n\n---\n\n⚙️ **A"
      }
    },
    {
      "id": "90172522-bfdb-4fa7-8ef3-48c2ee914994",
      "name": "StickyNote_Layer4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8688,
        0
      ],
      "parameters": {
        "width": 1712,
        "height": 2592,
        "content": "## 🟠 LAYER 4 — QUERY ENGINE\nLoads expense data from Google Sheets.\nMerges resolved intent with raw data for filtering.\n\n---\n\n⚙️ **ACTION REQUIRED**\n\n**1. Connect your `expenses` Google Sheet**\nOpen `G"
      }
    },
    {
      "id": "4e94aa93-bca3-4087-b52c-de55b3f7be23",
      "name": "StickyNote_Layer5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        10400,
        0
      ],
      "parameters": {
        "width": 564,
        "height": 2592,
        "content": "## 🔴 LAYER 5 — ANALYTICS & AGGREGATION\nApplies date, person, category and common_only filters.\nComputes totals, category breakdown and person breakdown.\n\n---\n\n⚙️ **ACTION REQUIRED**\n\nNo setup required"
      }
    },
    {
      "id": "a566ae23-972c-4c15-aa4d-b2ea0e14c8f4",
      "name": "StickyNote_Layer6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        10976,
        0
      ],
      "parameters": {
        "width": 560,
        "height": 2592,
        "content": "## 🟣 LAYER 6 — RESPONSE\nFormats the final message and sends it back via Telegram.\n\n---\n\n⚙️ **ACTION REQUIRED**\n\n**1. Connect Telegram Credential**\nOpen `TG | Send Reply` and connect your Telegram Bot "
      }
    },
    {
      "id": "ed4de5df-0c7f-49e6-9580-9bcec3a29756",
      "name": "StickyNote_Layer",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3712,
        1296
      ],
      "parameters": {
        "width": 4970,
        "height": 1296,
        "content": "## 🟡 LAYER 3b — ENTITY RESOLUTION: PERSONS\n\n\n---\n\n⚙️ **ACTION REQUIRED**\n\n**1. Connect your `person_mapping` Google Sheet**\nOpen `GS | Read Person Mapping` and `GS | Save Person Mapping`.\nReplace `YOU"
      }
    },
    {
      "id": "453a9f82-a297-4fb6-adc1-08f2e5619986",
      "name": "DOC | Layer 1 — Input",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1968,
        464
      ],
      "parameters": {
        "width": 540,
        "height": 500,
        "content": "\n\n**Purpose:** Entry point of the workflow. Receives all incoming Telegram events and routes them to the correct processing path.\n\n**What happens here:**\n- `MSG | Telegram Inbound` — Listens for incom"
      }
    },
    {
      "id": "a1283c4a-bad8-4116-a97c-0d2bbaa0f5ef",
      "name": "DOC | Layer 2 — Intent Parsing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1984,
        1664
      ],
      "parameters": {
        "width": 412,
        "height": 392,
        "content": "\n**Purpose:** Converts the user's free-text message into a structured JSON object that all downstream layers can work with.\n\n**What happens here:**\n- `LLM | Parse Intent` — Sends the raw message to GP"
      }
    },
    {
      "id": "f593b6d4-1dce-475b-928d-7c9850bda508",
      "name": "DOC | Layer 3a — Entity Resolution: Categories",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4384,
        128
      ],
      "parameters": {
        "width": 540,
        "height": 644,
        "content": "\n\n**Purpose:** Resolves raw category strings from the intent into valid canonical category names as defined in the categories master sheet.\n\n**What happens here:**\n- `IF | Category Present?` — Skips t"
      }
    },
    {
      "id": "1b9d5cb8-c8b9-4afd-919f-ef51ffbd2109",
      "name": "DOC | Layer 3b — Entity Resolution: Persons",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4448,
        1376
      ],
      "parameters": {
        "width": 572,
        "height": 556,
        "content": "**Purpose:** Mirrors Layer 3a but for person names — resolves nicknames and unknown names to canonical person records.\n\n**What happens here:**\n- `IF | Person Present?` — Skips the branch entirely when"
      }
    },
    {
      "id": "c7f0bb66-e951-4962-ac01-999e805c887c",
      "name": "DOC | Layer 4 — Query Engine",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8704,
        416
      ],
      "parameters": {
        "width": 524,
        "height": 428,
        "content": "\n**Purpose:** Assembles the fully resolved intent and loads raw expense data from Google Sheets, ready for filtering.\n\n**What happens here:**\n- `SET | Assemble Resolved Intent` — Merges canonical cate"
      }
    },
    {
      "id": "4f17b844-0e95-469e-a6b3-b33edf5b96f7",
      "name": "DOC | Layer 5 — Analytics & Aggregation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        10448,
        480
      ],
      "parameters": {
        "width": 348,
        "height": 340,
        "content": "\n**Purpose:** Applies all filters from the resolved intent to the expense data and computes the requested aggregations.\n\n**What happens here:**\n- `JS | Filter & Aggregate` — Single code node that does"
      }
    },
    {
      "id": "6d8d1b8c-93db-4374-9240-b01174d354d5",
      "name": "DOC | Layer 6 — Response",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        11008,
        128
      ],
      "parameters": {
        "width": 460,
        "height": 308,
        "content": "**Purpose:** Formats the aggregation result into a human-readable Telegram message and delivers it back to the user.\n\n**What happens here:**\n- `JS | Format Response Message` — Builds the final message"
      }
    },
    {
      "id": "9c55a9fc-45aa-4ac4-a94f-7736abee4e73",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1008,
        0
      ],
      "parameters": {
        "width": 912,
        "height": 2576,
        "content": "# 🔄 WORKFLOW OVERVIEW\n\n## 🟩 Input & Security (Layer 1)\n\nEvery incoming Telegram message or button tap is received by the trigger.\n\nThe workflow checks whether the sender's **Chat ID** is on the approv"
      }
    },
    {
      "id": "14b52ff0-e0db-42be-8f44-066315ba7642",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        0
      ],
      "parameters": {
        "width": 1024,
        "height": 2576,
        "content": "# HOW IT WORKS — AI TELEGRAM EXPENSE TRACKER (QUERY MODE)\n\nThis workflow answers natural language expense questions from Telegram using AI.\n\nInstead of opening spreadsheets, simply send a message like"
      }
    }
  ],
  "connections": {
    "GS | Load Expenses": {
      "main": [
        [
          {
            "node": "MERGE | Combine Expenses + Categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF | Person Known?": {
      "main": [
        [
          {
            "node": "MERGE | Combine Persons with List",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "MERGE | Loop Entry (Person)",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "LLM | Parse Intent": {
      "main": [
        [
          {
            "node": "JS | Extract Intent JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GS | Load Categories": {
      "main": [
        [
          {
            "node": "MERGE | Combine Expenses + Categories",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "IF | Category Known?": {
      "main": [
        [
          {
            "node": "MERGE | Combine Categories with List",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "MERGE | Loop Entry (Category)",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "IF | Person Present?": {
      "main": [
        [
          {
            "node": "MERGE | Combine Person Mapping",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "SPLIT | Split Persons",
            "type": "main",
            "index": 0
          },
          {
            "node": "GS | Read Person Mapping",
            "type": "main",
            "index": 0
          },
          {
            "node": "GS | Read Allowed Persons",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF | User Authorized?": {
      "main": [
        [
          {
            "node": "LLM | Parse Intent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM | Classify Person": {
      "main": [
        [
          {
            "node": "SET | Extract LLM Person",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SPLIT | Split Persons": {
      "main": [
        [
          {
            "node": "MERGE | Join Persons with Mapping",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF | Category Present?": {
      "main": [
        [
          {
            "node": "MERGE | Combine Category + Person",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "SPLIT | Split Categories",
            "type": "main",
            "index": 0
          },
          {
            "node": "GS | Read Category Mapping",
            "type": "main",
            "index": 0
          },
          {
            "node": "GS | Read Allowed Categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LOOP | Iterate Persons": {
      "main": [
        [
          {
            "node": "MERGE | Loop Entry (Person)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "LLM | Classify Person",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MSG | Telegram Inbound": {
      "main": [
        [
          {
            "node": "IF | Message or Callback?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Normalize Person": {
      "main": [
        [
          {
            "node": "MERGE | Check Person against Allowed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS | Filter & Aggregate": {
      "main": [
        [
          {
            "node": "JS | Format Response Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM | Classify Category": {
      "main": [
        [
          {
            "node": "SET | Extract LLM Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GS | Read Person Mapping": {
      "main": [
        [
          {
            "node": "MERGE | Join Persons with Mapping",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "GS | Save Person Mapping": {
      "main": [
        [
          {
            "node": "LOOP | Iterate Persons",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS | Extract Intent JSON": {
      "main": [
        [
          {
            "node": "IF | Category Present?",
            "type": "main",
            "index": 0
          },
          {
            "node": "IF | Person Present?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS | Merge Intent Fields": {
      "main": [
        [
          {
            "node": "SET | Assemble Resolved Intent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Extract LLM Person": {
      "main": [
        [
          {
            "node": "TG | Confirm Person Suggestion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Normalize Category": {
      "main": [
        [
          {
            "node": "MERGE | Check Category against Allowed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SPLIT | Split Categories": {
      "main": [
        [
          {
            "node": "MERGE | Join Categories with Mapping",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GS | Read Allowed Persons": {
      "main": [
        [
          {
            "node": "MERGE | Check Person against Allowed",
            "type": "main",
            "index": 1
          },
          {
            "node": "AGG | Aggregate Person List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF | Message or Callback?": {
      "main": [
        [
          {
            "node": "IF | User Authorized?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "IF | Category or Person Callback?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS | Read Person Callback": {
      "main": [
        [
          {
            "node": "HTTP | Forward Person Selection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LOOP | Iterate Categories": {
      "main": [
        [
          {
            "node": "MERGE | Loop Entry (Category)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "LLM | Classify Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Set Resolved Person": {
      "main": [
        [
          {
            "node": "MERGE | Combine Person Mapping",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "GS | Read Category Mapping": {
      "main": [
        [
          {
            "node": "MERGE | Join Categories with Mapping",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "GS | Save Category Mapping": {
      "main": [
        [
          {
            "node": "LOOP | Iterate Categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Extract LLM Category": {
      "main": [
        [
          {
            "node": "TG | Confirm Category Suggestion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AGG | Aggregate Person List": {
      "main": [
        [
          {
            "node": "MERGE | Combine Persons with List",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "JS | Read Category Callback": {
      "main": [
        [
          {
            "node": "HTTP | Forward Category Selection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Loop Entry (Person)": {
      "main": [
        [
          {
            "node": "SET | Set Resolved Person",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Set Resolved Category": {
      "main": [
        [
          {
            "node": "AGG | Aggregate Resolved Categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GS | Read Allowed Categories": {
      "main": [
        [
          {
            "node": "MERGE | Check Category against Allowed",
            "type": "main",
            "index": 1
          },
          {
            "node": "AGG | Aggregate Category List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS | Format Response Message": {
      "main": [
        [
          {
            "node": "TG | Send Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AGG | Aggregate Category List": {
      "main": [
        [
          {
            "node": "MERGE | Combine Categories with List",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "MERGE | Loop Entry (Category)": {
      "main": [
        [
          {
            "node": "SET | Set Resolved Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Combine Person Mapping": {
      "main": [
        [
          {
            "node": "MERGE | Combine Category + Person",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "SET | Assemble Resolved Intent": {
      "main": [
        [
          {
            "node": "GS | Load Expenses",
            "type": "main",
            "index": 0
          },
          {
            "node": "GS | Load Categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Set New+Old Person (LLM)": {
      "main": [
        [
          {
            "node": "MERGE | Combine Person Mapping Entries",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "TG | Confirm Person Suggestion": {
      "main": [
        [
          {
            "node": "IF | Person Suggestion Accepted?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP | Forward Person Selection": {
      "main": [
        [
          {
            "node": "TG | Confirm Person Selection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Read Callback Body (Cat.)": {
      "main": [
        [
          {
            "node": "SET | Set New+Old Category (Selection)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF | Person Suggestion Accepted?": {
      "main": [
        [
          {
            "node": "SET | Set New+Old Person (LLM)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "JS | Build Person Inline Buttons",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS | Build Person Inline Buttons": {
      "main": [
        [
          {
            "node": "HTTP | Send Person Selection Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Set New+Old Category (LLM)": {
      "main": [
        [
          {
            "node": "MERGE | Combine Category Mapping Entries",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "TG | Confirm Category Suggestion": {
      "main": [
        [
          {
            "node": "IF | Category Suggestion Accepted?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WAIT | Wait for Person Selection": {
      "main": [
        [
          {
            "node": "SET | Read Callback Body (Person)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP | Forward Category Selection": {
      "main": [
        [
          {
            "node": "TG | Confirm Category Selection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF | Category or Person Callback?": {
      "main": [
        [
          {
            "node": "JS | Read Category Callback",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "JS | Read Person Callback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Combine Category + Person": {
      "main": [
        [
          {
            "node": "JS | Merge Intent Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Combine Persons with List": {
      "main": [
        [
          {
            "node": "LOOP | Iterate Persons",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Join Persons with Mapping": {
      "main": [
        [
          {
            "node": "SET | Normalize Person",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Read Callback Body (Person)": {
      "main": [
        [
          {
            "node": "SET | Set New+Old Person (Selection)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF | Category Suggestion Accepted?": {
      "main": [
        [
          {
            "node": "SET | Set New+Old Category (LLM)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "JS | Build Category Inline Buttons",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS | Build Category Inline Buttons": {
      "main": [
        [
          {
            "node": "HTTP | Send Category Selection Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WAIT | Wait for Category Selection": {
      "main": [
        [
          {
            "node": "SET | Read Callback Body (Cat.)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AGG | Aggregate Resolved Categories": {
      "main": [
        [
          {
            "node": "MERGE | Combine Category + Person",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "HTTP | Send Person Selection Message": {
      "main": [
        [
          {
            "node": "WAIT | Wait for Person Selection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Check Person against Allowed": {
      "main": [
        [
          {
            "node": "IF | Person Known?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Combine Categories with List": {
      "main": [
        [
          {
            "node": "LOOP | Iterate Categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Join Categories with Mapping": {
      "main": [
        [
          {
            "node": "SET | Normalize Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Set New+Old Person (Selection)": {
      "main": [
        [
          {
            "node": "MERGE | Combine Person Mapping Entries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Combine Expenses + Categories": {
      "main": [
        [
          {
            "node": "JS | Filter & Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP | Send Category Selection Message": {
      "main": [
        [
          {
            "node": "WAIT | Wait for Category Selection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Check Category against Allowed": {
      "main": [
        [
          {
            "node": "IF | Category Known?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Combine Person Mapping Entries": {
      "main": [
        [
          {
            "node": "GS | Save Person Mapping",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET | Set New+Old Category (Selection)": {
      "main": [
        [
          {
            "node": "MERGE | Combine Category Mapping Entries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE | Combine Category Mapping Entries": {
      "main": [
        [
          {
            "node": "GS | Save Category Mapping",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}