Docs
Developer Documentation

Events & Clocks

Listen to Discord gateway events and schedule recurring background tasks.

Medusa separates time-triggered tasks and Discord gateway events into two dedicated building blocks: Events for responding to gateway events emitted by Discord.js, and Clocks for running scheduled background jobs via cron expressions.

Discord Gateway Events

Gateway listeners reside in src/events/ and load from every *.js file located directly in that directory. Each file exports a class extending Events from #medusa/modules.

modules/example/src/events/messageCreate.js
import { Events } from "#medusa/modules";
import { Events as DiscordEvents } from "discord.js";

export class MessageCreateEvent extends Events {
    constructor(medusa) {
        super(medusa, {
            name: "messageCreate",
            event: DiscordEvents.MessageCreate,
            once: false
        });
    }

    async run(message) {
        if (!message || message.author.bot || !message.guild) return;

        const handler = this.medusa.modules.handlers.get(this.module, "items");
        if (handler) {
            await handler.scanMessage(message);
        }
    }
}

Event Constructor Options

The configuration object passed to super(medusa, meta) accepts the following properties:

PropertyTypeDefaultDescription
namestringRequiredUnique event identifier within the module.
eventstringRequiredDiscord.js event identifier (e.g. Events.MessageCreate, Events.GuildMemberAdd, Events.VoiceStateUpdate).
oncebooleanfalseWhen true, Medusa registers the listener using client.once() instead of client.on().

Event Methods

MethodSignatureDescription
getName()() => stringReturns the event name.
getEvent()() => stringReturns the Discord.js event key string.
isOnce()() => booleanReturns whether the event triggers only once.
run()(...args) => Promise<void>Invoked whenever Discord emits the target gateway event with the arguments supplied by Discord.js.

Handling Discord Component Interactions

When users click buttons, select items from dropdown menus, or submit modal dialogs, Discord emits an interactionCreate event. Medusa parses valid custom IDs and populates interaction.customIdParsed:

modules/example/src/events/interactionCreate.js
import { Events } from "#medusa/modules";
import { Events as DiscordEvents, MessageFlags } from "discord.js";

export class InteractionCreateEvent extends Events {
    constructor(medusa) {
        super(medusa, {
            name: "exampleInteractionCreate",
            event: DiscordEvents.InteractionCreate,
            once: false
        });
    }

    async run(interaction) {
        if (!interaction || !interaction.guildId) return;

        if (interaction.isButton()) {
            if (!interaction.customIdParsed || interaction.customIdParsed.module !== this.module) return;

            const action = interaction.customIdParsed.name;
            switch (action) {
                case "confirm":
                    return interaction.reply({
                        embeds: [this.medusa.embeds.success("Confirmed.")],
                        flags: MessageFlags.Ephemeral
                    });
                default:
                    return interaction.deferUpdate().catch(() => {});
            }
        }

        if (interaction.isStringSelectMenu()) {
            if (!interaction.customIdParsed || interaction.customIdParsed.module !== this.module) return;

            const selected = interaction.values[0];
            return interaction.reply({
                embeds: [this.medusa.embeds.info("info", `Selected: ${selected}`)],
                flags: MessageFlags.Ephemeral
            });
        }

        if (interaction.isModalSubmit()) {
            if (!interaction.customIdParsed || interaction.customIdParsed.module !== this.module) return;

            const title = interaction.fields.getTextInputValue("title");
            return interaction.reply({
                embeds: [this.medusa.embeds.success(`Received title: ${title}`)],
                flags: MessageFlags.Ephemeral
            });
        }
    }
}

Scheduled Clocks

Background schedules reside in src/clocks/ and load from every *.js file located directly in that directory. Each file exports a class extending Clocks from #medusa/modules.

modules/example/src/clocks/cleanup.js
import { Clocks } from "#medusa/modules";

export class CleanupClock extends Clocks {
    constructor(medusa) {
        super(medusa, {
            name: "cleanup",
            schedule: "0 * * * *"
        });
    }

    async run() {
        const handler = this.medusa.modules.handlers.get(this.module, "items");
        if (!handler) return;

        this.medusa.console.log(this.medusa.console.levels.info, "Running scheduled cleanup routine...");
        await handler.purgeExpired();
    }
}

Clock Constructor Options

The configuration object passed to super(medusa, meta) accepts the following properties:

PropertyTypeDefaultDescription
namestringRequiredUnique clock identifier within the module.
schedulestringRequiredStandard 5-field cron expression string evaluated by croner.

Standard Cron Syntax

The schedule string uses standard 5-field cron format:

┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of the month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday)
│ │ │ │ │
* * * * *

Common Cron Expressions

ExpressionExecution Interval
* * * * *Executes every minute.
*/5 * * * *Executes every 5 minutes.
0 * * * *Executes hourly at minute 0.
0 0 * * *Executes daily at midnight UTC.
0 0 * * 0Executes weekly on Sunday at midnight UTC.

Clock Lifecycle

When Medusa finishes loading all modules, ClockInterface.schedule() initializes a Cron instance for each registered clock. When Medusa stops or deloads modules, ClockInterface.stop() cancels all running cron timers to ensure clean memory shutdown.

On this page