Triggers
Every script runs on exactly one of these 84 triggers, set when the script is created. Each entry below names the ctx.* object that trigger populates; see the ctx object for every field on every one of those types.
Always populated
Every trigger, with no exceptions, populates ctx.guildId,
ctx.eventType, ctx.args, ctx.env,
ctx.secret and ctx.fetchCallsRemaining.
ctx.actorId and ctx.channelId are populated on almost every
trigger too: the tables below flag the specific triggers where one or both genuinely
don't exist (e.g. there's no "channel" for a server-settings change, and no "actor"
for a Twitch event), rather than being empty/zero placeholders.
Commands & interactions(10)
Answered by the trigger type, not the script: plain command, button and select-menu triggers get a deferred "thinking…" ack that the script fills in with editResponse() (or, with "Update the clicked message" set on a button/select trigger, a silent ack whose editResponse() rewrites the clicked message); the ": Modal" variants are answered with the form in the script's own config instead, and the script body runs on submit. See Commands & modals for the full picture.
| Trigger | Populates | Description & notes |
|---|---|---|
| SlashCommand | none | A user runs a /command. The bot replies with a message the script fills in via editResponse(). |
| SlashCommand: Modal | InteractionContext | A user runs a /command and a popup form opens immediately. The script body runs when the form is submitted, with the answers in ctx.interaction.values and the command's options still in ctx.args. |
| MessageCommand | MessageContext | A user runs a message context-menu command (right-click a message → Apps). The target message is in ctx.message. Names may include spaces and capitals, and are matched verbatim (not lowercased). |
| MessageCommand: Modal | MessageContext + InteractionContext | Same, but a popup form opens immediately; the script runs on submit with ctx.message still available. |
| UserCommand | MemberContext | A user runs a user context-menu command (right-click a user → Apps). The target member is in ctx.member (ctx.actorId is the invoker). Names may include spaces and capitals, matched verbatim. |
| UserCommand: Modal | MessageContext + InteractionContext | Same, but a popup form opens immediately; the script runs on submit with ctx.member still available. |
| ButtonClick | InteractionContext | A user clicks a button a script sent. ctx.interaction.payload carries the button's payload (set by the sending script), and ctx.interaction.customId/messageId/channelId identify where it was clicked. |
| ButtonClick: Modal | InteractionContext | A user clicks a button a script sent and a popup form opens immediately; the script runs on submit, with the button's payload in ctx.interaction.payload alongside the form's ctx.interaction.values. |
| SelectMenu | InteractionContext | A user picks from a select menu a script sent (selects[] on sendMessage/editMessage). The chosen values are in ctx.interaction.selected: option values for a string menu, snowflake IDs for user/role/channel/mentionable menus, with the picked users/roles/channels resolved in ctx.resolved.Set "Update the clicked message" on the trigger to acknowledge by editing the menu's own message (editResponse() then rewrites it) instead of posting a new reply. |
| SelectMenu: Modal | InteractionContext | A user picks from a select menu and a popup form opens immediately; the script runs on submit with the picked values still in ctx.interaction.selected. |
Members(3)
| Trigger | Populates | Description & notes |
|---|---|---|
| GuildMemberAdd | MemberContext | A member joins the guild. no ctx.channelId |
| GuildMemberRemove | MemberContext | A member leaves or is removed from the guild. no ctx.channelId Not dispatched when the removed member is the bot itself (Discord sends this right before the guild becomes inaccessible on kick/ban). |
| GuildMemberUpdate | MemberContext | A member's roles, nickname, or other profile fields change. no ctx.channelId |
Channels & roles(6)
Fired from Discord's own channel and role events, so they see every change whoever made it: the backbone of anti-nuke and server-log scripts. Pair with getAuditLog() to learn who acted.
| Trigger | Populates | Description & notes |
|---|---|---|
| ChannelCreate | ChannelInfo | A channel or category is created (not threads; see ThreadCreate). ctx.channel has the new channel's detail. no ctx.actorId |
| ChannelUpdate | ChannelInfo | A channel's name, topic, category, permissions or settings change. ctx.channel is the new state and ctx.previousChannel what the channel cache held before (name, type, category and position; null when it wasn't cached). no ctx.actorId |
| ChannelDelete | ChannelInfo | A channel or category is deleted. ctx.channel is its last known detail. Who did it is in the audit log (GuildAuditLogEntryCreate or getAuditLog()). no ctx.actorId |
| RoleCreate | RoleInfo | A role is created. ctx.role has it, including its permission names. no ctx.actorIdno ctx.channelId |
| RoleUpdate | RoleInfo | A role's name, colour, position or permissions change. ctx.previousRole is what the role cache held before (null when it wasn't cached), so a script can spot a dangerous permission being added. no ctx.actorIdno ctx.channelId |
| RoleDelete | RoleInfo | A role is deleted. ctx.role is its last cached state (only id is guaranteed). no ctx.actorIdno ctx.channelId |
Messages(4)
| Trigger | Populates | Description & notes |
|---|---|---|
| MessageCreate | MessageContext | A message is posted in a guild channel. |
| MessageUpdate | MessageContext | A message is edited. content is the new text; previousContent is what it said before, recovered from the 3-hour message cache (null when unknown).Only fires for an actual content edit. Discord also resends this event for embed population and other non-edit changes, which are filtered out before dispatch. |
| MessageDelete | MessageContext | A message is deleted. content/authorId are recovered best-effort from the 3-hour message cache: empty strings on a cache miss (message never seen, or older than 3 hours). no ctx.actorId |
| MessageBulkDelete | BulkDeleteContext | Multiple messages are deleted at once. no ctx.actorId |
Reactions(4)
| Trigger | Populates | Description & notes |
|---|---|---|
| MessageReactionAdd | ReactionContext | A reaction is added to a message. |
| MessageReactionRemove | ReactionContext | A reaction is removed from a message. |
| MessageReactionRemoveAll | ReactionContext | Every reaction is cleared from a message (no single user; ctx.reaction has no userId/emoji). no ctx.actorId |
| MessageReactionRemoveEmoji | ReactionContext | Every reaction for one specific emoji is cleared from a message (ctx.reaction has the emoji but no single user). no ctx.actorId |
Polls(2)
| Trigger | Populates | Description & notes |
|---|---|---|
| MessagePollVoteAdd | PollVoteContext | A user votes on a native Discord poll (ctx.pollVote.answerId is the chosen answer). |
| MessagePollVoteRemove | PollVoteContext | A user retracts a native Discord poll vote. |
Threads & forums(5)
| Trigger | Populates | Description & notes |
|---|---|---|
| ThreadCreate | ThreadContext | A thread or forum/media post is created. ctx.thread.isForumPost distinguishes a forum post from an ordinary text-channel thread, and ctx.channelId is the new thread itself.Only fires on genuine creation. Discord also resends this event when the bot merely regains visibility of an existing thread (e.g. on reconnect), which is ignored so old posts don't re-trigger scripts. |
| ThreadUpdate | ThreadContext | A thread or forum post is renamed, archived, locked, or re-tagged (ctx.thread reflects the current state, including applied forum tags). |
| ThreadDelete | ThreadContext | A thread or forum post is deleted. ctx.thread is degraded to only id/parentId/parentType/isForumPost: name, ownerId and tags are unavailable because the thread is already gone by dispatch time. no ctx.actorId |
| ChannelPinsUpdate | none | A message is pinned or unpinned in a channel (ctx.channelId is the channel). no ctx.actorId |
| ThreadMembersUpdate | ThreadMembersContext | Users join or leave a thread. ctx.threadMembers lists who was added and removed, and the new member count. no ctx.actorId |
Moderation events(7)
| Trigger | Populates | Description & notes |
|---|---|---|
| GuildBanAdd | BanContext | A user is banned. Fires distinctly from GuildMemberRemove; who performed it is in the audit log, not ctx.ban. no ctx.channelId |
| GuildBanRemove | BanContext | A user is unbanned (has no other trigger). no ctx.channelId |
| GuildAuditLogEntryCreate | eventJson (JSON string) | A new audit log entry is created. The entry is in ctx.eventJson, including its changes array (what actually changed, before and after) and the action-specific options; no typed DTO, since audit log entries vary widely by action type. The fields are listed under Context. no ctx.channelId |
| AutoModerationActionExecution | eventJson (JSON string) | Discord's AutoMod takes an action. The payload is in ctx.autoModAction (and, for older scripts, as a JSON string in ctx.eventJson); note the fields are Mallard's camelCase names, not Discord's raw snake_case ones. |
| AutoModRuleCreate | AutoModRuleContext | An AutoMod rule is created. ctx.autoModRule has it. no ctx.actorIdno ctx.channelId |
| AutoModRuleUpdate | AutoModRuleContext | An AutoMod rule is edited or toggled. ctx.autoModRule is the new state. no ctx.actorIdno ctx.channelId |
| AutoModRuleDelete | AutoModRuleContext | An AutoMod rule is deleted. ctx.autoModRule is its last state. no ctx.actorIdno ctx.channelId |
Invites(2)
| Trigger | Populates | Description & notes |
|---|---|---|
| InviteCreate | InviteContext | An invite is created (ctx.invite has the code, inviter, uses and expiry). |
| InviteDelete | InviteContext | An invite is deleted or expires (ctx.invite has only the code + channelId; Discord sends nothing else on delete). no ctx.actorId |
Scheduled events (Discord's native events)(5)
Discord's own "Scheduled Events" feature (an RSVP-able calendar entry on the server), distinct from the Scheduled trigger below, which is Mallard's own timer.
| Trigger | Populates | Description & notes |
|---|---|---|
| GuildScheduledEventCreate | ScheduledEventContext | A scheduled event is created (ctx.scheduledEvent has the full event). no ctx.actorIdno ctx.channelId |
| GuildScheduledEventUpdate | ScheduledEventContext | A scheduled event changes (time, status, details). no ctx.actorIdno ctx.channelId |
| GuildScheduledEventDelete | ScheduledEventContext | A scheduled event is deleted. no ctx.actorIdno ctx.channelId |
| GuildScheduledEventUserAdd | ScheduledEventContext (sparse) | A user RSVPs to a scheduled event. ctx.scheduledEvent carries only id + userId (the subscribing user, also mirrored as ctx.actorId); no name/description/times. no ctx.channelId |
| GuildScheduledEventUserRemove | ScheduledEventContext (sparse) | A user withdraws their RSVP from a scheduled event. no ctx.channelId |
Stage instances(3)
| Trigger | Populates | Description & notes |
|---|---|---|
| StageInstanceCreate | StageContext | A stage channel goes live (ctx.stage has the channel + topic). no ctx.actorId |
| StageInstanceUpdate | StageContext | A live stage changes its topic or privacy level. no ctx.actorId |
| StageInstanceDelete | StageContext | A stage ends. no ctx.actorId |
Server settings(6)
| Trigger | Populates | Description & notes |
|---|---|---|
| GuildUpdate | GuildUpdateContext | Server settings change (ctx.guildUpdate has name, description, ownerId). no ctx.channelId |
| GuildEmojisUpdate | ExpressionSetContext | The server's custom emoji set changes (ctx.expressions.count / names). no ctx.actorIdno ctx.channelId |
| GuildStickersUpdate | ExpressionSetContext | The server's sticker set changes (ctx.expressions.count / names). no ctx.actorIdno ctx.channelId |
| WebhooksUpdate | none | A channel's webhooks change (ctx.channelId is the channel). no ctx.actorId |
| GuildIntegrationsUpdate | none | An integration (a bot, Twitch/YouTube subscription role, or other connection) is added, changed or removed. Discord says only that something changed, so read the current list with getIntegrations(). no ctx.actorIdno ctx.channelId |
| BotInstalled | none | Mallard is added (or re-added) to the server, after the default scripts are seeded. The place for a setup message or seeding your own key-value records. no ctx.actorIdno ctx.channelId |
Voice(2)
| Trigger | Populates | Description & notes |
|---|---|---|
| VoiceStateUpdate | VoiceStateContext | A user joins, leaves, or moves between voice channels, or changes mute/deafen/stream/camera. ctx.voiceState.action says which ("join", "leave", "move" or "update") and previousChannelId where they were before; channelId is empty when the user disconnected. |
| VoiceChannelStatusUpdate | VoiceStatusContext | A voice channel's status text (the line under its name) is set or cleared. ctx.voiceStatus has the channel and new status. no ctx.actorId |
Direct messages(1)
| Trigger | Populates | Description & notes |
|---|---|---|
| DirectMessageCreate | MessageContext | The bot receives a DM. Not guild-scoped: it's fanned out to every guild the sender shares with an enabled script, using the live member cache. Zero matches gets a canned reply, one match dispatches directly, and multiple matches show a server-picker whose buttons route back through the same handling. |
Timers & follow-ups(3)
Mallard's own clock rather than a Discord event: repeating schedules, one-off jobs queued by runLater(), and replies awaited with expectReply().
| Trigger | Populates | Description & notes |
|---|---|---|
| Scheduled | none | Runs on a timer instead of a Discord event: every N minutes, or on a cron schedule in a time zone you pick ("0 9 * * 1" = 09:00 every Monday). The basis for periodic work (feed polling, daily posts, stats channels). For one-off "do this later" work, use runLater() and a Delayed script instead. no ctx.actorIdno ctx.channelId The minimum interval is 5 minutes on every tier, enforced both at save time and again at dispatch time; a cron schedule may not fire more often than that either. ctx.scheduled says when it last ran. |
| Delayed | DelayedContext | Runs when a job another script queued with runLater("thisScriptName", seconds, payload) comes due: temp bans and roles, reminders, giveaway endings, follow-up messages. ctx.delayed carries the payload and who scheduled it. no ctx.actorIdno ctx.channelId Jobs survive restarts and fire within a few seconds of their due time. A job for a script that has been disabled, deleted or renamed is dropped when it comes due. |
| ReplyReceived | ReplyContext | Runs when a user a script is waiting on (expectReply(userId, channelId, "thisScriptName", timeoutSeconds, payload)) posts their next message in that channel, or when the wait times out (ctx.reply.timedOut). The building block for conversational setups ("which channel should I post in?"). |
Script to script(2)
Ways for scripts to build on each other: events one script emits for others to handle, and libraries of shared code.
| Trigger | Populates | Description & notes |
|---|---|---|
| CustomEvent | EventContext | Runs when any script calls emit("eventName", payload) with the event name set on this trigger. One event can reach several scripts without the emitter knowing their names: "ticket.closed" can save a transcript, update stats and post to the mod log from three separate scripts. ctx.event carries the payload and who emitted it. no ctx.actorIdno ctx.channelId Subscribers run within a couple of seconds of the emit, each budgeted like any other run. Events can chain (a CustomEvent script may emit again) at most 3 deep. |
| Library | none | Never runs by itself: code other scripts load with require("libraryName"), which returns what the library put on module.exports. Keeps shared helpers (a mod-log embed, a permissions check) in one place instead of copied into every script. no ctx.actorIdno ctx.channelId A library runs inside the calling script's run, with the full API and its budget. The name passed to require() must be a plain string, since libraries are looked up when the run is queued. A library can require other libraries, up to 5 deep and 10 in total. A library still required by another script can't be deleted. |
Webhooks & feeds(2)
Outside services reaching the server: set up on the Integrations page.
| Trigger | Populates | Description & notes |
|---|---|---|
| IncomingWebhook | WebhookContext | Runs when something sends an HTTP request to the script's own webhook URL: GitHub pushes, Ko-fi or Patreon donations, Stripe payments, uptime monitors, CI results, game servers. ctx.webhook has the method, headers, body (and json / form when it parses), and whether its signature checked out. no ctx.actorIdno ctx.channelId The URL and optional signature check (GitHub, Stripe, Ko-fi or a generic HMAC) are set on the Integrations page. The sender gets 202 as soon as the run is queued, so a script can't choose the HTTP response. Bodies are capped at 256 KB, and each script accepts 60 requests an hour on Free (300 Plus, 1,000 Pro, 3,000 Ultra). |
| FeedItem | FeedContext | Runs for each new item in an RSS or Atom feed the server follows: blogs, subreddits, GitHub releases, Steam news, podcasts, status pages, Mastodon or Bluesky. ctx.feed has the feed and the item. no ctx.actorIdno ctx.channelId Feeds are added on the Integrations page and checked every 60 minutes on Free (30 Plus, 15 Pro, 5 Ultra). Following a feed doesn't replay its history: only items that appear after it's added fire, at most 5 per check. |
Twitch(11)
Requires linking a Twitch channel on the guild's General page. Dispatched by a separate EventSub subsystem, not the Discord gateway.
| Trigger | Populates | Description & notes |
|---|---|---|
| TwitchStreamOnline | TwitchContext | A linked Twitch channel goes live. no ctx.actorIdno ctx.channelId |
| TwitchStreamOffline | TwitchContext | A linked Twitch channel goes offline. no ctx.actorIdno ctx.channelId |
| TwitchChannelUpdate | TwitchContext | A linked Twitch channel changes its title or category. no ctx.actorIdno ctx.channelId |
| TwitchChatBatch | TwitchContext (messages populated) | Buffered Twitch chat messages (and channel-point redemptions/bit cheers) for a linked channel, flushed every ~10 minutes, never per message, so it can't burn through the hourly execution budget. Additionally requires a chat-reader authorization from the streamer or one of their moderators. no ctx.actorIdno ctx.channelId |
| TwitchRaid | TwitchContext | Another channel raids the linked channel. ctx.twitch.user is the raider and ctx.twitch.raid.viewers the party size. no ctx.actorIdno ctx.channelId |
| TwitchFollow | TwitchContext | Someone follows the linked channel. Needs the connected Twitch account to be the streamer or a moderator. no ctx.actorIdno ctx.channelId |
| TwitchSubscription | TwitchContext | A new sub, a resub message, or a gifted batch. ctx.twitch.subscription.kind is "sub", "resub" or "gift". Needs the streamer themselves to connect their Twitch account. no ctx.actorIdno ctx.channelId |
| TwitchCheer | TwitchContext | Someone cheers bits, delivered immediately rather than in the chat batch. Needs the streamer to connect their account. no ctx.actorIdno ctx.channelId |
| TwitchRedemption | TwitchContext | A channel-points reward is redeemed (every reward, not only text-input ones). Needs the streamer to connect their account. no ctx.actorIdno ctx.channelId |
| TwitchHypeTrain | TwitchContext | A hype train starts or ends (ctx.twitch.hypeTrain.phase). Needs the streamer to connect their account. no ctx.actorIdno ctx.channelId |
| TwitchAdBreak | TwitchContext | An ad break starts on the linked channel. Needs the streamer to connect their account. no ctx.actorIdno ctx.channelId |
YouTube(5)
Requires linking one or more YouTube channels on the Integrations page. Dispatched via WebSub push notifications.
| Trigger | Populates | Description & notes |
|---|---|---|
| YouTubeVideoPublished | YouTubeContext | A linked YouTube channel publishes a video or Short (ctx.youtube has details). no ctx.actorIdno ctx.channelId |
| YouTubeVideoUpdated | YouTubeContext | A linked YouTube channel updates a video's title or description. no ctx.actorIdno ctx.channelId |
| YouTubeVideoDeleted | YouTubeContext | A video from a linked YouTube channel is deleted. no ctx.actorIdno ctx.channelId |
| YouTubeStreamOnline | YouTubeContext | A linked YouTube channel starts a live stream. no ctx.actorIdno ctx.channelId |
| YouTubeStreamOffline | YouTubeContext | A linked YouTube channel ends a live stream. no ctx.actorIdno ctx.channelId |
Forms(1)
Dispatched when a user submits a public guild form at /forms/{guildId}/{formId}. Submissions are stored in the key-value store automatically, and the script receives the answers in ctx.form.
| Trigger | Populates | Description & notes |
|---|---|---|
| FormSubmitted | FormContext | A user submits a public guild form at /forms/{guildId}/{formId}. Fires for every form on the guild, so branch on ctx.form.formName (or formId) when you only want one. Not dispatched by a gateway event, so there is no ctx.channelId; ctx.actorId and ctx.actor are set only when the submitter signed in with Discord (null for anonymous and Twitch submitters). no ctx.channelId Submissions are stored in the key-value entry forms:submissions:<formId> whether or not a script is listening, so disabling every FormSubmitted script stops the notifications, not the form. Use the form's own Enabled switch on the Forms tab for that. |
Filters
A trigger says what kind of event runs a script. Filters, set on the script in
the dashboard, say which of those events are worth running it for: only
#support, only messages starting with !ticket, only the ⭐
reaction, only bans.
They are applied before the script is queued, so an event that doesn't match costs no executions, no runtime and no REST actions against your hourly limits, and never reaches the script at all, so there is nothing for it in the logs either. The editor shows only the filters the selected trigger can actually answer.
| Filter | Available on | What it does |
|---|---|---|
| Channels | Messages, reactions, threads, polls, voice, stage, invites, AutoMod, pins, webhooks, and every command or button | Only these channels, or every channel except these. A message posted in a thread also matches the channel the thread hangs under, so naming a forum covers every post in it. Archived threads aren't in the gateway cache, so only their own ID matches. |
| Message content | MessageCreate, MessageUpdate, MessageDelete, DirectMessageCreate, AutoModerationActionExecution | Run only when the message contains one of a list of phrases, or matches a regular expression, or only when it doesn't. Matched against the new text on an edit. Patterns run on a linear-time engine, so lookarounds, backreferences and atomic groups are rejected when you save; use "doesn't match" instead of a negative lookahead. |
| Member roles | Commands and buttons, member joins/leaves/updates, reactions, messages, voice | Only members holding one of these roles, or skip them. On a member update this reads the roles they hold after the change. A member Mallard hasn't cached yet is never filtered out by this; the filter is skipped rather than treated as "no roles". |
| Specific members | Every trigger with a ctx.actorId | Only these members, or ignore them. Handy for muting one noisy integration. |
| Reaction emoji | The four reaction triggers | Only these emoji, or every emoji except these. Give the emoji itself, or a custom emoji's name or ID. Clearing all reactions from a message carries no emoji, so the filter doesn't apply there. |
| Audit log actions | GuildAuditLogEntryCreate | Only these audit actions (MemberBanAdd, ChannelDelete, …). Without it this trigger fires for every administrative action in the server, so it's usually the filter that saves the most runs. |
| Attachments | MessageCreate, MessageUpdate, MessageDelete, DirectMessageCreate | Only messages that have an attachment, or only ones that don't. On a delete this reads the cached copy, so a message older than 3 hours has neither and the filter doesn't apply. |
| Other bots and webhooks | MessageCreate, MessageUpdate, MessageDelete | Off by default: messages from other bots and webhook posts never run a script unless you turn this on. Mallard's own messages never do either way, since that would be a script triggering itself. |
| Cooldown | Every trigger except Scheduled, which already has an interval | A minimum number of seconds between runs, either for the script as a whole or for each member separately. A command used during the cooldown gets a private "on cooldown" reply; an event that arrives during it is discarded rather than queued, so the script just doesn't run for it. |