# Events & Clocks

URL: https://zerodev.ca/docs/medusa/developer-documentation/events

> 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`.

```js title="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);
        }
    }
}
```

### Direct Messages

The client subscribes to the `DirectMessages` intent with the `Channel` partial, so `messageCreate` listeners also receive messages sent to the bot in DMs. DM messages have no `guild` and run outside any server scope. Guard with `if (!message.guild) return;` when a listener only handles server messages, or check `message.channel.type === ChannelType.DM` to handle DMs on purpose, as the Modmail module does. Prefix commands only run in servers.

### Event Constructor Options

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

| Property | Type      | Default     | Description                                                                                       |
| -------- | --------- | ----------- | ------------------------------------------------------------------------------------------------- |
| `name`   | `string`  | Required    | Unique event identifier within the module.                                                        |
| `event`  | `string`  | Required    | Event identifier string (Discord.js event key or Medusa event name).                              |
| `source` | `string`  | `"discord"` | Event dispatcher source: `"discord"` for gateway events or `"medusa"` for Medusa internal events. |
| `once`   | `boolean` | `false`     | When `true`, registers the listener using `once()` instead of `on()`.                             |

### Event Methods

| Method        | Signature                    | Description                                                  |
| ------------- | ---------------------------- | ------------------------------------------------------------ |
| `getName()`   | `() => string`               | Returns the event name.                                      |
| `getEvent()`  | `() => string`               | Returns the event identifier string.                         |
| `getSource()` | `() => string`               | Returns the dispatcher source (`"discord"` or `"medusa"`).   |
| `isOnce()`    | `() => boolean`              | Returns whether the event triggers only once.                |
| `run()`       | `(...args) => Promise<void>` | Invoked whenever the event fires with the payload arguments. |

### 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`:

```js title="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
            });
        }
    }
}
```

## Medusa Internal Events

Modules and framework components emit internal events across `this.medusa.events` whenever lifecycle phases complete or state changes occur. Any module or third-party addon can emit and listen to these events.

### Emitting Events

The `Modules` and `Handlers` base classes expose direct emission methods that delegate to `this.medusa.emitAsync`:

```js
await this.emitAsync("ticket.opened", { ticket: record, channel, opener });
```

### Listening via Event Classes

Event classes in `src/events/` support Medusa events by declaring `source: "medusa"`. Medusa automatically wires these to `this.medusa.events` during startup:

