Triggers
Every script runs on exactly one of these 51 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(8)
Answered by the trigger type, not the script: plain command/button triggers get a deferred "thinking…" ack that the script fills in with editResponse(); the four ": 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 | MemberContext + 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. |
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 |
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 30-minute message cache (null when unknown, e.g. after a restart).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 30-minute message cache: empty strings on a cache miss (message never seen, aged out, or a bot restart happened since). 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(4)
| 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 |
Moderation events(4)
| 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.eventJson, whose fields are listed under Context; note they are Mallard's camelCase names, not Discord's raw snake_case ones. |
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(4)
| 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 |
Voice(1)
| Trigger | Populates | Description & notes |
|---|---|---|
| VoiceStateUpdate | VoiceStateContext | A user joins, leaves, or moves between voice channels. ctx.voiceState.channelId is empty when the user disconnected. |
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. |
Timer(1)
| Trigger | Populates | Description & notes |
|---|---|---|
| Scheduled | none | Runs on a timer instead of a Discord event; the basis for delayed or periodic work (temp-ban sweeps, reminders, feed polling). no ctx.actorIdno ctx.channelId The minimum interval is tier-gated (30/15/10/5 minutes on Free/Plus/Pro/Ultra), enforced both at save time and again at dispatch time, so a downgraded guild's existing scripts still get clamped. |
Twitch(4)
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 |
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: no ctx.channelId/ctx.actorId at all, since the submitter may be anonymous or identified via Twitch. no ctx.actorIdno 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 30 minutes 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. |