n8n  

Build a Task Reminder Bot Using n8n + Telegram + Scheduler

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

  1. Telegram Trigger → parse commands
  2. Code node → store reminders in static data
  3. Telegram node → confirmations and lists
  4. Schedule Trigger → checks due reminders
  5. Code node → pops due items
  6. 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

  1. Add Telegram Trigger.
  2. Set your Telegram credentials (the token from @BotFather).
  3. 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 } }];
  • Why static data? It’s simple, built-in persistence with $getWorkflowStaticData('global'), ideal for lightweight queues. (n8n Community)

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:

  • Every: 1
  • Unit: minute

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

  1. Activate the workflow.
  2. DM your bot:
    /remind 2025-08-20 18:30 Buy milk
    /list
    
  3. 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.