This article walks you through a production-ready Telegram reminder bot built in one n8n workflow. Users DM your bot like:
/remind 2025-08-20 18:30 Buy milk
/list
/done 123456
Reminders are stored in workflow static data (persisted by n8n) and dispatched by a Schedule Trigger that runs every minute. No external DB needed.
You’ll set up
- Telegram Trigger → parse commands
- Code node → store reminders in static data
- Telegram node → confirmations and lists
- Schedule Trigger → checks due reminders
- Code node → pops due items
- Telegram node → sends reminders
Prerequisites
- n8n (Cloud or self-hosted).
- A Telegram bot token from @BotFather.
- If you’re self-hosting, make sure your instance has a public HTTPS base URL, so Telegram webhooks can reach it. (Configure n8n’s base URL accordingly.) (n8n Docs, 2acrestudios.com)
![Workflow Image]()
Step 1. Create the workflow shell
- In n8n, create a new workflow and name it “Telegram Reminder Bot (Scheduler)”.
- We’ll add four nodes to start: Telegram Trigger, Code, Telegram, and Schedule Trigger.
- Telegram Trigger: receives messages from your bot. (n8n Community)
- Telegram: sends chat replies. (n8n Docs)
- Schedule Trigger: runs the scan every minute. (n8n Docs)
- Code: lightweight JS to parse/store reminders. (n8n Community)
Tip: The Schedule Trigger (formerly Cron) replaced the old Cron node. Make sure the workflow is activated; otherwise schedules won’t run. (n8n, n8n Docs)
Step 2. Telegram Trigger
- Add Telegram Trigger.
- Set your Telegram credentials (the token from @BotFather).
- Update types → choose message (text DMs to your bot).
The trigger listens via Telegram webhooks and starts workflow executions when messages arrive. (n8n Community)
Step 3. Command router (Code node)
Add a Code node after Telegram Trigger. This node:
- Parses
/start
, /help
, /list
, /done <id>
, and /remind YYYY-MM-DD HH:mm <text>
.
- Stores reminders in workflow static data so they persist between executions.
- Emits a single item
{ chatId, reply }
for the next Telegram node.
Paste this JS in the Code node:
// Command Router — parses Telegram messages and stores reminders
// Uses workflow static data (persists across executions/activations).
// Docs: https://docs.n8n.io/code/code-node/#use-workflow-static-data
const data = $getWorkflowStaticData('global');
if (!data.reminders) data.reminders = []; // Array of { id, chatId, text, dueAt }
// Helpers
const pad = (n) => (n < 10 ? '0' + n : '' + n);
const fmt = (ts) => {
const d = new Date(ts);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
const help = [
'Hi! I can remind you of tasks.',
'Commands:',
'/remind YYYY-MM-DD HH:mm Your reminder text',
'/list — show upcoming reminders',
'/done <id> — delete a reminder',
].join('\n');
const msg = $json.message?.text || '';
const chatId = $json.message?.chat?.id;
if (!chatId) {
return [{ json: { chatId: 0, reply: 'No chat id found.' } }];
}
if (!msg || /^\/(start|help)\b/.test(msg)) {
return [{ json: { chatId, reply: help } }];
}
if (/^\/list\b/.test(msg)) {
const mine = data.reminders
.filter(r => r.chatId === chatId)
.sort((a, b) => a.dueAt - b.dueAt)
.slice(0, 50);
const text = mine.length
? 'Upcoming reminders:\n' + mine.map(r => `#${r.id} — ${fmt(r.dueAt)} — ${r.text}`).join('\n')
: 'No reminders found.';
return [{ json: { chatId, reply: text } }];
}
if (/^\/done\b/.test(msg)) {
const id = msg.split(/\s+/)[1];
if (!id) return [{ json: { chatId, reply: 'Usage: /done <id>' } }];
const before = data.reminders.length;
data.reminders = data.reminders.filter(r => !(r.chatId === chatId && String(r.id) === String(id)));
const ok = data.reminders.length !== before;
return [{ json: { chatId, reply: ok ? `Removed #${id}` : `Couldn’t find #${id}` } }];
}
if (/^\/remind\b/.test(msg)) {
// Strict format: /remind YYYY-MM-DD HH:mm Text...
const m = msg.match(/^\/remind\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\s+([\s\S]+)/);
if (!m) {
return [{ json: { chatId, reply: 'Format: /remind YYYY-MM-DD HH:mm Your reminder text' } }];
}
const [, datePart, timePart, text] = m;
const date = new Date(`${datePart}T${timePart}:00`);
if (isNaN(date.getTime())) {
return [{ json: { chatId, reply: 'Invalid date/time. Use YYYY-MM-DD HH:mm (24h).' } }];
}
const id = Math.floor(Math.random() * 1e6);
data.reminders.push({ id, chatId, text, dueAt: date.getTime() });
return [{ json: { chatId, reply: `✅ Reminder #${id} set for ${fmt(date.getTime())}\nText: ${text}` } }];
}
// Fallback
return [{ json: { chatId, reply: help } }];
Step 4. Telegram (reply)
Add a Telegram node after the Code node (name it Telegram Reply):
- Operation: Send Message
- Chat ID:
={{$json.chatId}}
- Text:
={{$json.reply}}
That’s your confirmation/list/error path. (n8n Docs)
Step 5. Scheduler (every minute)
Add a Schedule Trigger:
Activate the workflow so this actually runs on schedule. (n8n Docs)
Step 6. Due-item dispatcher (Code node)
Connect the Schedule Trigger to a second Code node (name it Due Dispatcher). Paste:
// Pops due reminders and emits items for Telegram delivery
const data = $getWorkflowStaticData('global');
if (!data.reminders) data.reminders = [];
const now = Date.now();
const due = [];
const keep = [];
for (const r of data.reminders) {
if (r.dueAt <= now) {
due.push({ json: { chatId: r.chatId, text: `⏰ Reminder: ${r.text}` } });
} else {
keep.push(r);
}
}
// Keep only future reminders
data.reminders = keep;
return due;
Step 7. Telegram (send reminders)
Add another Telegram node (name it Telegram Send Reminder) after Due Dispatcher:
- Operation: Send Message
- Chat ID:
={{$json.chatId}}
- Text:
={{$json.text}}
This node will fire once per due reminder item. (n8n Docs)
Test
- Activate the workflow.
- DM your bot:
/remind 2025-08-20 18:30 Buy milk
/list
- Wait for the scheduled run (within a minute of the due time) and you’ll receive your reminder.
Notes & Troubleshooting
- Only one Telegram webhook per bot: Don’t use the same bot token in multiple active Telegram-Trigger workflows. (n8n Community)
- Schedules respect time zones: Ensure your n8n instance/workflow timezone is set as expected if reminders feel off. (n8n Docs)
- Static data persists across executions—perfect for small queues. For heavy-duty or multi-instance setups, consider a DB instead. (n8n Community)
Complete workflow JSON
Import this into n8n (Credentials aren’t included; set your Telegram credentials on both Telegram nodes and the Telegram Trigger after import).
{
"name": "Telegram Reminder Bot (Scheduler)",
"active": false,
"nodes": [
{
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"id": "c4a15b64-7f6b-4d1b-9d8d-teletrig",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.2,
"position": [
-600,
-40
],
"webhookId": "11111111-2222-3333-4444-555555555555"
},
{
"parameters": {
"jsCode": "// Command Router — parses Telegram messages and stores reminders\n// Uses workflow static data (persists across executions/activations).\n// Docs: https://docs.n8n.io/code/code-node/#use-workflow-static-data\n\nconst data = $getWorkflowStaticData('global');\nif (!data.reminders) data.reminders = []; // Array of { id, chatId, text, dueAt }\n\n// Helpers\nconst pad = (n) => (n < 10 ? '0' + n : '' + n);\nconst fmt = (ts) => {\n const d = new Date(ts);\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;\n};\nconst help = [\n 'Hi! I can remind you of tasks.',\n 'Commands:',\n '/remind YYYY-MM-DD HH:mm Your reminder text',\n '/list — show upcoming reminders',\n '/done <id> — delete a reminder',\n].join('\\n');\n\nconst msg = $json.message?.text || '';\nconst chatId = $json.message?.chat?.id;\n\nif (!chatId) {\n return [{ json: { chatId: 0, reply: 'No chat id found.' } }];\n}\n\nif (!msg || /^\\/(start|help)\\b/.test(msg)) {\n return [{ json: { chatId, reply: help } }];\n}\n\nif (/^\\/list\\b/.test(msg)) {\n const mine = data.reminders\n .filter(r => r.chatId === chatId)\n .sort((a, b) => a.dueAt - b.dueAt)\n .slice(0, 50);\n const text = mine.length\n ? 'Upcoming reminders:\\n' + mine.map(r => `#${r.id} — ${fmt(r.dueAt)} — ${r.text}`).join('\\n')\n : 'No reminders found.';\n return [{ json: { chatId, reply: text } }];\n}\n\nif (/^\\/done\\b/.test(msg)) {\n const id = msg.split(/\\s+/)[1];\n if (!id) return [{ json: { chatId, reply: 'Usage: /done <id>' } }];\n const before = data.reminders.length;\n data.reminders = data.reminders.filter(r => !(r.chatId === chatId && String(r.id) === String(id)));\n const ok = data.reminders.length !== before;\n return [{ json: { chatId, reply: ok ? `Removed #${id}` : `Couldn’t find #${id}` } }];\n}\n\nif (/^\\/remind\\b/.test(msg)) {\n // Strict format: /remind YYYY-MM-DD HH:mm Text...\n const m = msg.match(/^\\/remind\\s+(\\d{4}-\\d{2}-\\d{2})\\s+(\\d{2}:\\d{2})\\s+([\\s\\S]+)/);\n if (!m) {\n return [{ json: { chatId, reply: 'Format: /remind YYYY-MM-DD HH:mm Your reminder text' } }];\n }\n const [, datePart, timePart, text] = m;\n const date = new Date(`${datePart}T${timePart}:00`);\n if (isNaN(date.getTime())) {\n return [{ json: { chatId, reply: 'Invalid date/time. Use YYYY-MM-DD HH:mm (24h).' } }];\n }\n const id = Math.floor(Math.random() * 1e6);\n data.reminders.push({ id, chatId, text, dueAt: date.getTime() });\n return [{ json: { chatId, reply: `✅ Reminder #${id} set for ${fmt(date.getTime())}\\nText: ${text}` } }];\n}\n\n// Fallback\nreturn [{ json: { chatId, reply: help } }];\n"
},
"id": "1b0d3f3f-64d2-4f69-bb22-router",
"name": "Command Router",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-330,
-40
]
},
{
"parameters": {
"operation": "sendMessage",
"chatId": "={{$json.chatId}}",
"text": "={{$json.reply}}",
"additionalFields": {}
},
"id": "b8db0ce9-5a9f-4f2b-8e24-tgreply",
"name": "Telegram Reply",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1,
"position": [
-80,
-40
]
},
{
"parameters": {
"interval": 1,
"unit": "minutes"
},
"id": "0a09476f-3452-4028-b32b-sched",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-600,
240
]
},
{
"parameters": {
"jsCode": "// Pops due reminders and emits items for Telegram delivery\nconst data = $getWorkflowStaticData('global');\nif (!data.reminders) data.reminders = [];\n\nconst now = Date.now();\nconst due = [];\nconst keep = [];\n\nfor (const r of data.reminders) {\n if (r.dueAt <= now) {\n due.push({ json: { chatId: r.chatId, text: `⏰ Reminder: ${r.text}` } });\n } else {\n keep.push(r);\n }\n}\n\n// Keep only future reminders\ndata.reminders = keep;\n\nreturn due;\n"
},
"id": "e3a2d6af-6b7b-40d7-8a5f-dispatch",
"name": "Due Dispatcher",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-330,
240
]
},
{
"parameters": {
"operation": "sendMessage",
"chatId": "={{$json.chatId}}",
"text": "={{$json.text}}",
"additionalFields": {}
},
"id": "f9d8f34e-1b53-4a2c-9a9b-tgsend",
"name": "Telegram Send Reminder",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1,
"position": [
-80,
240
]
}
],
"connections": {
"Telegram Trigger": {
"main": [
[
{
"node": "Command Router",
"type": "main",
"index": 0
}
]
]
},
"Command Router": {
"main": [
[
{
"node": "Telegram Reply",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Due Dispatcher",
"type": "main",
"index": 0
}
]
]
},
"Due Dispatcher": {
"main": [
[
{
"node": "Telegram Send Reminder",
"type": "main",
"index": 0
}
]
]
}
}
}
You’re done. Activate the workflow, DM your bot, and let the Scheduler handle the rest.