AutomationFlowsSlack & Telegram › Telegram Bot Dynamic Menu System

Telegram Bot Dynamic Menu System

Original n8n title: Create a Dynamic Telegram Bot Menu System with Multi-level Navigation

ByRuslan Elishev @relishev on n8n.io

Ever wanted to build a Telegram bot with professional-looking menus that actually work? This n8n workflow creates an interactive bot with dynamic inline keyboards, multi-level navigation, and smart button routing - all without writing complex code from scratch.

Event trigger★★★★☆ complexity28 nodesTelegram TriggerHTTP Request
Slack & Telegram Trigger: Event Nodes: 28 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #8844 — we link there as the canonical source.

This workflow follows the HTTP Request → Telegram Trigger 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 →

Download .json
{
  "nodes": [
    {
      "id": "34f248f6-ef2c-4aee-a328-03b92aa39ff1",
      "name": "\ud83d\udcda REAL EXAMPLES",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5536,
        752
      ],
      "parameters": {
        "color": 5,
        "width": 660,
        "height": 1796,
        "content": "# \ud83d\udcda REAL EXAMPLES TO COPY\n\n## Example 1: Add a Contact Menu\n\n**In Load Menu Config, add:**\n```javascript\n  contact: {\n    text: '\ud83d\udcde <b>Contact Us</b>\\n\\nHow can we help?',\n    keyboard: [\n      [{ text: '\ud83d\udce7 Email', callback_data: 'email' }],\n      [{ text: '\ud83d\udcac Chat', callback_data: 'chat' }],\n      [{ text: '\ud83d\udcf1 Phone', callback_data: 'phone' }],\n      [{ text: '\ud83d\udd19 Back', callback_data: 'main' }]\n    ]\n  },\n  \n  email: {\n    text: '\ud83d\udce7 Email: support@example.com',\n    keyboard: [[{ text: '\ud83d\udd19 Back', callback_data: 'contact' }]]\n  },\n```\n\n**In Main Menu, add button:**\n```javascript\n{ text: '\ud83d\udcde Contact', callback_data: 'contact' }\n```\n\n---\n\n## Example 2: Add Subscription Status\n\n**Step 1 - Add to actionCommands:**\n```javascript\n'check_subscription',\n```\n\n**Step 2 - In Action Router expression:**\n```javascript\n$json.command === 'check_subscription' ? 6 :\n```\n\n**Step 3 - In Default Handler:**\n```javascript\nif (input.command === 'check_subscription') {\n  input.customData = {\n    status: 'Premium User',\n    expires: '30 days'\n  };\n}\n```\n\n**Step 4 - Menu with placeholder:**\n```javascript\n  subscription: {\n    text: 'Status: {status}\\nExpires: {expires}',\n    keyboard: [[{ text: '\ud83d\udd19', callback_data: 'main' }]]\n  },\n```\n\n---\n\n## Example 3: Multi-Level Settings\n\n```javascript\n  settings: {\n    text: '\u2699\ufe0f Settings',\n    keyboard: [\n      [{ text: '\ud83d\udd14 Notifications', callback_data: 'notif_menu' }],\n      [{ text: '\ud83c\udf0d Language', callback_data: 'lang_menu' }],\n      [{ text: '\ud83d\udd19 Main', callback_data: 'main' }]\n    ]\n  },\n  \n  notif_menu: {\n    text: '\ud83d\udd14 Notifications',\n    keyboard: [\n      [{ text: '\u2705 On', callback_data: 'notif_on' }],\n      [{ text: '\u274c Off', callback_data: 'notif_off' }],\n      [{ text: '\ud83d\udd19 Settings', callback_data: 'settings' }]\n    ]\n  },\n```\n\n## Copy & Modify These!\nJust change the text and callback_data values"
      },
      "typeVersion": 1
    },
    {
      "id": "90319918-ce1a-4e45-9be5-d33552518205",
      "name": "Telegram Trigger4",
      "type": "n8n-nodes-base.telegramTrigger",
      "position": [
        4816,
        448
      ],
      "parameters": {
        "updates": [
          "message",
          "callback_query"
        ],
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "0b08989c-6516-4df1-8566-640dc5ed8f92",
      "name": "Load Menu Config2",
      "type": "n8n-nodes-base.function",
      "position": [
        5104,
        352
      ],
      "parameters": {
        "functionCode": "// YOUR_AWS_SECRET_KEY_HERE\n// \ud83d\udccb MENU CONFIGURATION - PURE NAVIGATION\n// YOUR_AWS_SECRET_KEY_HERE\n// This is the ONLY place where menus are defined\n// Structure: Simple navigation tree with no business logic\n// \n// HOW TO ADD A NEW MENU:\n// 1. Add menu key to MENU_CONFIG object\n// 2. Define 'text' - what user sees (supports HTML)\n// 3. Define 'keyboard' - array of button rows\n// 4. Each button needs 'text' (label) and 'callback_data' (routing key)\n// 5. Use callback_data for navigation or action routing\n// YOUR_AWS_SECRET_KEY_HERE\n\nconst data = items[0].json;\n\nconsole.log('\ud83d\udccb Loading menu structure...');\n\n// YOUR_AWS_SECRET_KEY_HERE\n// MENU_CONFIG: Central menu definition\n// - Key = routing identifier (used in callback_data)\n// - Value = menu object with text and keyboard\n// YOUR_AWS_SECRET_KEY_HERE\nconst MENU_CONFIG = {\n  \n  // ==================\n  // MAIN MENU - Entry point (Level 0)\n  // ==================\n  // This is where users land on /start\n  main: {\n    text: '\ud83c\udfe0 <b>MAIN MENU</b>\\n\\nSelect an option:',\n    keyboard: [\n      // Row 1: Feature menus\n      [\n        { text: '\ud83d\udcc1 Menu 1', callback_data: 'menu1' },  // Navigate to menu1\n        { text: '\ud83d\udcc1 Menu 2', callback_data: 'menu2' }   // Navigate to menu2\n      ],\n      // Row 2: More features\n      [\n        { text: '\ud83d\udcc1 Menu 3', callback_data: 'menu3' },  // Navigate to menu3\n        { text: '\ud83d\udcc1 Menu 4', callback_data: 'menu4' }   // Navigate to menu4\n      ],\n      // Row 3: System menus\n      [\n        { text: '\u2699\ufe0f Settings', callback_data: 'settings' },  // Settings menu\n        { text: '\u2753 Help', callback_data: 'help' }           // Help menu\n      ]\n    ]\n  },\n  \n  // ==================\n  // MENU 1 - Submenu example (Level 1)\n  // ==================\n  // Shows how to create actions and nested submenus\n  menu1: {\n    text: '\ud83d\udcc1 <b>MENU 1</b>\\n\\nChoose action:',\n    keyboard: [\n      // Individual action buttons\n      [{ text: '\ud83d\udd38 Action 1.1', callback_data: 'action_1_1' }],  // Triggers action\n      [{ text: '\ud83d\udd38 Action 1.2', callback_data: 'action_1_2' }],  // Triggers action\n      // Navigate to deeper submenu\n      [{ text: '\ud83d\udcc2 Submenu 1.3', callback_data: 'submenu_1_3' }], // Goes deeper\n      // IMPORTANT: Always include navigation back\n      [{ text: '\ud83d\udd19 Back', callback_data: 'main' }]  // Returns to main menu\n    ]\n  },\n  \n  // Action endpoints - These trigger business logic in handlers\n  // callback_data will be routed to appropriate handler\n  action_1_1: {\n    text: '\u2705 Action 1.1 Selected',\n    keyboard: [[{ text: '\ud83d\udd19 Back to Menu 1', callback_data: 'menu1' }]]\n  },\n  \n  action_1_2: {\n    text: '\u2705 Action 1.2 Selected',\n    keyboard: [[{ text: '\ud83d\udd19 Back to Menu 1', callback_data: 'menu1' }]]\n  },\n  \n  // Submenu example (Level 2) - Can go infinitely deep\n  submenu_1_3: {\n    text: '\ud83d\udcc2 <b>SUBMENU 1.3</b>\\n\\nDeeper level:',\n    keyboard: [\n      [{ text: '\u25ab\ufe0f Option A', callback_data: 'option_a' }],     // Action\n      [{ text: '\u25ab\ufe0f Option B', callback_data: 'option_b' }],     // Action\n      [{ text: '\ud83d\udd19 Back to Menu 1', callback_data: 'menu1' }]  // Navigate up\n    ]\n  },\n  \n  option_a: {\n    text: '\u2705 Option A Selected',\n    keyboard: [[{ text: '\ud83d\udd19 Back', callback_data: 'submenu_1_3' }]]\n  },\n  \n  option_b: {\n    text: '\u2705 Option B Selected',\n    keyboard: [[{ text: '\ud83d\udd19 Back', callback_data: 'submenu_1_3' }]]\n  },\n  \n  // ==================\n  // MENU 2 - Level 1\n  // ==================\n  menu2: {\n    text: '\ud83d\udcc1 <b>MENU 2</b>\\n\\nSelect option:',\n    keyboard: [\n      [{ text: '\ud83d\udd39 Action 2.1', callback_data: 'action_2_1' }],\n      [{ text: '\ud83d\udd39 Action 2.2', callback_data: 'action_2_2' }],\n      [{ text: '\ud83d\udd39 Action 2.3', callback_data: 'action_2_3' }],\n      [{ text: '\ud83d\udd19 Main Menu', callback_data: 'main' }]\n    ]\n  },\n  \n  action_2_1: {\n    text: '\u2705 Action 2.1 Triggered',\n    keyboard: [[{ text: '\ud83d\udd19 Back', callback_data: 'menu2' }]]\n  },\n  \n  action_2_2: {\n    text: '\u2705 Action 2.2 Triggered',\n    keyboard: [[{ text: '\ud83d\udd19 Back', callback_data: 'menu2' }]]\n  },\n  \n  action_2_3: {\n    text: '\u2705 Action 2.3 Triggered',\n    keyboard: [[{ text: '\ud83d\udd19 Back', callback_data: 'menu2' }]]\n  },\n  \n  // ==================\n  // MENU 3 - Level 1\n  // ==================\n  menu3: {\n    text: '\ud83d\udcc1 <b>MENU 3</b>\\n\\nPick one:',\n    keyboard: [\n      [\n        { text: '1\ufe0f\u20e3', callback_data: 'opt_1' },\n        { text: '2\ufe0f\u20e3', callback_data: 'opt_2' },\n        { text: '3\ufe0f\u20e3', callback_data: 'opt_3' },\n        { text: '4\ufe0f\u20e3', callback_data: 'opt_4' },\n        { text: '5\ufe0f\u20e3', callback_data: 'opt_5' }\n      ],\n      [{ text: '\ud83d\udd19 Main', callback_data: 'main' }]\n    ]\n  },\n  \n  opt_1: { text: '\u2705 Option 1 Selected', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'menu3' }]] },\n  opt_2: { text: '\u2705 Option 2 Selected', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'menu3' }]] },\n  opt_3: { text: '\u2705 Option 3 Selected', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'menu3' }]] },\n  opt_4: { text: '\u2705 Option 4 Selected', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'menu3' }]] },\n  opt_5: { text: '\u2705 Option 5 Selected', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'menu3' }]] },\n  \n  // ==================\n  // MENU 4 - Level 1\n  // ==================\n  menu4: {\n    text: '\ud83d\udcc1 <b>MENU 4</b>',\n    keyboard: [\n      [{ text: '\ud83d\udccb List Items', callback_data: 'list' }],\n      [{ text: '\u2795 Add Item', callback_data: 'add' }],\n      [{ text: '\u270f\ufe0f Edit Item', callback_data: 'edit' }],\n      [{ text: '\ud83d\uddd1 Delete Item', callback_data: 'delete' }],\n      [{ text: '\ud83d\udd19 Main', callback_data: 'main' }]\n    ]\n  },\n  \n  list: { text: '\ud83d\udccb Listing items...', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'menu4' }]] },\n  add: { text: '\u2795 Adding item...', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'menu4' }]] },\n  edit: { text: '\u270f\ufe0f Editing item...', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'menu4' }]] },\n  delete: { text: '\ud83d\uddd1 Deleting item...', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'menu4' }]] },\n  \n  // ==================\n  // SETTINGS - Level 1\n  // ==================\n  settings: {\n    text: '\u2699\ufe0f <b>SETTINGS</b>',\n    keyboard: [\n      [{ text: '\ud83d\udd14 Notifications', callback_data: 'notif' }],\n      [{ text: '\ud83c\udf0d Language', callback_data: 'lang' }],\n      [{ text: '\ud83d\udc64 Profile', callback_data: 'profile' }],\n      [{ text: '\ud83d\udd10 Privacy', callback_data: 'privacy' }],\n      [{ text: '\ud83d\udd19 Main', callback_data: 'main' }]\n    ]\n  },\n  \n  notif: {\n    text: '\ud83d\udd14 Notification Settings',\n    keyboard: [\n      [{ text: 'Enable All', callback_data: 'notif_on' }],\n      [{ text: 'Disable All', callback_data: 'notif_off' }],\n      [{ text: '\ud83d\udd19 Settings', callback_data: 'settings' }]\n    ]\n  },\n  \n  notif_on: { text: '\u2705 Notifications Enabled', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'notif' }]] },\n  notif_off: { text: '\u274c Notifications Disabled', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'notif' }]] },\n  \n  lang: {\n    text: '\ud83c\udf0d Select Language',\n    keyboard: [\n      [{ text: '\ud83c\uddec\ud83c\udde7 English', callback_data: 'lang_en' }],\n      [{ text: '\ud83c\uddea\ud83c\uddf8 Espa\u00f1ol', callback_data: 'lang_es' }],\n      [{ text: '\ud83c\uddeb\ud83c\uddf7 Fran\u00e7ais', callback_data: 'lang_fr' }],\n      [{ text: '\ud83d\udd19 Settings', callback_data: 'settings' }]\n    ]\n  },\n  \n  lang_en: { text: '\u2705 English Selected', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'lang' }]] },\n  lang_es: { text: '\u2705 Espa\u00f1ol Seleccionado', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'lang' }]] },\n  lang_fr: { text: '\u2705 Fran\u00e7ais S\u00e9lectionn\u00e9', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'lang' }]] },\n  \n  profile: { text: '\ud83d\udc64 Profile Settings', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'settings' }]] },\n  privacy: { text: '\ud83d\udd10 Privacy Settings', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'settings' }]] },\n  \n  // ==================\n  // HELP - Level 1\n  // ==================\n  help: {\n    text: '\u2753 <b>HELP</b>',\n    keyboard: [\n      [{ text: '\ud83d\udcd6 Guide', callback_data: 'guide' }],\n      [{ text: '\u2753 FAQ', callback_data: 'faq' }],\n      [{ text: '\ud83d\udcde Contact', callback_data: 'contact' }],\n      [{ text: '\ud83d\udd19 Main', callback_data: 'main' }]\n    ]\n  },\n  \n  guide: { text: '\ud83d\udcd6 User Guide', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'help' }]] },\n  faq: { text: '\u2753 FAQ', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'help' }]] },\n  contact: { text: '\ud83d\udcde Contact Support', keyboard: [[{ text: '\ud83d\udd19', callback_data: 'help' }]] },\n  \n  // ==================\n  // DEFAULT FALLBACK - IMPORTANT!\n  // ==================\n  // This menu is shown when callback_data doesn't match any menu key\n  default: {\n    text: '\u2753 Unknown selection',\n    keyboard: [[{ text: '\ud83c\udfe0 Main Menu', callback_data: 'main' }]]\n  }\n};\n\n// YOUR_AWS_SECRET_KEY_HERE\n// MENU STRUCTURE RULES:\n// 1. Each menu MUST have 'text' and 'keyboard'\n// 2. Text supports HTML: <b>bold</b>, <i>italic</i>, <code>code</code>\n// 3. Use \\n for line breaks in text\n// 4. Keyboard is array of arrays (rows of buttons)\n// 5. Each button MUST have 'text' (display) and 'callback_data' (routing)\n// 6. callback_data should match a menu key or be handled by router\n// 7. Always provide navigation back to parent menu\n// YOUR_AWS_SECRET_KEY_HERE\n\nconsole.log(`\u2705 Menu structure loaded: ${Object.keys(MENU_CONFIG).length} menus`);\n\n// Pass menu config with original data\nreturn [{\n  json: {\n    ...data,\n    menuConfig: MENU_CONFIG\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "bf40de58-d392-4d6c-9401-1e96a0a13780",
      "name": "Extract Command4",
      "type": "n8n-nodes-base.function",
      "position": [
        5104,
        560
      ],
      "parameters": {
        "functionCode": "// Extract command and user data\n\nconst data = items[0].json;\nlet command = '';\nlet userName = '';\nlet userId = '';\nlet chatId = '';\nlet messageId = '';\nlet isCallback = false;\n\n// Extract data\nif (data.message) {\n  const message = data.message;\n  chatId = message.chat.id;\n  userId = message.from.id;\n  userName = message.from.first_name || 'User';\n  \n  const text = (message.text || '').toLowerCase();\n  if (text.startsWith('/start')) command = 'main';\n  else if (text.includes('help')) command = 'help';\n  else if (text.includes('settings')) command = 'settings';\n  else if (text.includes('stats')) command = 'stats';\n  else if (text.includes('feedback')) command = 'feedback';\n  else command = 'main';\n  \n} else if (data.callback_query) {\n  const callback = data.callback_query;\n  chatId = callback.message.chat.id;\n  messageId = callback.message.message_id;\n  userId = callback.from.id;\n  userName = callback.from.first_name || 'User';\n  isCallback = true;\n  command = callback.data;\n}\n\nconsole.log(`Command: '${command}', User: ${userName}`);\n\nreturn [{\n  json: {\n    ...data,\n    command: command,\n    userName: userName,\n    userId: userId,\n    chatId: chatId,\n    messageId: messageId,\n    isCallback: isCallback,\n    callbackQueryId: data.callback_query ? data.callback_query.id : null\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "0f33970c-1585-4655-8344-67a38eb1d942",
      "name": "Merge Config & Command2",
      "type": "n8n-nodes-base.merge",
      "position": [
        5344,
        448
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combinationMode": "mergeByPosition"
      },
      "typeVersion": 2.1
    },
    {
      "id": "e01e3ca1-f83a-4233-9d25-721d584e8e6c",
      "name": "Process Command2",
      "type": "n8n-nodes-base.function",
      "position": [
        5552,
        448
      ],
      "parameters": {
        "functionCode": "// Process command and check if action needed\n\nconst input = items[0].json;\n\n// Get menu\nlet menu = input.menuConfig[input.command] || input.menuConfig.default;\n\n// Commands that need actions\nconst actionCommands = [\n  'rate_1', 'rate_2', 'rate_3', 'rate_4', 'rate_5',\n  'lang_en', 'lang_es',\n  'analytics', 'report',\n  'stats', 'achievements',\n  'bug_report', 'feature_request',\n  'notif', 'profile'\n];\n\nconst requiresAction = actionCommands.includes(input.command);\n\nconsole.log(`Processing: ${input.command}, Action needed: ${requiresAction}`);\n\nreturn [{\n  json: {\n    ...input,\n    menu: menu,\n    requiresAction: requiresAction\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "e1434658-5196-46e8-a026-b7781593207b",
      "name": "Needs Action?2",
      "type": "n8n-nodes-base.if",
      "position": [
        5744,
        448
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.requiresAction }}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "04d837da-226a-47b2-b91b-e68dae10cb26",
      "name": "Action Router2",
      "type": "n8n-nodes-base.switch",
      "position": [
        5952,
        352
      ],
      "parameters": {
        "mode": "expression",
        "output": "={{ $json.command.startsWith('rate_') ? 0 : $json.command.startsWith('lang_') ? 1 : $json.command === 'analytics' || $json.command === 'report' ? 2 : $json.command === 'stats' || $json.command === 'achievements' ? 3 : $json.command === 'bug_report' || $json.command === 'feature_request' ? 4 : $json.command === 'notif' || $json.command === 'profile' ? 5 : 6 }}",
        "numberOutputs": 7
      },
      "typeVersion": 3.2
    },
    {
      "id": "edb6a6d4-6e8f-4136-81ff-ccfc34fc041d",
      "name": "Rating Handler2",
      "type": "n8n-nodes-base.function",
      "position": [
        6304,
        112
      ],
      "parameters": {
        "functionCode": "// Rating Handler - Process ratings\n\nconst input = items[0].json;\nconst rating = parseInt(input.command.split('_')[1]);\n\nconst messages = {\n  1: 'We\\'re sorry to hear that. We\\'ll improve!',\n  2: 'Thank you for the feedback. We\\'ll do better!',\n  3: 'Good to know! We\\'re always improving.',\n  4: 'Great! We\\'re glad you liked it!',\n  5: 'Awesome! Thank you so much!'\n};\n\nconsole.log(`\u2b50 Rating saved: ${rating}`);\n\nreturn [{\n  json: {\n    ...input,\n    customData: {\n      ratingMessage: messages[rating]\n    },\n    actionExecuted: 'rating_saved'\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "99d8f666-efc1-499b-b397-52646ee38874",
      "name": "Language Handler2",
      "type": "n8n-nodes-base.function",
      "position": [
        6304,
        240
      ],
      "parameters": {
        "functionCode": "// Language Handler - Change language\n\nconst input = items[0].json;\nconst lang = input.command.split('_')[1];\n\nconsole.log(`\ud83c\udf0d Language changed to: ${lang}`);\n\nreturn [{\n  json: {\n    ...input,\n    customData: {},\n    actionExecuted: 'language_changed'\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "cb801cf0-aa7e-43cf-9d1c-eb7eb42f97bc",
      "name": "Analytics Handler2",
      "type": "n8n-nodes-base.function",
      "position": [
        6304,
        368
      ],
      "parameters": {
        "functionCode": "// Analytics Handler - Generate analytics/reports\n\nconst input = items[0].json;\nlet customData = {};\n\nif (input.command === 'analytics') {\n  const views = Math.floor(Math.random() * 1000) + 100;\n  const clicks = Math.floor(Math.random() * 500) + 50;\n  customData.analyticsData = `Views: ${views}\\nClicks: ${clicks}\\nCTR: ${(clicks/views*100).toFixed(2)}%`;\n  console.log('\ud83d\udcc8 Analytics generated');\n} else if (input.command === 'report') {\n  customData.reportData = `Report ID: RPT-${Date.now()}\\nPeriod: Last 30 days\\nStatus: Ready`;\n  console.log('\ud83d\udcc4 Report generated');\n}\n\nreturn [{\n  json: {\n    ...input,\n    customData: customData,\n    actionExecuted: 'analytics_generated'\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "67c86bcd-bf2c-466a-b1f5-a2459561808e",
      "name": "Statistics Handler3",
      "type": "n8n-nodes-base.function",
      "position": [
        6304,
        496
      ],
      "parameters": {
        "functionCode": "// Statistics Handler - Load stats/achievements\n\nconst input = items[0].json;\nlet customData = {};\n\nif (input.command === 'stats') {\n  customData.statsData = `Total actions: ${Math.floor(Math.random() * 100)}\\nActive days: ${Math.floor(Math.random() * 30)}\\nRatings given: ${Math.floor(Math.random() * 10)}`;\n  console.log('\ud83d\udcca Stats loaded');\n} else if (input.command === 'achievements') {\n  customData.achievementData = '\ud83e\udd47 Early Adopter\\n\ud83e\udd48 Active User\\n\ud83e\udd49 Feedback Champion';\n  console.log('\ud83c\udfc6 Achievements loaded');\n}\n\nreturn [{\n  json: {\n    ...input,\n    customData: customData,\n    actionExecuted: 'stats_loaded'\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "957c06c4-221e-4ecd-83af-ff84c44c6b00",
      "name": "Feedback Handler3",
      "type": "n8n-nodes-base.function",
      "position": [
        6304,
        624
      ],
      "parameters": {
        "functionCode": "// Feedback Handler - Create tickets\n\nconst input = items[0].json;\nconst ticketType = input.command === 'bug_report' ? 'BUG' : 'REQ';\nconst ticketId = `${ticketType}-${Date.now()}`;\n\nconst customData = {\n  ticketInfo: `Ticket created: ${ticketId}\\nStatus: Open\\nPriority: Medium`\n};\n\nconsole.log(`\ud83c\udfab Ticket created: ${ticketId}`);\n\nreturn [{\n  json: {\n    ...input,\n    customData: customData,\n    actionExecuted: 'ticket_created'\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "c54a9d75-aec8-4ed5-b970-7d2482678125",
      "name": "Settings Handler3",
      "type": "n8n-nodes-base.function",
      "position": [
        6304,
        752
      ],
      "parameters": {
        "functionCode": "// Settings Handler - Load settings data\n\nconst input = items[0].json;\nlet customData = {};\n\nif (input.command === 'notif') {\n  customData.notificationStatus = '\u2705 Push: ON\\n\u2705 Email: ON\\n\u274c SMS: OFF';\n  console.log('\ud83d\udd14 Notifications loaded');\n} else if (input.command === 'profile') {\n  customData.userStatus = 'Active';\n  console.log('\ud83d\udc64 Profile loaded');\n}\n\nreturn [{\n  json: {\n    ...input,\n    customData: customData,\n    actionExecuted: 'settings_loaded'\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "801f73ff-be99-40da-bd01-93e8b29579c7",
      "name": "Default Handler2",
      "type": "n8n-nodes-base.function",
      "position": [
        6304,
        880
      ],
      "parameters": {
        "functionCode": "// Default Handler - No specific action\n\nconst input = items[0].json;\n\nconsole.log(`\u2139\ufe0f No action for: ${input.command}`);\n\nreturn [{\n  json: {\n    ...input,\n    customData: {},\n    actionExecuted: 'none'\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "0286be0c-4f86-410c-9544-acd50ebfe998",
      "name": "Build Response2",
      "type": "n8n-nodes-base.function",
      "position": [
        6624,
        1232
      ],
      "parameters": {
        "functionCode": "// Build final response with data\n\nconst input = items[0].json;\nconst menu = input.menu;\n\n// Replace placeholders\nlet responseText = menu.text\n  .replace(/{userName}/g, input.userName)\n  .replace(/{userId}/g, input.userId);\n\n// Replace custom data\nif (input.customData) {\n  for (const [key, value] of Object.entries(input.customData)) {\n    responseText = responseText.replace(new RegExp(`{${key}}`, 'g'), value);\n  }\n}\n\n// Clean remaining placeholders\nresponseText = responseText.replace(/{\\w+}/g, '');\n\nconsole.log(`\ud83d\udce4 Sending: ${input.command}`);\nif (input.actionExecuted) {\n  console.log(`Action: ${input.actionExecuted}`);\n}\n\nreturn [{\n  json: {\n    ...input,\n    responseText: responseText,\n    keyboard: menu.keyboard\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "0bd5c651-f87a-4f11-aa6a-8767db73b75c",
      "name": "Prepare Telegram2",
      "type": "n8n-nodes-base.function",
      "position": [
        6816,
        1232
      ],
      "parameters": {
        "functionCode": "// Prepare Telegram request\n\nconst input = items[0].json;\n\nconst apiMethod = input.isCallback ? 'editMessageText' : 'sendMessage';\n\nlet requestBody = {\n  chat_id: input.chatId,\n  text: input.responseText,\n  parse_mode: 'HTML',\n  reply_markup: {\n    inline_keyboard: input.keyboard\n  }\n};\n\nif (input.isCallback && input.messageId) {\n  requestBody.message_id = input.messageId;\n}\n\nreturn [{\n  json: {\n    apiMethod: apiMethod,\n    requestBody: requestBody,\n    isCallback: input.isCallback,\n    callbackQueryId: input.callbackQueryId\n  }\n}];"
      },
      "typeVersion": 1
    },
    {
      "id": "3a6785fa-53b8-495a-991c-d05fc05c6ed5",
      "name": "Set Bot Token4",
      "type": "n8n-nodes-base.set",
      "position": [
        7024,
        1232
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "bot-token",
              "name": "botToken",
              "type": "string",
              "value": "[!!! PALCE YOUR BOT TOKEN HERE !!!] "
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "838dd900-a6b3-4a81-96cb-b222b0b787de",
      "name": "Is Callback?4",
      "type": "n8n-nodes-base.if",
      "position": [
        7216,
        1232
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.isCallback }}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "885941fd-fcb2-4c99-883b-95b942d5ec53",
      "name": "Send to Telegram4",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        7424,
        1152
      ],
      "parameters": {
        "url": "={{ 'https://api.telegram.org/bot' + $json.botToken + '/' + $json.apiMethod }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.requestBody }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "d8d18120-1c1d-44db-8d18-6429c0f651aa",
      "name": "Answer Callback4",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        7424,
        1312
      ],
      "parameters": {
        "url": "={{ 'https://api.telegram.org/bot' + $('Set Bot Token4').item.json.botToken + '/answerCallbackQuery' }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ { \"callback_query_id\": $('Prepare Telegram2').item.json.callbackQueryId, \"text\": \"\u2705\" } }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "e9445b8e-cc2c-479b-be11-e90988424122",
      "name": "\ud83d\udd00 Router Logic2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6240,
        -336
      ],
      "parameters": {
        "color": 4,
        "width": 298,
        "height": 424,
        "content": "## \ud83d\udd00 Router Outputs\n\n0. Rating Handler\n1. Language Handler\n2. Analytics Handler\n3. Statistics Handler\n4. Feedback Handler\n5. Settings Handler\n6. Default Handler\n\nEach handler processes its specific commands independently"
      },
      "typeVersion": 1
    },
    {
      "id": "36fd73cd-6e54-4815-89c3-fc720e641de5",
      "name": "\ud83d\ude80 QUICK START GUIDE1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4096,
        592
      ],
      "parameters": {
        "width": 592,
        "height": 664,
        "content": "# \ud83d\ude80 QUICK START GUIDE\n\n## Step 1: Add Your Bot Token\n1. Open Telegram and message @BotFather\n2. Create a bot with `/newbot` or use existing\n3. Copy your bot token\n4. Find **'Set Bot Token'** node (purple)\n5. Replace `YOUR_BOT_TOKEN_HERE` with your token\n\n## Step 2: Set Webhook\n1. After saving workflow, click **'Production'**\n2. Copy the webhook URL shown\n3. Activate the workflow\n\n## Step 3: Test Your Bot\n1. Message your bot on Telegram\n2. Try commands: /start, /help\n3. Click the interactive buttons\n\n## Troubleshooting:\n- **404 Error**: Wrong bot token\n- **No response**: Webhook not active\n- **Buttons not working**: Check callback_data values"
      },
      "typeVersion": 1
    },
    {
      "id": "5949ce06-9acc-427d-ae01-a7cd9a08df0f",
      "name": "\ud83d\udcda WORKFLOW ARCHITECTURE1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4096,
        -336
      ],
      "parameters": {
        "color": 5,
        "width": 592,
        "height": 904,
        "content": "# \ud83d\udcda WORKFLOW ARCHITECTURE\n\n## SIMPLIFIED DESIGN:\n**Separation of Concerns**\n\n### 1\ufe0f\u20e3 Menu Config (Load Menu Config node)\n- ONLY navigation structure\n- Text & button definitions\n- Multi-level hierarchy support\n- NO business logic here!\n\n### 2\ufe0f\u20e3 Router (Action Router node)\n- Detects callback_data\n- Routes to handlers (7 outputs)\n- Pure switching logic\n\n### 3\ufe0f\u20e3 Handlers (7 separate nodes)\n- ALL business logic\n- API calls, database ops\n- Dynamic content generation\n\n## DATA FLOW:\n1. **Telegram Trigger** \u2192 Receives messages\n2. **Parallel**: Load Config + Extract Command\n3. **Merge** \u2192 Combine data\n4. **Process** \u2192 Find menu & check action\n5. **Route** \u2192 Send to handler if needed\n6. **Build** \u2192 Create response\n7. **Send** \u2192 Back to Telegram\n\n## BENEFITS:\n\u2705 Easy menu modifications\n\u2705 Portable configuration\n\u2705 Clear responsibilities\n\u2705 Scalable architecture"
      },
      "typeVersion": 1
    },
    {
      "id": "94d1d1ff-ca68-40fb-94d1-f254385631df",
      "name": "\ud83d\udca1 ADDING HANDLERS1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6592,
        -336
      ],
      "parameters": {
        "color": 3,
        "width": 464,
        "height": 1468,
        "content": "# \ud83d\udd27 ADVANCED: CREATE CUSTOM HANDLER\n\n## When You Need a Custom Handler:\nWhen your action needs to:\n- Save data to database\n- Call external API\n- Complex calculations\n- Multiple steps\n\n## STEP-BY-STEP:\n\n### Step 1: Copy a Handler\n1. Right-click any handler (like 'Default Handler')\n2. Click 'Duplicate'\n3. Drag the new node to empty space\n4. Double-click to rename it\n\n### Step 2: Update Router\n1. Double-click **'Action Router'** node\n2. Change 'Number of Outputs' from 7 to 8\n3. Update the expression:\n```javascript\n$json.command === 'my_action' ? 7 :\n// (7 is your new output number)\n```\n\n### Step 3: Connect Your Handler\n1. Click the small square on Action Router\n2. Drag to your new handler\n3. Select output '7' from popup\n\n### Step 4: Write Handler Logic\nDouble-click your handler and modify:\n```javascript\nconst input = items[0].json;\n\nif (input.command === 'my_action') {\n  // Your code here\n  const timestamp = new Date();\n  \n  return [{\n    json: {\n      ...input,\n      customData: {\n        result: 'Success!',\n        time: timestamp\n      }\n    }\n  }];\n}\n```\n\n### Step 5: Connect to Response\n1. Click small square on your handler\n2. Drag to 'Build Response' node\n3. Connection complete!\n\n## TEST YOUR HANDLER:\n1. Save workflow\n2. Test with your bot\n3. Check execution to see data flow"
      },
      "typeVersion": 1
    },
    {
      "id": "31fd910a-aed4-4f1b-8f23-da619a868704",
      "name": "\ud83d\udccb MENU CODE1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4864,
        -336
      ],
      "parameters": {
        "color": 6,
        "width": 636,
        "height": 618,
        "content": "# \ud83d\udccb CHECK THE CODE COMMENTS!\n\nThe **Load Menu Config** node has detailed inline comments explaining:\n- How to add new menus\n- Menu structure rules\n- Button format\n- Navigation patterns\n- Multi-level examples\n\n\ud83d\udc49 Open the node and read the comments for complete guidance!"
      },
      "typeVersion": 1
    },
    {
      "id": "6513c9ec-a1aa-4fd2-92ec-5b77e5cb19a1",
      "name": "\u2699\ufe0f MENU SETUP GUIDE1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4848,
        752
      ],
      "parameters": {
        "color": 6,
        "width": 660,
        "height": 2560,
        "content": "# \ud83d\udcd6 COMPLETE GUIDE: ADDING MENUS & ACTIONS\n\n## \ud83c\udd95 PART 1: ADD A SIMPLE MENU\n\n**Step 1:** Double-click the **'Load Menu Config'** node\n\n**Step 2:** Find this line (around line 200):\n```\n  // DEFAULT FALLBACK - IMPORTANT!\n```\n\n**Step 3:** Add your menu BEFORE that line:\n```javascript\n  // Your new menu\n  my_menu: {\n    text: 'Welcome to My Menu',\n    keyboard: [\n      [{ text: 'Option 1', callback_data: 'option1' }],\n      [{ text: '\ud83d\udd19 Back', callback_data: 'main' }]\n    ]\n  },\n```\n\n**Step 4:** To make a button go to your menu, find any button and change its callback_data:\n```javascript\n{ text: '\ud83d\udcc1 My Menu', callback_data: 'my_menu' }\n```\n\n**Step 5:** Click \u2705 Save\n\n---\n\n## \ud83c\udfaf PART 2: ADD AN ACTION BUTTON\n\n### A) Create the Button:\n\n**Step 1:** In your menu, add an action button:\n```javascript\n  my_menu: {\n    text: 'My Menu',\n    keyboard: [\n      [{ text: '\u26a1 Do Something', callback_data: 'do_action' }],\n      [{ text: '\ud83d\udd19 Back', callback_data: 'main' }]\n    ]\n  },\n```\n\n### B) Make It Trigger an Action:\n\n**Step 2:** Double-click **'Process Command'** node\n\n**Step 3:** Find the actionCommands array (line ~10):\n```javascript\nconst actionCommands = [\n  'rate_1', 'rate_2', // existing items...\n```\n\n**Step 4:** Add your action command:\n```javascript\nconst actionCommands = [\n  'do_action',  // \u2190 ADD THIS LINE\n  'rate_1', 'rate_2', // existing items...\n```\n\n### C) Route to Handler:\n\n**Step 5:** Double-click **'Action Router'** node\n\n**Step 6:** Find the long expression and ADD at the beginning:\n```javascript\n$json.command === 'do_action' ? 6 :\n```\n(The number 6 sends it to the Default Handler)\n\n### D) Process the Action:\n\n**Step 7:** Double-click **'Default Handler'** node\n\n**Step 8:** Add your logic:\n```javascript\nif (input.command === 'do_action') {\n  // What happens when button is clicked\n  input.customData = {\n    message: 'Action completed!'\n  };\n}\n```\n\n---\n\n## \ud83d\udca1 IMPORTANT TIPS:\n\n**Naming Rules:**\n- Use underscore: `my_menu` \u2705\n- No spaces: `my menu` \u274c\n- Keep it simple: `settings` \u2705\n\n**Text Formatting:**\n- `\\n` = new line\n- `<b>Bold</b>` = bold text\n- Use emojis: \ud83c\udfaf \ud83d\udcc1 \u26a1 \u2705\n\n**Always Test:**\n1. Save workflow\n2. Click Execute Workflow\n3. Message your bot\n4. Click your new button\n\n**Common Mistakes:**\n- Forgot comma after }\n- Wrong callback_data spelling\n- Didn't add to actionCommands\n- Missing back button"
      },
      "typeVersion": 1
    },
    {
      "id": "4e45ea08-9db3-4f5b-a694-5b22bbf3e65f",
      "name": "\ud83d\udc1b TROUBLESHOOTING & TIPS1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7104,
        -336
      ],
      "parameters": {
        "width": 568,
        "height": 856,
        "content": "# \ud83d\udc1b TROUBLESHOOTING & TIPS\n\n## COMMON ISSUES:\n\n**Bot not responding:**\n- Workflow must be Active\n- Check webhook URL\n- Verify bot token\n\n**404 Error:**\n- Replace YOUR_BOT_TOKEN_HERE\n\n**Buttons not working:**\n- Check callback_data matches\n- Verify Answer Callback node\n\n**Menu not found:**\n- Falls back to 'default' menu\n\n## PRODUCTION TIPS:\n\n**Security:**\n- Use environment variables for token\n- Never commit real tokens\n- Validate user inputs\n\n**Performance:**\n- Add database for persistence\n- Cache frequently used data\n- Use sub-workflows for complex logic\n\n**Debugging:**\n- Use console.log() in code nodes\n- Check execution history\n- Test with single messages first"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Is Callback?4": {
      "main": [
        [
          {
            "node": "Send to Telegram4",
            "type": "main",
            "index": 0
          },
          {
            "node": "Answer Callback4",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send to Telegram4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Action Router2": {
      "main": [
        [
          {
            "node": "Rating Handler2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Language Handler2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Analytics Handler2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Statistics Handler3",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Feedback Handler3",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Settings Handler3",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Default Handler2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs Action?2": {
      "main": [
        [
          {
            "node": "Action Router2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Response2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Bot Token4": {
      "main": [
        [
          {
            "node": "Is Callback?4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Response2": {
      "main": [
        [
          {
            "node": "Prepare Telegram2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rating Handler2": {
      "main": [
        [
          {
            "node": "Build Response2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Default Handler2": {
      "main": [
        [
          {
            "node": "Build Response2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Command4": {
      "main": [
        [
          {
            "node": "Merge Config & Command2",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Process Command2": {
      "main": [
        [
          {
            "node": "Needs Action?2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Feedback Handler3": {
      "main": [
        [
          {
            "node": "Build Response2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Language Handler2": {
      "main": [
        [
          {
            "node": "Build Response2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Menu Config2": {
      "main": [
        [
          {
            "node": "Merge Config & Command2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Telegram2": {
      "main": [
        [
          {
            "node": "Set Bot Token4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings Handler3": {
      "main": [
        [
          {
            "node": "Build Response2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Trigger4": {
      "main": [
        [
          {
            "node": "Load Menu Config2",
            "type": "main",
            "index": 0
          },
          {
            "node": "Extract Command4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analytics Handler2": {
      "main": [
        [
          {
            "node": "Build Response2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Statistics Handler3": {
      "main": [
        [
          {
            "node": "Build Response2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Config & Command2": {
      "main": [
        [
          {
            "node": "Process Command2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Ever wanted to build a Telegram bot with professional-looking menus that actually work? This n8n workflow creates an interactive bot with dynamic inline keyboards, multi-level navigation, and smart button routing - all without writing complex code from scratch.

Source: https://n8n.io/workflows/8844/ — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Slack & Telegram

Try on any outfit virtually - right inside Telegram. A user sends a person photo, then a garment photo (captioned ), and the bot replies with an AI-generated try-on result image using a dedicated Virt

Telegram Trigger, Telegram, Google Sheets +1
Slack & Telegram

A robust n8n workflow designed to enhance Telegram bot functionality for user management and broadcasting. It facilitates automatic support ticket creation, efficient user data storage in Redis, and a

HTTP Request, Redis, Telegram +1
Slack & Telegram

Transform your digital payment business with a fully-featured Telegram bot that handles everything from product listings to transaction processing. Perfect for entrepreneurs looking to automate their

Telegram Trigger, Telegram, HTTP Request
Slack & Telegram

TGBot. Uses telegram, googleSheets, telegramTrigger, httpRequest. Event-driven trigger; 30 nodes.

Telegram, Google Sheets, Telegram Trigger +1
Slack & Telegram

This template provides a workflow to integrate a Telegram bot with NeurochainAI's inference capabilities, supporting both text processing and image generation. Follow these steps to get started:

Telegram Trigger, HTTP Request, Telegram