⏱️ 20 min read · 📥 Free template + dynamic menus, media & polling coverage
Here's a confession: my first Telegram bot ignored me for two hours. The trigger was "working" — the workflow was active, the token was right — but the bot sat there silent. The cause was a setting buried in BotFather's menu that nobody's guide mentions. You'll read it in section 2, and you'll never lose those two hours yourself.
Connecting n8n to Telegram takes three steps: (1) create a bot with BotFather and copy its token; (2) add that token as a Telegram credential in n8n; (3) build a workflow with a Telegram Trigger (to receive messages and button presses) and a Telegram node (to send replies). Inline keyboard buttons are sent via the Bot API, and the full importable command bot is in section 8.
The two things that trip up 90% of people — finding the chat ID and the trigger not firing in groups — get their own sections (6 and 16), because nobody else explains them properly.
On this page
- What the Telegram Nodes Can Do
- BotFather: Token, Commands & Privacy Mode
- Connect n8n (Credential + Token Security)
- Your First Bot: Trigger + Echo
- Inline Keyboards & Callback Buttons
- Find Any Chat ID
- Groups & Channels
- Free: The Importable Command Bot
- Telegram as Your Alert Hub
- Webhooks vs Polling
- Sending Files, Photos & Videos
- Retries, Rate Limits & Long Messages
- Community Nodes Worth Installing
- Real-World Builds
- Security Checklist
- 8 Common Errors + Fixes
- FAQ
- Sources & References
1. What the Telegram Nodes Can Do
n8n has two Telegram nodes, and knowing the split saves you an hour of searching the node panel:
| Node | Direction | What it does |
|---|---|---|
| Telegram Trigger | Telegram → n8n | Fires on new messages, callback queries (button presses), inline queries, and edited messages. This is the bot's ears. |
| Telegram | n8n → Telegram | Sends messages, photos, videos, audio, documents; edits and deletes messages; manages chat members; pins messages. This is the bot's mouth. |
The sending node's most useful operations, in the order you'll actually use them:
| Operation | Use it for |
|---|---|
Send Message | Text replies, alerts, HTML/Markdown formatting, keyboards |
Send Photo / Document | Attaching files from other nodes or by URL |
Edit Message | Live status updates — "processing…" → "done ✅" |
Delete Message | Cleaning up bot replies, removing expired content |
Get Chat / Get Chat Members | Reading group info, building member lists |
Pin / Unpin Message | Pinning daily reports in a channel |
Answer Callback Query | Closing the "loading" state after a button press |
2. BotFather: Token, Commands & Privacy Mode
Every bot starts in a chat with @BotFather — Telegram's official bot-creator. Open the chat and run /newbot. It asks for a name (anything) and a username (must end in bot, must be unique). It then returns the only thing that matters:
Use this token to access the HTTP API:
7123456789:AAHxKpQ1wZ4vV5mN8rT2yU9sW3cB6dE0fG7h
Keep your token secure and store it safely.
It can be used by anyone to control your bot.
Then run these three BotFather commands — the third one is the setting that ignored me for two hours:
/setcommands — register your bot's commands
Paste a list like start - Welcome message per line. It powers the little / menu users see in the chat — a detail that makes your bot feel professional for free.
/setprivacy — then choose Disable
This is the two-hour trap. With privacy mode ON (the default), your bot in a group only sees messages that mention it or start with /. Disable it if the bot must read all group messages — e.g., an auto-moderation bot.
/setjoingroups — decide where the bot may live
If the bot is personal (alerts only), disable group joining — it removes an entire class of abuse and confusion.
3. Connect n8n (Credential + Token Security)
In n8n: Credentials → Add credential → Telegram API, paste the token from BotFather, save. Give it a name like MyBot. That's it — both Telegram nodes will offer this credential.
/revoke in BotFather generates a fresh one in seconds.One more thing worth doing now: when you publish a workflow template, n8n replaces real credential references with placeholders — but your own exports contain the credential IDs. That's fine for your backups; just don't share raw exports publicly.
4. Your First Bot: Trigger + Echo
The minimal bot is two nodes: a Telegram Trigger listening for messages, and a Telegram Send Message replying. The trigger returns the raw Telegram update — the fields you'll use most:
$json.message.text— what the user typed$json.message.chat.id— where to reply$json.message.from.first_name— who wrote it$json.callback_query.data— which button was pressed (section 5)
The trigger node, exported (this is the exact structure to import):
{
"parameters": {
"updates": ["message", "callback_query"],
"additionalFields": {
"download": true
}
},
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.1,
"position": [250, 300],
"credentials": {
"telegramApi": {
"id": "YOUR_TELEGRAM_CREDENTIAL_ID"
}
}
}
And the reply node — the two required fields are chatId and text:
{
"parameters": {
"chatId": "={{ $json.message.chat.id }}",
"text": "=You said: {{ $json.message.text }}",
"additionalFields": {
"parseMode": "HTML"
}
},
"name": "Send Reply",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [650, 300],
"credentials": {
"telegramApi": {
"id": "YOUR_TELEGRAM_CREDENTIAL_ID"
}
}
}
= rule: any field using {{ }} must start with = — otherwise n8n sends the expression as literal text. It's the single most common "my bot replies with brackets" bug.Connect them, activate the workflow (production activation — manual "Execute workflow" doesn't run triggers), open your bot in Telegram, press Start, and send a message. The echo comes back instantly.
5. Inline Keyboards & Callback Buttons
A bot that only echoes is a toy. A bot with buttons is an interface. Inline keyboards are the row of tappable buttons under a message — and they're the single biggest gap in every competitor's guide, because most of them never explain what happens after the tap.
5.1 The two halves of a button
- Sending the keyboard: the message carries a
reply_markupwith aninline_keyboardarray. Each button hastext(the label) and eithercallback_data(fires your workflow) orurl(opens a link). - Receiving the tap: when a user presses a callback button, Telegram sends a callback_query update — which is why the trigger in section 4 listens for
callback_querytoo, not just messages.
5.2 The reliable way: send via the Bot API
Here's the honest engineering truth: the Telegram node's visual keyboard builder is rigid — one fixed keyboard per node. The moment your buttons need to change (dynamic lists, per-user menus), the community-standard solution is to call Telegram's API directly from an HTTP Request node. It works on every n8n version and every plan. One trick makes it clean: the token comes from an environment variable ($env.TELEGRAM_TOKEN), so it never has to sit inside the workflow itself:
// Code node — build the payload (keyboard included)
const item = $input.item.json;
const chatId = item.message.chat.id;
const payload = {
chat_id: chatId,
text: 'What would you like to do?',
reply_markup: {
inline_keyboard: [
[
{ text: '✅ Approve', callback_data: 'approve' },
{ text: '❌ Reject', callback_data: 'reject' }
],
[
{ text: '🌐 Visit site', url: 'https://triggerworkflow.com' }
]
]
}
};
return [{ json: { body: JSON.stringify(payload) } }];
# First, expose the token once — in your docker-compose.yml
environment:
- TELEGRAM_TOKEN=7123456789:AA-your-real-token
# HTTP Request node — POST the payload to the Bot API
# URL:
=https://api.telegram.org/bot{{ $env.TELEGRAM_TOKEN }}/sendMessage
# Body content type: JSON, body:
={{ $json.body }}
Limits worth memorizing: callback_data is capped at 64 bytes, and one message fits roughly 1–2 rows × 8 buttons before the keyboard gets cramped. For anything bigger, link to a sub-menu message instead.
5.3 Dynamic menus & multi-level navigation
Static keyboards answer one question. Dynamic menus build an interface — main menu → settings → profile — where every tap redraws the buttons. The pattern is a switch on callback_data:
// Menu router — every button's callback_data is a route
const cq = item.callback_query;
const route = cq.data; // 'main' | 'settings' | 'profile'
let text = '';
let keyboard = null;
switch (route) {
case 'main':
text = 'Main menu';
keyboard = [[
{ text: '⚙️ Settings', callback_data: 'settings' },
{ text: '👤 Profile', callback_data: 'profile' }
]];
break;
case 'settings':
text = 'Settings — nothing here yet.';
keyboard = [[{ text: '🔙 Back', callback_data: 'main' }]];
break;
default:
text = 'Unknown action';
}
const payload = {
chat_id: cq.message.chat.id,
text,
reply_markup: keyboard ? { inline_keyboard: keyboard } : undefined
};
return [{ json: { body: JSON.stringify(payload) } }];
- Update in place: instead of a new message per tap, call
editMessageTextwith the samemessage_id— the menu redraws itself in the same bubble, and the user never scrolls through a stack of old menus. - The full pattern: the official dynamic menus template on n8n.io implements this exact switch-case structure with a rating system on top.
answerCallbackQuery. Sending a new message does NOT close it (I learned that the hard way). The template in section 8 answers every callback automatically, so your users never see the spinner.6. Find Any Chat ID (The #1 Question)
Every send needs a destination. Here's the complete map, because "what's my chat ID?" is the most-asked Telegram question in existence:
Your own ID — 10 seconds
Open a chat with @userinfobot or @RawDataBot and press Start. They reply with your numeric ID instantly.
Group / channel ID — the API way
Add your bot to the group (or channel, as admin), send any message there, then open this URL in a browser:
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
Find the latest update and read message.chat.id. Group and channel IDs are negative (like -1001234567890) — copy the minus sign too, or you'll get "chat not found" and blame the bot.
@username as the chat ID directly — the one case where you don't need the number at all.7. Groups & Channels
- Groups: add the bot via the group's "Add member" menu. If the bot must read all messages (moderation, logging), recall section 2:
/setprivacy → Disable. - Channels: add the bot as an administrator — without admin rights it can't post. Use the channel's negative ID or public
@usernameas the destination. - Posting rights: "Not enough rights to send" errors mean the bot is a plain member or lacks the posting permission — check the channel's administrator list.
- Rate limits: Telegram throttles bots at ~1 message/second per chat and ~20/minute per group. Batch sends need a Wait node between messages — or Telegram will drop them silently.
8. Free Template: The Importable Command Bot
Everything above, assembled into one production-ready bot: /start sends a welcome with an inline keyboard, /help lists commands, button presses are answered and their spinner is closed properly, and anything else is echoed. Five nodes, no credentials beyond Telegram.
{
"name": "Telegram Command Bot",
"nodes": [
{
"parameters": {
"updates": ["message", "callback_query"],
"additionalFields": {
"download": true
}
},
"id": "trigger",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.1,
"position": [250, 300],
"credentials": {
"telegramApi": {
"id": "YOUR_TELEGRAM_CREDENTIAL_ID"
}
}
},
{
"parameters": {
"jsCode": "const item = $input.item.json;\nconst isCallback = Boolean(item.callback_query);\n\nconst chatId = isCallback\n ? item.callback_query.message.chat.id\n : (item.message?.chat?.id || 0);\nconst text = isCallback\n ? item.callback_query.data\n : (item.message?.text || '');\nconst firstName = isCallback\n ? item.callback_query.from.first_name\n : (item.message?.from?.first_name || 'there');\nconst callbackQueryId = isCallback ? item.callback_query.id : '';\n\nlet reply;\nlet keyboard = null;\n\nif (isCallback) {\n reply = `Button pressed: ${text}`;\n} else if (text === '/start') {\n reply = `Welcome, ${firstName}! I am your n8n-powered bot.`;\n keyboard = {\n inline_keyboard: [\n [{ text: '✅ Get Started', callback_data: 'started' }],\n [{ text: '❓ Help', callback_data: 'help' }, { text: '🌐 Website', url: 'https://triggerworkflow.com' }]\n ]\n };\n} else if (text === '/help') {\n reply = 'Commands:\\n/start — welcome + menu\\n/help — this list\\nAnything else is echoed back.';\n} else {\n reply = text || 'Send /help for commands.';\n}\n\nconst payload = { chat_id: chatId, text: reply };\nif (keyboard) { payload.reply_markup = keyboard; }\n\nreturn [{ json: { isCallback, chatId, reply, callbackQueryId, body: JSON.stringify(payload) } }];"
},
"id": "parse",
"name": "Parse & Decide",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [450, 300]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.isCallback }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
"id": "if-callback",
"name": "Callback?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [650, 300]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\"callback_query_id\": \"{{ $json.callbackQueryId }}\", \"text\": \"Done ✅\"}",
"options": {}
},
"id": "answer-cb",
"name": "Answer Callback",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [850, 200]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $('Parse & Decide').item.json.body }}",
"options": {}
},
"id": "http-send",
"name": "Send Reply (Bot API)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [1050, 300]
}
],
"connections": {
"Telegram Trigger": {
"main": [
[
{
"node": "Parse & Decide",
"type": "main",
"index": 0
}
]
]
},
"Parse & Decide": {
"main": [
[
{
"node": "Callback?",
"type": "main",
"index": 0
}
]
]
},
"Callback?": {
"main": [
[
{
"node": "Answer Callback",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Reply (Bot API)",
"type": "main",
"index": 0
}
]
]
},
"Answer Callback": {
"main": [
[
{
"node": "Send Reply (Bot API)",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true
}
}
- The token stays secret: both raw API calls read it from the
TELEGRAM_TOKENenvironment variable (set it once in your compose file, section 5.2) — nothing sensitive sits inside the workflow, and the template contains no real token. - Want Markdown instead of plain text? add
parse_mode: 'HTML'to the payload in the Code node and use<b>bold</b>tags. - More ready-to-import blueprints live in our n8n Workflow Templates Library.
9. Telegram as Your Alert Hub
The bot is nice. The alert hub is where Telegram earns its keep: a fixed chat ID (yours, from section 6) and every workflow on your server can reach your pocket in seconds.
- Error alerts: route your Error Trigger workflow's critical branch to Telegram — our n8n Error Handling guide builds exactly this, with severity-based routing.
- Downtime alerts: the Website Downtime Checker template ends in a Telegram node — your site goes down, your phone buzzes before your users notice.
- Daily digests: a scheduled workflow that sums up yesterday's numbers and sends one morning message beats a dashboard nobody opens.
- Incoming webhooks from anywhere: any external service can POST to an n8n Webhook node that forwards to Telegram — see What is a Webhook if the concept is new.
10. Webhooks vs Polling: Which One to Use
The Telegram Trigger works over a webhook: on activation, n8n registers itself with Telegram, and Telegram pushes every update to your WEBHOOK_URL — which means n8n must be reachable through a public HTTPS address. Not every host can do that. Here is the honest comparison:
| Webhook (Telegram Trigger) | Polling (community node) | |
|---|---|---|
| How it works | Telegram pushes updates to n8n | n8n pulls updates via getUpdates on an interval |
| Requirements | Public HTTPS URL (WEBHOOK_URL set) | Nothing — works behind CGNAT, on a LAN, without a domain |
| Latency | Near-instant | Slight delay (the polling interval) |
| Best for | Production bots on a real VPS + domain | Development, home servers, ISPs that block port 443 |
- Stuck behind CGNAT or a home network? Install
@mentoster/n8n-nodes-telegram-pollingfrom Settings → Community Nodes — it adds a Telegram Trigger (long polling) node that works anywhere, no public URL needed. - Switching between the two: while a webhook is set,
getUpdatesreturns nothing. To go back to polling, delete the webhook first (Telegram'sdeleteWebhookendpoint). - Need a public URL temporarily? A Cloudflare Tunnel or ngrok exposes your local n8n in minutes — useful for testing webhook bots before you buy a domain.
- Rule of thumb: one bot, one consumer. Never run the webhook trigger and a polling trigger on the same bot at once.
11. Sending Files, Photos & Videos
Text is the start, not the end. Three patterns cover 95% of media use:
- Send a photo by URL: the Telegram node's
Send Photooperation accepts a direct URL in thephotofield — no download step needed. - Send a file from your workflow: connect a node that produces binary data (HTTP download, Google Drive) and pass it to
Send Document. Enabledownloadon the trigger to receive files users send you — they arrive as binary data, ready for Drive, S3, or Sheets. - Send a batch as one album: Telegram groups up to 10 photos/videos into a single media group via
sendMediaGroup:
// Build a media group — up to 10 items, one message
const urls = ['https://example.com/a.jpg', 'https://example.com/b.jpg'];
const media = urls.map((u, i) => ({
type: 'photo',
media: u,
caption: i === 0 ? 'Batch report' : undefined
}));
return [{ json: { body: JSON.stringify({ chat_id: CHAT_ID, media }) } }];
// POST https://api.telegram.org/bot{{ $env.TELEGRAM_TOKEN }}/sendMediaGroup
12. Retries, Rate Limits & Long Messages
Three production realities every bot hits sooner or later:
12.1 Retry transient failures
Network blips (ECONNRESET, timeouts) are normal. On the Telegram or HTTP Request node, enable Retry On Fail with 3 tries and a short wait — that alone clears most flaky sends. Full resilience patterns live in the n8n Error Handling guide.
12.2 Respect the rate limits
Telegram throttles bots to roughly 1 message per second per chat (and ~30 per second globally). Batch broadcasts need a Wait node (0.6–1.2 s) between sends — otherwise Telegram silently drops the extras.
12.3 Split long messages (4096-char limit)
Telegram rejects messages over 4096 characters. Split before sending:
// Split long text under Telegram's 4096-character limit
const MAX = 4000;
const chunks = [];
for (let i = 0; i < text.length; i += MAX) {
chunks.push(text.slice(i, i + MAX));
}
return chunks.map(c => ({ json: { chatId, text: c } }));
13. Community Nodes Worth Installing
The built-in nodes cover most needs. Two verified community nodes fill the real gaps:
| Package | What it adds | Install |
|---|---|---|
@mentoster/n8n-nodes-telegram-polling | A Telegram Trigger (long polling) node — bots without a public URL | Settings → Community Nodes |
@topvisor/n8n-nodes-telegram-send-message-custom | Send messages with raw JSON reply_markup through your telegramApi credential | Settings → Community Nodes |
14. Real-World Builds
- AI support bot: Telegram Trigger → AI Agent node — answers come from your LLM with the conversation as context. Add memory to make it remember users across sessions.
- Rating system: buttons
rate_1…rate_5→ append to Google Sheets — the official dynamic menus & rating template on n8n.io demonstrates the full switch-case pattern. - Task manager: a personal bot as the front end to Airtable or Supabase — add, list, and complete tasks from any chat, no app install.
15. Security Checklist
- ✅ Token only in the n8n credential — never in shared JSON, logs, or screenshots
- ✅ For raw Bot API calls (HTTP Request nodes), pass the token via the
TELEGRAM_TOKENenvironment variable — never hardcode it in node fields - ✅
/setjoingroups → Disablefor personal alert bots - ✅
/setcommandskeeps the bot's surface predictable — unknown commands fall through to your default reply - ✅ Webhook workflows that feed Telegram should check a secret header or path token (our webhook guides cover this)
- ✅ If the token leaks:
/revokein BotFather, then update the n8n credential — two minutes, total reset - ✅ Store the bot's chat IDs you trust in a Code node list — don't let any webhook spam your phone
16. The 8 Most Common Errors & Fixes
| # | Error / Symptom | Cause | Fix |
|---|---|---|---|
| 1 | Trigger never fires — bot is silent | Workflow not activated, wrong token, or the bot was never added to the group | Activate the workflow, re-check the credential, add the bot to the chat |
| 2 | Bot ignores normal group messages | BotFather privacy mode is ON — the bot only sees mentions and commands | /setprivacy → Disable in BotFather (section 2, step 2) |
| 3 | Forbidden: bot was blocked by the user |
The user blocked the bot; your broadcast keeps hitting the dead ID | Skip that ID in your send loop — and prune blocked IDs from your list regularly |
| 4 | Bad Request: chat not found |
Wrong chat ID, missing minus sign on group IDs, or bot not a member | Re-check the ID with getUpdates (section 6) and bot membership |
| 5 | 409 Conflict: terminated by other getUpdates request |
Two processes polling the same bot (two n8n instances, or another bot app) | One bot = one polling consumer. Deactivate the duplicate workflow or app |
| 6 | Bot replies with literal {{ $json... }} text |
Expression field missing the leading = |
Start the field with = (section 4 callout) |
| 7 | Buttons do nothing when pressed | The trigger isn't listening for callback_query updates |
Add callback_query to the trigger's updates list (section 4) |
| 8 | Not enough rights to send ... |
Bot is not an admin (channels) or lacks posting rights | Promote the bot in the channel's administrators list (section 7) |
17. Frequently Asked Questions
Create a bot with BotFather to get the token, add a Telegram credential in n8n with that token, then use the Telegram Trigger node to receive updates and the Telegram node to send messages (sections 2–4).
The five usual causes: the workflow is not activated, the bot token is wrong, the bot was never added to the group, BotFather privacy mode hides group messages, or another process is polling the same bot (409 Conflict). Section 16 covers each.
Message @userinfobot or @RawDataBot for your personal ID. For groups and channels: add the bot, send a message, then open api.telegram.org/bot<TOKEN>/getUpdates and read chat.id — group and channel IDs are negative numbers (section 6).
Yes. Add the bot to the group (privacy mode off if it must read messages) or as a channel administrator, then use the negative chat ID. Public channels accept their @username as the destination (section 7).
You send a reply_markup with an inline_keyboard array — each button carries a callback_data or url. The Telegram Trigger must listen for callback_query updates, which arrive with the callback data when a user presses a button (section 5).
Not with polling. Telegram allows only one active getUpdates consumer per bot — a second instance gets a 409 Conflict and one of them silently stops receiving updates. Point one bot at one n8n instance.
Yes — anyone with the token controls your bot. Keep it in the n8n credential, never in shared workflow JSON or screenshots, and /revoke it in BotFather if it ever leaks (section 3).
Yes. The Telegram node supports Send Photo, Send Video, Send Audio, and Send Document operations — attach files from previous nodes or send them by URL (section 1).
Add a Telegram sendMessage node at the end of any workflow, or route your Error Trigger workflow's critical branch to Telegram for every failure. A fixed chat ID turns Telegram into your personal notification center (section 9).
Yes. The Telegram Trigger works over a webhook — on activation n8n registers itself with Telegram, so n8n must be reachable through a public HTTPS address. Behind CGNAT or without a domain, install the long-polling community node instead (section 10).
Telegram rejects messages over 4096 characters. Split the text into chunks of about 4000 characters before sending, one sendMessage call per chunk — the ready-to-copy snippet is in section 12.3.
Sources & References
- n8n Documentation — Telegram node
- n8n Documentation — Telegram Trigger node
- Telegram Bot API — sendMessage & InlineKeyboardMarkup
- Telegram — Bots FAQ (privacy mode, limits)
- n8n Community — Dynamic inline keyboards discussion
- n8n Templates — Telegram bot with dynamic menus & rating system
- npm — @mentoster/n8n-nodes-telegram-polling
- n8n Documentation — Community nodes
