NewSchokr 13 is here. A whole new UI, seven locations, snapshots and more. Read more →

How to schedule recurring tasks in a Discord bot

Run code on a timer in your bot: node-cron for discord.js and discord.ext.tasks for discord.py, with time zones and error handling explained.

Updated · 2 min read

To run something on a schedule (a daily message, a reminder check, a stats update), use a scheduler inside your bot's process: node-cron for discord.js, or discord.ext.tasks for discord.py. Because Schokr servers don't sleep, these run reliably as long as the bot is online.

discord.js with node-cron

npm install node-cron
const cron = require('node-cron');

client.once(Events.ClientReady, (readyClient) => {
  // Every day at 09:00 London time
  cron.schedule(
    '0 9 * * *',
    async () => {
      try {
        const channel = await readyClient.channels.fetch(process.env.ANNOUNCE_CHANNEL_ID);
        await channel.send('Good morning!');
      } catch (error) {
        console.error('Daily message failed:', error);
      }
    },
    { timezone: 'Europe/London' },
  );
});

Start jobs inside the ready handler so the client is logged in before they run, and use once so jobs aren't added twice.

Cron syntax quick reference

ExpressionRuns
*/5 * * * *Every 5 minutes
0 * * * *Every hour, on the hour
0 9 * * *Every day at 09:00
0 18 * * 5Every Friday at 18:00
0 0 1 * *The first day of every month at midnight

The fields are minute, hour, day of month, month, day of week.

Simple intervals

For "every N minutes" without exact times, setInterval works too:

setInterval(updateStats, 10 * 60 * 1000); // every 10 minutes

discord.py with discord.ext.tasks

import datetime
from zoneinfo import ZoneInfo

from discord.ext import commands, tasks

LONDON = ZoneInfo("Europe/London")


class Daily(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.morning.start()
        self.cleanup.start()

    def cog_unload(self):
        self.morning.cancel()
        self.cleanup.cancel()

    @tasks.loop(time=datetime.time(hour=9, minute=0, tzinfo=LONDON))
    async def morning(self):
        channel = self.bot.get_channel(ANNOUNCE_CHANNEL_ID)
        if channel:
            await channel.send("Good morning!")

    @tasks.loop(minutes=5)
    async def cleanup(self):
        ...  # runs every 5 minutes

    @morning.before_loop
    @cleanup.before_loop
    async def wait_until_ready(self):
        await self.bot.wait_until_ready()

    @morning.error
    async def morning_error(self, error):
        print(f"morning task failed: {error!r}", flush=True)

before_loop with wait_until_ready() prevents the task from running before the bot's cache is ready. If an unhandled exception occurs in a loop, the loop stops; the error handler lets you see why. ZoneInfo may need the tzdata package on some systems; add it to requirements.txt if you get a ZoneInfoNotFoundError.

Time zones

Servers usually run in UTC. If you don't pass a time zone, "9 AM" means 9 AM UTC. Always set a time zone explicitly for user-facing schedules.

Schedules survive restarts, missed runs don't

A scheduler in your code restarts when the bot restarts, but any run that was due while the bot was offline is skipped. For things that must happen eventually (like reminders), store the due time in a database and on each check run everything that's overdue. See using a database with your bot.

Schokr's Schedules feature

Your server also has a Schedules section in the console. It handles server-level actions such as restarting the server at a set time. Use it for things like a nightly restart, and use the in-code schedulers above for bot logic.