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
| Expression | Runs |
|---|---|
*/5 * * * * | Every 5 minutes |
0 * * * * | Every hour, on the hour |
0 9 * * * | Every day at 09:00 |
0 18 * * 5 | Every 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.