```js title="modules/example/src/events/ticketOpened.js"
import { Events } from "#medusa/modules";

export class TicketOpenedEvent extends Events {
    constructor(medusa) {
        super(medusa, {
            name: "exampleTicketOpened",
            event: "ticket.opened",
            source: "medusa"
        });
    }

    async run({ ticket, channel, opener }) {
        this.medusa.console.log(this.medusa.console.levels.info, `Ticket #${ticket.ticketId} opened by ${opener.username}`);
    }
}
```

### Listening Programmatically

Modules can also subscribe directly inside `onStartup()`:

```js
this.medusa.events.on("ticket.opened", async ({ ticket, channel, opener }) => {
    // React to event
});
```

### Framework Lifecycle Events

| Event                      | Payload                     | Description                                                                                                                                                                    |
| -------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `medusa.ready`             | None                        | Emitted once Medusa finishes initial boot and all modules are loaded.                                                                                                          |
| `medusa.discord.ready`     | `client`                    | Emitted after the bots connect, after a bot restarts, and after modules move between bots. Listeners added in `onStartup()` run as their module, so they use the module's bot. |
| `medusa.discord.bot.ready` | `bot`                       | Emitted once per bot when that bot connects. Used for per-bot work such as application emoji sync.                                                                             |
| `module.registered`        | `module`                    | Emitted after a module registers its static packages.                                                                                                                          |
| `module.loaded`            | `module`                    | Emitted when a module finishes loading and executes `onStartup()`.                                                                                                             |
| `<module>.ready`           | `module`                    | Per-module readiness event (e.g. `ticket.ready`, `leveling.ready`).                                                                                                            |
| `modules.ready`            | `loadedMap`                 | Emitted after all registered modules have been loaded and models synchronized.                                                                                                 |
| `module.failed`            | `{ folder, module, error }` | Emitted when a module fails during registration or loading.                                                                                                                    |
| `module.deloaded`          | `module`                    | Emitted when a module deloads during shutdown.                                                                                                                                 |
| `module.toggled`           | `{ id, guildId, enabled }`  | Emitted when a module is turned on or off for one server.                                                                                                                      |
| `<module>.shutdown`        | `module`                    | Per-module shutdown event (e.g. `ticket.shutdown`).                                                                                                                            |

### Module Domain Events

Modules emit the following domain events when state changes occur:

| Module           | Event                             | Payload                                                                            | Description                                                                                                                                                              |
| ---------------- | --------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ticket`         | `ticket.opened`                   | `{ ticket, channel, opener, category, openedBy }`                                  | Fired when a new support ticket channel opens.                                                                                                                           |
| `ticket`         | `ticket.closed`                   | `{ ticket, channel, closer, auto, reason, silent }`                                | Fired when a ticket is closed.                                                                                                                                           |
| `ticket`         | `ticket.claimed`                  | `{ ticket, channel, staff }`                                                       | Fired when staff claims a ticket.                                                                                                                                        |
| `ticket`         | `ticket.unclaimed`                | `{ ticket, channel, staff }`                                                       | Fired when staff unclaims a ticket.                                                                                                                                      |
| `moderation`     | `moderation.banned`               | `{ guild, targetUser, moderator, reason, durationMs, caseId, record }`             | Fired when a member is banned.                                                                                                                                           |
| `moderation`     | `moderation.unbanned`             | `{ guild, targetUserId, moderator, reason, caseId, record }`                       | Fired when a member is unbanned.                                                                                                                                         |
| `moderation`     | `moderation.kicked`               | `{ guild, member, moderator, reason, caseId, record }`                             | Fired when a member is kicked.                                                                                                                                           |
| `moderation`     | `moderation.muted`                | `{ guild, member, moderator, durationMs, reason, expiresAt, caseId, record }`      | Fired when a member is timed out.                                                                                                                                        |
| `moderation`     | `moderation.unmuted`              | `{ guild, member, moderator, reason, caseId, record }`                             | Fired when a timeout is removed.                                                                                                                                         |
| `moderation`     | `moderation.warned`               | `{ guild, member, moderator, reason, points, durationMs, caseId, warnId, record }` | Fired when a warning is issued.                                                                                                                                          |
| `moderation`     | `moderation.warn_removed`         | `{ guild, warning, moderator, reason, caseId, record }`                            | Fired when a warning is revoked.                                                                                                                                         |
| `moderation`     | `moderation.purged`               | `{ channel, amount, targetUser, moderator, count }`                                | Fired when messages are bulk deleted.                                                                                                                                    |
| `moderation`     | `moderation.slowmode_set`         | `{ channel, seconds, moderator }`                                                  | Fired when channel rate limit changes.                                                                                                                                   |
| `moderation`     | `moderation.channel_locked`       | `{ channel, reason, moderator }`                                                   | Fired when a channel is locked.                                                                                                                                          |
| `moderation`     | `moderation.channel_unlocked`     | `{ channel, moderator }`                                                           | Fired when a channel is unlocked.                                                                                                                                        |
| `moderation`     | `moderation.case_created`         | `record`                                                                           | Fired on any moderation case creation.                                                                                                                                   |
| `moderation`     | `moderation.notice`               | `{ guild, user, action, reason, caseId, components }`                              | Fired before a punishment DM is sent. Push component rows into `components` to attach buttons to the DM. The Appeals module uses it to add its appeal button to ban DMs. |
| `leveling`       | `leveling.xp_added`               | `{ guild, userId, amount, source, xp, level, oldLevel }`                           | Fired when a member earns experience points.                                                                                                                             |
| `leveling`       | `leveling.leveled_up`             | `{ guild, userId, oldLevel, newLevel, xp }`                                        | Fired when a member advances in level.                                                                                                                                   |
| `leveling`       | `leveling.xp_removed`             | `{ guild, userId, amount, xp, level }`                                             | Fired when administrator deducts experience.                                                                                                                             |
| `leveling`       | `leveling.xp_set`                 | `{ guild, userId, xp, level }`                                                     | Fired when experience is explicitly set.                                                                                                                                 |
| `leveling`       | `leveling.reset`                  | `{ guildId, userId }`                                                              | Fired when member leveling progress is cleared.                                                                                                                          |
| `economy`        | `economy.balance_added`           | `{ guildId, userId, amount, type }`                                                | Fired when funds are credited.                                                                                                                                           |
| `economy`        | `economy.balance_removed`         | `{ guildId, userId, amount, type }`                                                | Fired when funds are debited.                                                                                                                                            |
| `economy`        | `economy.balance_set`             | `{ guildId, userId, amount, type }`                                                | Fired when balance is explicitly updated.                                                                                                                                |
| `economy`        | `economy.deposited`               | `{ guildId, userId, amount }`                                                      | Fired when currency moves from wallet to bank.                                                                                                                           |
| `economy`        | `economy.withdrawn`               | `{ guildId, userId, amount }`                                                      | Fired when currency moves from bank to wallet.                                                                                                                           |
| `economy`        | `economy.transferred`             | `{ guildId, fromUserId, toUserId, amount }`                                        | Fired when currency is sent between members.                                                                                                                             |
| `giveaways`      | `giveaways.started`               | `{ guild, channel, host, giveaway }`                                               | Fired when a giveaway commences.                                                                                                                                         |
| `giveaways`      | `giveaways.entered`               | `{ giveaway, user }`                                                               | Fired when a user enters the giveaway pool.                                                                                                                              |
| `giveaways`      | `giveaways.ended`                 | `{ guild, giveaway, winners }`                                                     | Fired when a giveaway concludes and draws winners.                                                                                                                       |
| `giveaways`      | `giveaways.rerolled`              | `{ guild, giveaway, winners }`                                                     | Fired when replacement winners are selected.                                                                                                                             |
| `suggestions`    | `suggestions.submitted`           | `{ guild, member, text, category, suggestionId, messageId }`                       | Fired when a suggestion is submitted.                                                                                                                                    |
| `suggestions`    | `suggestions.voted`               | `{ guild, user, messageId, direction }`                                            | Fired when a vote is recorded on a suggestion.                                                                                                                           |
| `suggestions`    | `suggestions.status_updated`      | `{ guild, member, entry, status, reason }`                                         | Fired when staff changes suggestion status.                                                                                                                              |
| `counting`       | `counting.succeeded`              | `{ guild, channel, user, count, highest, isRecord }`                               | Fired when a valid count number is posted.                                                                                                                               |
| `counting`       | `counting.failed`                 | `{ guild, channel, user, evaluated, expected }`                                    | Fired when a broken count resets or warns.                                                                                                                               |
| `invites`        | `invites.joined`                  | `{ guild, member, inviterId, code, fake, credited }`                               | Fired when a member joins via tracked invite.                                                                                                                            |
| `invites`        | `invites.left`                    | `{ guild, member, inviterId }`                                                     | Fired when a member departs the guild.                                                                                                                                   |
| `invites`        | `invites.bonus_adjusted`          | `{ guild, userId, delta, before, after }`                                          | Fired when bonus invites are adjusted.                                                                                                                                   |
| `invites`        | `invites.reset`                   | `{ guild, userId }`                                                                | Fired when member invite statistics reset.                                                                                                                               |
| `join-to-create` | `join-to-create.channel_created`  | `{ channel, member }`                                                              | Fired when a temporary voice channel spawns.                                                                                                                             |
| `join-to-create` | `join-to-create.channel_deleted`  | `{ channel, channelName }`                                                         | Fired when an empty temporary channel deletes.                                                                                                                           |
| `join-to-create` | `join-to-create.channel_locked`   | `{ channel, member }`                                                              | Fired when a temporary channel locks.                                                                                                                                    |
| `join-to-create` | `join-to-create.channel_unlocked` | `{ channel, member }`                                                              | Fired when a temporary channel unlocks.                                                                                                                                  |
| `alerts`         | `alerts.dispatched`               | `{ sub, details, channel }`                                                        | Fired when a livestream or upload alert dispatches. `details` holds `title`, `url` and `thumbnail`.                                                                      |
| `alerts`         | `alerts.subscription_added`       | `{ guildId, channelId, platform, streamer, actor }`                                | Fired when a creator subscription is created.                                                                                                                            |
| `alerts`         | `alerts.subscription_removed`     | `{ guildId, platform, streamer, actor }`                                           | Fired when a creator subscription is removed.                                                                                                                            |

## 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`.

```js title="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:

| Property   | Type     | Default  | Description                                                    |
| ---------- | -------- | -------- | -------------------------------------------------------------- |
| `name`     | `string` | Required | Unique clock identifier within the module.                     |
| `schedule` | `string` | Required | Standard 5-field cron expression string evaluated by `croner`. |

### Standard Cron Syntax

The `schedule` string uses standard 5-field cron format:

```text
┌───────────── 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

| Expression    | Execution Interval                         |
| ------------- | ------------------------------------------ |
| `* * * * *`   | Executes every minute.                     |
| `*/5 * * * *` | Executes every 5 minutes.                  |
| `0 * * * *`   | Executes hourly at minute 0.               |
| `0 0 * * *`   | Executes daily at midnight UTC.            |
| `0 0 * * 0`   | Executes 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.
