The ctx object
Every script has a global ctx available with details about the event that
triggered it. Not every property is populated on every trigger; see
Triggers
for exactly which. The dashboard's script
editor autocompletes only the properties valid for the script you're editing.
Sending messages
Message calls follow discord.js: .send(...) on the channel, member or user
you want to reach, and a bare string means message content. Every trigger can send
somewhere, even the ones with no ctx.channel.
| Call | Where it works |
|---|---|
| ctx.channel.send(options) | Any trigger that has a channel (ctx.channelId is set). Null on member, ban, role and similar server-level events. |
| ctx.message.reply(options) | Message triggers, and the message context commands. Also ctx.message.react(emoji) and ctx.message.delete(). |
| ctx.interaction.reply(options) | Command and component triggers only. See ctx.interaction below. |
| ctx.author.send(options) | Any trigger with ctx.actorId. Sends a DM. Same for ctx.member.send() where a member object is present. |
| ctx.guild.channels.fetch(channelId) | Every trigger. Resolves to a channel object with .send(), .delete() and .messages. This is the discord.js way to message a fixed channel when there is no ctx.channel. |
| ctx.client.channels.fetch(channelId) | Same as above, from the client rather than the guild. Use whichever reads better. |
| getChannel(channelId) | Every trigger. Returns the same channel object, so await getChannel(id).send(...) works. Use fetch above for a more discord.js-shaped call. |
| discord.channels.createMessage(channelId, options) | Every trigger. The raw REST call, if you want to skip the object layer. |
A member joins, leaves or is updated without a channel of its own, so fetch the channel you want to post in first:
// GuildMemberAdd has no ctx.channel, so fetch the channel first.
const channel = await ctx.guild.channels.fetch(ctx.env.welcome_channel_id);
await channel.send(`Welcome ${ctx.member.displayName}!`);
// getChannel() returns the same channel object, so its .send() works too.
const log = await getChannel(ctx.env.mod_log_channel_id);
await log.send("Persistent role check failed.");
Any place that takes options accepts a string (the content) or an
object with content, embeds, buttons,
selects, files and pings:
await ctx.channel.send({
content: "A new member joined",
embeds: [{ title: "Welcome", description: "Say hi!", colorArgb: 5763719 }],
buttons: [{ label: "Verify", style: "primary", handler: "verify" }]
});ctx.interaction
Present on slash commands, context commands, buttons and select menus (including their
: Modal forms). It answers the interaction itself, rather than posting to a
channel.
| Method | What it does |
|---|---|
| ctx.interaction.reply(options) | Writes the interaction's response. On a command that is the message the user sees under their command. |
| ctx.interaction.editReply(options) | Edits that same response, replacing its content, embeds and buttons. |
| ctx.interaction.followUp(options) | Posts an additional message after the reply, without replacing it. |
| ctx.interaction.deleteReply() | Deletes the response. |
const who = ctx.actor?.displayName ?? ctx.actorId;
await ctx.interaction.reply(`Hi ${who}!`);
// Later in the same run, or from the button this reply puts on screen:
await ctx.interaction.editReply({ content: "Updated.", buttons: [] });
await ctx.interaction.followUp({ content: "One more thing." });
The interaction also carries the data the script was invoked with:
ctx.interaction.values (form answers), ctx.interaction.selected
(select-menu picks), ctx.interaction.message (the message a button sits on),
ctx.interaction.customId and ctx.interaction.payload. Full field
list under InteractionContext below.
Top-level properties
55 properties on ctx itself.
The guild's Discord snowflake ID (as a string).
Available on: All triggers
The acting user's Discord snowflake ID (as a string).
Available on: 33 triggers (see Triggers)
The acting user's own member data (username, display name, nickname, avatar URL, roles and join date), so you don't have to call getMember(ctx.actorId) yourself. Available on every trigger that has a ctx.actorId. On commands, buttons and modals it comes straight from Discord and is always current; on other events it is read from the member cache, so it is null for a user the bot hasn't cached (and its discriminator is empty, a field Discord has retired anyway). Note ctx.actor is always the person who acted, whereas on UserCommand ctx.member is the right-clicked target.
Available on: 33 triggers (see Triggers)
The invoking user / author object. Includes .send(options) to send a DM.
Available on: 33 triggers (see Triggers)
Discord client context: ctx.client.users (.fetch, .send) and ctx.client.channels (.fetch).
Available on: All triggers
The channel's Discord snowflake ID (as a string).
Available on: 39 triggers (see Triggers)
The event type that triggered this script.
Available on: All triggers
Key/value map of arguments passed to the script.
Available on: All triggers
Custom environment variables set via the Database page: the single 'env' KV record holds a JSON object whose keys are exposed here (e.g. {"mod_log_channel_id":"123"} becomes ctx.env.mod_log_channel_id). Read-only during script execution.
Available on: All triggers
How many fetch() calls remain in the guild's hourly budget when this execution starts. A fetch() beyond this throws, so size data-dependent loops against it.
Available on: All triggers
Message data. Present on MessageCreate/MessageUpdate/MessageDelete/DirectMessageCreate events and on MessageCommand (the right-clicked target message). On MessageUpdate, content is the edited (new) version and previousContent is what it said before, or null when that isn't known, since the pre-edit text comes from the 3-hour message cache. On MessageDelete, content is the deleted text recovered from that same cache.
Available on: MessageCommand, MessageCommand: Modal, MessageCreate, MessageUpdate, MessageDelete, DirectMessageCreate, ReplyReceived
Member data. Present on GuildMemberAdd/GuildMemberRemove/GuildMemberUpdate events. Also present on UserCommand, where it is the right-clicked target member (ctx.actorId is the invoker).
Available on: UserCommand, UserCommand: Modal, GuildMemberAdd, GuildMemberRemove, GuildMemberUpdate
Reaction data. Present on the four MessageReaction* triggers.
Available on: MessageReactionAdd, MessageReactionRemove, MessageReactionRemoveAll, MessageReactionRemoveEmoji
Bulk delete data. Present on MessageBulkDelete events.
Available on: MessageBulkDelete
Voice state data. Present on VoiceStateUpdate events; channelId is empty when the user disconnected.
Available on: VoiceStateUpdate
Interaction data. Present on command and component triggers. Includes .editReply(options), .followUp(options), and .deleteReply() methods.
Available on: SlashCommand, SlashCommand: Modal, MessageCommand: Modal, UserCommand: Modal, ButtonClick, ButtonClick: Modal, SelectMenu, SelectMenu: Modal
The event's payload as a JSON string. Present for GuildAuditLogEntryCreate and AutoModerationActionExecution, and kept for scripts written before ctx.auditLog / ctx.autoModAction, which carry the same data already parsed. Mallard's own summary, not Discord's raw gateway object: keys are camelCase and snowflakes are strings. AutoModerationActionExecution: userId, channelId, messageId, ruleId, ruleTriggerType, content, matchedContent, matchedKeyword, actionType, alertSystemMessageId, alertChannelId, timeoutDurationSeconds, customMessage (the last four follow actionType: SendAlertMessage fills the alert id + channel, Timeout the duration, BlockMessage the custom message). GuildAuditLogEntryCreate: id, actionType, userId, targetId, reason, changes (array of {key, oldValue, newValue}, where key is Discord's snake_case name and the values keep the JSON type Discord sent), options (action-specific extras such as roleName/channelId/count, null when the action carries none).
Available on: GuildAuditLogEntryCreate, AutoModerationActionExecution
Twitch event data. Present on Twitch* triggers; messages is populated only for TwitchChatBatch (all chat messages from the last ~10 minutes, dispatched as one batch, never per message).
Available on: 11 triggers (see Triggers)
YouTube event data. Present on YouTube* triggers; carries video, channel, stream, and isShort information.
Available on: YouTubeVideoPublished, YouTubeVideoUpdated, YouTubeVideoDeleted, YouTubeStreamOnline, YouTubeStreamOffline
Thread / forum post data. Present on ThreadCreate/ThreadUpdate/ThreadDelete events. isForumPost is true when the thread lives in a forum or media channel; parentId is the channel it was created under, ctx.channelId is the thread itself, and tags holds the forum tags applied to the post (resolved to names, and empty for non-forum threads and on ThreadDelete).
Available on: ThreadCreate, ThreadUpdate, ThreadDelete
Ban data. Present on GuildBanAdd (user was banned) and GuildBanRemove (user was unbanned). Who performed it is in the audit log, not here.
Available on: GuildBanAdd, GuildBanRemove
Invite data. Present on InviteCreate (full invite) and InviteDelete (only code + channelId are available).
Available on: InviteCreate, InviteDelete
Scheduled-event data. Present on the GuildScheduledEvent* triggers. On the Create/Update/Delete triggers the full event is populated; on UserAdd/UserRemove only id and userId are set (the (un)subscribing user).
Available on: GuildScheduledEventCreate, GuildScheduledEventUpdate, GuildScheduledEventDelete, GuildScheduledEventUserAdd, GuildScheduledEventUserRemove
Poll-vote data. Present on MessagePollVoteAdd/MessagePollVoteRemove; answerId identifies which poll answer was voted for or retracted.
Available on: MessagePollVoteAdd, MessagePollVoteRemove
Stage-instance data. Present on StageInstanceCreate/Update/Delete; channelId is the stage voice channel.
Available on: StageInstanceCreate, StageInstanceUpdate, StageInstanceDelete
Updated guild settings. Present on GuildUpdate.
Available on: GuildUpdate
Emoji/sticker set data. Present on GuildEmojisUpdate/GuildStickersUpdate; kind is "emojis" or "stickers", and count/names describe the set after the change.
Available on: GuildEmojisUpdate, GuildStickersUpdate
Submitted form data. Present on FormSubmitted, fired when anyone submits one of the guild's public forms. platform is "Discord", "Twitch" or "Anonymous" depending on the form's identity requirement; submitterId/submitterDisplayName come from that platform. A Discord submitter is also ctx.actorId/ctx.actor; a Twitch submitterId is not a Discord snowflake, so it isn't.
Available on: FormSubmitted
Who is looking at the form, present in a form's page script. Populated only once the visitor has passed the form's sign-in gate, so submitterId/submitterDisplayName are the verified visitor and previousSubmissions lists their earlier answers to this form (newest first). Page scripts run read-only: their return value, a markdown string, is rendered above the form fields.
Available on: FormPage
The server itself: name, icon, owner, member count, boost level, locale. Read from Mallard's guild cache, so it costs nothing.
Available on: All triggers
Mallard in this server: its user ID (to recognise its own messages) and its roles.
Available on: All triggers
The running script's id, name and trigger, e.g. to name itself as a button handler or runLater() target.
Available on: All triggers
This run's ID, as shown on the Logs page. Useful to correlate log lines.
Available on: All triggers
The server's hourly budget as this run started: runtime and fetch calls remaining.
Available on: All triggers
The event's channel. On ChannelCreate/Update/Delete it is the full detail from Discord; elsewhere it comes from the channel cache, which knows id, name, type, category and position (topic, nsfw, slowmode, userLimit and bitrate read as empty/0; call getChannel() for those). Null when the channel isn't cached, and on triggers that have no channel at all (member joins, bans, role changes, …). To message a fixed channel from one of those, use ctx.guild.channels.fetch(channelId) or ctx.client.channels.fetch(channelId), which return a channel with .send().
Available on: 39 triggers (see Triggers)
ChannelUpdate: the channel as the cache held it before (name, type, category, position), or null when it wasn't cached.
Available on: ChannelUpdate
The role a RoleCreate/Update/Delete is about.
Available on: RoleCreate, RoleUpdate, RoleDelete
RoleUpdate: the role as the cache held it before, so a script can diff permissions (null when it wasn't cached).
Available on: RoleUpdate
The invoking user's Discord language, e.g. "en-GB", for replying in it.
Available on: 10 triggers (see Triggers)
The server's primary language.
Available on: 10 triggers (see Triggers)
The command's options with their real types: integer/number options as numbers and boolean options as booleans. ctx.args keeps every value as a string, as it always has.
Available on: SlashCommand, SlashCommand: Modal
The users, members, roles, channels and attachments the command's options or the select menu's picks refer to, keyed by ID.
Available on: SlashCommand, SlashCommand: Modal, SelectMenu, SelectMenu: Modal
The full command as invoked, e.g. "role add" for a subcommand. Equal to the script's name.
Available on: SlashCommand, SlashCommand: Modal
The last word of a subcommand script's name ("add" for "role add"); empty for a top-level command.
Available on: SlashCommand, SlashCommand: Modal
When this Scheduled script last ran, and its schedule.
Available on: Scheduled
The runLater() job that is due, with its payload.
Available on: Delayed
The expectReply() wait that was answered (or timed out).
Available on: ReplyReceived
The new audit-log entry, parsed.
Available on: GuildAuditLogEntryCreate
What AutoMod did, parsed.
Available on: AutoModerationActionExecution
The AutoMod rule that was created, changed or deleted.
Available on: AutoModRuleCreate, AutoModRuleUpdate, AutoModRuleDelete
The voice channel whose status changed, and its new status.
Available on: VoiceChannelStatusUpdate
Who joined and left the thread.
Available on: ThreadMembersUpdate
The emit() this run answers: its name, payload and who emitted it.
Available on: CustomEvent
The HTTP request that reached this script's webhook URL.
Available on: IncomingWebhook
The followed feed and its new item.
Available on: FeedItem
Nested types
The full field list for every object type referenced above.
AttachmentContext
An attachment on a message.
id(string)fileName(string)contentType(string)size(number)url(string)proxyUrl(string): Discord's media-proxy copy of the file.description(string): Alt text (empty when none).isSpoiler(boolean)isAnimated(boolean): True when Discord flags the attachment as an animated image (animated GIF, APNG, WebP or AVIF), which a file extension or MIME type cannot reliably tell you.durationSeconds(number): Length of a voice message (0 for anything else).
MessageContext
Message data, present on message-related events.
id(string)content(string)authorId(string)authorUsername(string)authorDisplayName(string): Discord display name (global_name), falling back to username when unset.authorAvatarUrl(string)channelId(string)attachments(AttachmentContext[])referencedMessageId(string): ID of the message this one replies to (empty when not a reply).isForwarded(boolean): True when the message is a forward.forwardedAttachments(AttachmentContext[]): Uploads carried inside the forwarded snapshot of a forward. A forward has no attachments of its own, so check this alongside attachments when judging uploads.previousContent(string | null): On MessageUpdate, the content before the edit, or null when it isn't known (any other trigger, or a message not in the 3-hour cache).createdAt(string): ISO 8601 time the message was posted. On MessageDelete this comes from the 3-hour cache, so it is empty for an older message.editedAt(string | null): ISO 8601 time of the last edit, or null if it has never been edited.authorIsBot(boolean): Whether the author is a bot or webhook (only ever true with the allowBots filter on).mentionedUserIds(string[]): User IDs the message mentions; a reply's implicit ping counts.mentionedRoleIds(string[])mentionsEveryone(boolean): Whether it pinged @everyone/@here, and the author had permission to.isPinned(boolean)guildId(string): Empty for a DM.type(string): Discord's message type: "Default", "Reply", "GuildMemberJoined" (a join message), "PremiumGuildSubscription" (a boost), "ThreadCreated", "ChannelFollowAdd", "PollResult", "AutoModerationAction" and so on. Filter on it to skip system messages.flags(string[]): Message flags, e.g. "IsVoiceMessage", "SuppressNotifications" (sent with @silent), "SuppressEmbeds", "HasSnapshot" (a forward).webhookId(string): The webhook that posted it (empty when a user or bot did).embeds(EmbedData[]): The message's embeds, in the shape sendMessage() accepts.buttons(ButtonData[]): Buttons on the message. A Mallard button's handler is recovered; other bots' buttons come back with only a label.stickers(StickerSummary[])poll(PollData | null): The poll on the message, if it carries one.referencedMessage(ReferencedMessage | null): The message this one replies to, as Discord sent it inline (null when not a reply, or when the original was deleted).isThread(boolean): Whether the message was posted in a thread or forum post.threadParentId(string): For a message in a thread, the channel the thread belongs to (the forum for a forum post). Empty otherwise.channelType(string): "Text", "Voice", "Announcement", "Thread", "Forum", "Stage", "DM" or "Unknown".reply((options: string | MessageOptions) => Promise<any>): Replies to this message (Discord.js syntax).delete(() => Promise<any>): Deletes this message.react((emoji: string) => Promise<any>): Adds a reaction emoji to this message.
UserContext
Discord user object.
id(string): User snowflake ID.userId(string): User snowflake ID.username(string): Username.displayName(string): Display name.avatarUrl(string): Avatar URL.send((options: string | MessageOptions) => Promise<any>): Sends a direct message (DM) to this user.
ClientUsersManager
Discord client users manager (ctx.client.users).
fetch((userId: string) => Promise<UserContext>): Fetches a user by ID.send((userId: string, options: string | MessageOptions) => Promise<any>): Sends a direct message (DM) to a user.
ClientChannelsManager
Discord client channels manager (ctx.client.channels).
fetch((channelId: string) => Promise<ChannelInfo | null>): Fetches a channel by ID, returning a channel object with .send(), .delete() and .messages. Works on every trigger, including ones with no ctx.channel.
ClientContext
Discord client context (ctx.client).
users(ClientUsersManager): User manager (.fetch, .send).channels(ClientChannelsManager): Channel manager (.fetch). Use this to send from a trigger with no ctx.channel, such as GuildMemberAdd.
AuthorContext
The user who triggered the event.
id(string): User snowflake ID.userId(string): User snowflake ID.send((options: string | MessageOptions) => Promise<any>): Sends a direct message (DM) to this user.
MemberRoleCache
Member role cache helper.
has((roleId: string) => boolean): Checks whether the member has a role.get((roleId: string) => any): Retrieves role data if present.
MemberRoleMethods
Member role helper methods.
add((roleId: string) => Promise<any>): Assigns a role to this member.remove((roleId: string) => Promise<any>): Removes a role from this member.cache(MemberRoleCache): Role cache check (.has, .get).
MemberContext
Member data. ctx.member on member-related events, and ctx.actor anywhere there is a ctx.actorId.
id(string): Member snowflake ID.userId(string)username(string)displayName(string): Nickname, else global display name, else username.discriminator(string): Legacy; Discord retired discriminators. Empty on cache-backed ctx.actor.nickname(string)avatarUrl(string): Server avatar, else global avatar, else Discord's default. Never empty.user(UserContext): Underlying Discord user object.send((options: string | MessageOptions) => Promise<any>): Sends a direct message (DM) to this member.roleIds(string[])roles(MemberRoleMethods): Discord.js member role helper: .add(roleId), .remove(roleId), .cache.has(roleId).kick((reason?: string) => Promise<any>): Kicks this member from the server.ban((options?: BanOptions | string, deleteDays?: number) => Promise<any>): Bans this member from the server. Accepts either a BanOptions object or a string reason.timeout((durationMinutes: number | null, reason?: string) => Promise<any>): Times out this member for N minutes, or 0/null to remove.joinedAt(string): ISO 8601 join timestamp (empty when unknown).isBot(boolean | null): The fields below are null when the member came from Mallard's member cache (getMember/listMembers, and ctx.actor outside commands), which does not store them. Null means "this source can't say", not false.premiumSince(string | null): ISO 8601 time they started boosting, or null if they never have.timeoutUntil(string | null): ISO 8601 timeout expiry. A PAST time means an expired timeout, so compare it against now rather than testing for null.isPending(boolean | null): True while they still have membership screening (the rules gate) to pass.serverMuted(boolean | null)serverDeafened(boolean | null)createdAt(string): ISO 8601 time the Discord ACCOUNT was created (derived from the user ID, so always present). The usual anti-alt check: new Date(ctx.member.createdAt) > Date.now() - 7 * 864e5.flags(MemberFlags | null): Join/onboarding flags. Null when the member came from the member cache.previous(MemberContext | null): GuildMemberUpdate only: the member as the cache held them before this change (roles, nickname, avatar), or null when they weren't cached.addedRoleIds(string[]): GuildMemberUpdate only: roles gained in this change (empty when unknown or none).removedRoleIds(string[]): GuildMemberUpdate only: roles lost in this change (empty when unknown or none).inviteCode(string): GuildMemberAdd only: the invite they joined with, when Discord reports it (needs Manage Server). Empty when unknown, a vanity URL shows as the vanity code.inviterId(string): GuildMemberAdd only: who created that invite (empty when unknown).
VoiceStateContext
Voice state data, present on VoiceStateUpdate events. Discord sends the whole state on every change and never says what changed, so compare against your own stored copy to tell a channel move from someone starting a stream.
userId(string)channelId(string): Voice channel the user is now in; empty when they disconnected.selfMuted(boolean)selfDeafened(boolean)serverMuted(boolean): Muted by a moderator, so they cannot unmute themselves.serverDeafened(boolean)streaming(boolean): Screen-sharing / "going live".video(boolean): Camera on.suppressed(boolean): On a stage channel, in the audience rather than speaking.requestedToSpeakAt(string): ISO 8601 stage "request to speak" time (empty when there is none).previousChannelId(string): The voice channel they were in before this update (empty when they weren't in one, or it isn't known after a restart).action(string): "join" (not in voice → in voice), "leave" (in voice → not), "move" (between channels) or "update" (same channel, mute/deafen/stream/camera changed).
EventContext
An emit() reaching a CustomEvent script (ctx.event).
name(string): The event name.payload(any): What emit() was given (null when nothing).emittedBy(string): Name of the script that called emit().depth(number): 1 for an emit from an ordinary run, 2 when a CustomEvent script emitted it, and so on (at most 3).
WebhookContext
A request to an IncomingWebhook script's URL (ctx.webhook).
method(string): "POST", "PUT" or "GET".query(Record<string, string | string[]>): The URL's query string, parsed.contentType(string)headers(Record<string, string>): Request headers, lower-cased. Authorization, cookies and proxy headers are removed.body(string): The raw body (up to 256 KB).json(any): The body parsed as JSON, or null when it isn't JSON.form(Record<string, string | string[]> | null): A form-urlencoded body, parsed (Ko-fi sends one).verified(boolean): Whether the signature check set on the Integrations page passed. Always false when none is set.receivedAt(string): ISO 8601 time the request arrived.deliveryId(string): The sender's delivery ID from the header set on the Integrations page (empty when none).
FeedContext
A followed RSS/Atom feed and its new item (ctx.feed).
url(string): The feed's URL as it was added.title(string): The feed's own title.item(FeedItem)
ForumTag
A forum tag, applied to a post (ctx.thread.tags) or defined on a forum (getForumTags).
id(string)name(string)emojiName(string): Unicode emoji on the tag (empty when none or when the tag uses a custom emoji).emojiId(string): Custom emoji ID on the tag (empty when none or when the tag uses a unicode emoji).moderated(boolean): Whether only moderators can add/remove this tag.
ThreadContext
Thread / forum post data, present on ThreadCreate, ThreadUpdate and ThreadDelete events.
id(string): ID of the new thread / forum post (same as ctx.channelId).name(string): The thread / forum post title.ownerId(string): User who created it (same as ctx.actorId).parentId(string): Channel the thread was created under (the forum channel for a forum post).parentType(string): Parent channel kind: "Forum", "MediaForum", "Announcement", "Text", or "Unknown".isForumPost(boolean): True when the parent is a forum/media channel, meaning this thread is a forum post.tags(ForumTag[]): Forum tags applied to this post, resolved to names (empty for non-forum threads).archived(boolean): Whether the thread is archived (closed). ThreadUpdate is the only place archiving and reopening are visible. False on ThreadDelete.locked(boolean): Locked: only moderators can unarchive it.autoArchiveDuration(number): Idle minutes before auto-archive: 60, 1440, 4320 or 10080.archivedAt(string): ISO 8601 time of the last archive/unarchive (empty when unknown).messageCount(number): Messages in the thread; Discord stops counting at 50 on older threads.memberCount(number): Members who joined the thread; Discord stops counting at 50.starterMessage(MessageContext | null): ThreadCreate on a forum/media post only: the post's opening message (its body and attachments), fetched when the post is created. Null elsewhere, or if it couldn't be read.
BanContext
Ban data, present on GuildBanAdd / GuildBanRemove events.
userId(string): The banned (GuildBanAdd) or unbanned (GuildBanRemove) user's ID.username(string)displayName(string): Global display name, falling back to the username. There is no nickname: by the time this fires they are not a member any more.avatarUrl(string): Avatar URL, never empty (falls back to Discord's default).isBot(boolean)createdAt(string): ISO 8601 account creation time.
FormContext
Submitted form data, present on FormSubmitted events.
formId(string): The form's GUID, as it appears in its public URL.formName(string): The form's name, e.g. "Ban Appeal".submissionId(string): This submission's GUID, matching the id in forms:submissions:<formId>:<submissionId>.platform(string): Which identity the submitter proved: "Discord", "Twitch", or "Anonymous" on a form that requires none.submitterId(string): The submitter's ID on that platform (not a Discord snowflake for Twitch submitters). Empty when anonymous.submitterDisplayName(string): Empty when anonymous.answers({ [label: string]: string }): Each field's answer, keyed by the field's label exactly as it read when the form was submitted. Labels are unique within a form. A field left blank is present with an empty string.answersText(string): Every answer pre-rendered as "**Label**\nvalue" blocks, for pasting straight into a message or embed.submittedAt(string): ISO 8601 submission time.
FormPageContext
Who is looking at a public form, present in that form's page script.
formId(string): The form's GUID, as it appears in its public URL.formName(string): The form's name.platform(string): Which identity the visitor proved: "Discord", "Twitch", or "Anonymous" on a form that requires none.submitterId(string): The visitor's ID on that platform (not a Discord snowflake for Twitch visitors). Empty when anonymous.submitterDisplayName(string): Empty when anonymous.previousSubmissions(FormPageSubmission[]): This visitor's earlier submissions to this form, newest first. Empty when they have never submitted it.
FormPageSubmission
One earlier submission by the visitor, in ctx.formPage.previousSubmissions.
submissionId(string): This submission's GUID.submittedAt(string): ISO 8601 submission time.answersText(string): Every answer pre-rendered as "**Label**\nvalue" blocks.
InviteContext
Invite data, present on InviteCreate / InviteDelete events.
code(string): The invite code (the part after discord.gg/).channelId(string)inviterId(string): Inviter's user ID (empty on InviteDelete).maxUses(number): Max uses before expiry (0 = unlimited; 0 on InviteDelete).maxAge(number): Lifetime in seconds (0 = never; 0 on InviteDelete).temporary(boolean)expiresAt(string): ISO 8601 expiry time (empty when it never expires or on InviteDelete).uses(number): Times used. Always 0 on InviteCreate; an invite tracker counts with getInvites().createdAt(string): ISO 8601 creation time (empty on InviteDelete).inviterUsername(string): The inviter's username (empty on InviteDelete).inviterDisplayName(string): The inviter's display name, falling back to username (empty on InviteDelete).
ScheduledEventContext
Scheduled-event data, present on the GuildScheduledEvent* triggers.
id(string)name(string)description(string)channelId(string): Voice/stage channel (empty for external-location events).location(string): Physical location for external events (empty otherwise).creatorId(string): Event creator's user ID (empty when unknown or on user add/remove).entityType(string): Entity kind: "StageInstance", "Voice", or "External".status(string): Lifecycle status: "Scheduled", "Active", "Completed", or "Canceled".startsAt(string): ISO 8601 scheduled start time.endsAt(string): ISO 8601 scheduled end time (empty when open-ended).userId(string): The (un)subscribing user (GuildScheduledEventUserAdd/Remove only; empty otherwise).userCount(number): How many members have RSVP'd (0 on the user add/remove triggers).privacyLevel(string): Always "GuildOnly"; Discord has never shipped another value.coverImageUrl(string): Cover image URL (empty when it has none).
PollVoteContext
Poll-vote data, present on MessagePollVoteAdd / MessagePollVoteRemove events.
userId(string)messageId(string)channelId(string)answerId(number): ID of the poll answer that was voted for / removed.
StageContext
Stage-instance data, present on StageInstanceCreate/Update/Delete events.
id(string)channelId(string): The stage voice channel this instance is live in.topic(string)privacyLevel(string): Privacy level: "Public" or "GuildOnly".discoverableDisabled(boolean): Hidden from Stage Discovery.
GuildUpdateContext
The server's settings as they stand after a GuildUpdate. Discord sends the whole guild and never says which field moved, so to detect "the name changed" keep your own copy in the KV store and compare.
name(string)description(string)ownerId(string)iconUrl(string): Empty when unset.bannerUrl(string)vanityUrlCode(string): The discord.gg vanity code (empty when the server has none).premiumTier(number): Boost level, 0-3.boostCount(number)verificationLevel(string): "None", "Low", "Medium", "High" or "VeryHigh".explicitContentFilter(string): "Disabled", "MembersWithoutRoles" or "AllMembers".nsfwLevel(string): "Default", "Explicit", "Safe" or "AgeRestricted".preferredLocale(string): The server's primary locale, e.g. "en-GB".features(string[]): Feature flags, e.g. "COMMUNITY", "ANIMATED_BANNER".afkChannelId(string): Empty when unset.systemChannelId(string)rulesChannelId(string)previous(GuildUpdateContext | null): The settings Mallard last recorded for the server (name, description, icon, banner, owner, vanity, boosts, verification level), so a script can say what changed. Null when nothing was recorded yet; fields Mallard doesn't record are empty.
ExpressionSetContext
Emoji/sticker set data, present on GuildEmojisUpdate / GuildStickersUpdate events.
kind(string): Which set changed: "emojis" or "stickers".count(number): Number of items in the set after the change.names(string[]): Names of the items in the set after the change.ids(string[]): IDs of the items, positionally matching names. Discord sends the whole set rather than a delta, so store this and diff it next time to learn what was added or removed; names alone cannot tell a rename from a replacement.
TwitchChatMessage
A single Twitch chat message inside a TwitchChatBatch.
chatterId(string)chatterLogin(string)chatterDisplayName(string)text(string)rewardId(string): Channel-points reward id when the message was a text-input redemption (empty otherwise).bits(number): Number of bits cheered with this message (0 otherwise).sentAt(string): ISO 8601 timestamp the message was received.
TwitchContext
Twitch event data, present on Twitch* triggers.
broadcasterId(string)broadcasterLogin(string)broadcasterDisplayName(string)title(string): Stream/channel title (TwitchChannelUpdate and TwitchStreamOnline; empty otherwise).category(string): Game/category name (TwitchChannelUpdate and TwitchStreamOnline; empty otherwise).startedAt(string): ISO 8601 stream start time (TwitchStreamOnline only; empty otherwise).streamUrl(string): https://twitch.tv/<login>.thumbnailUrl(string): TwitchStreamOnline: a 1280x720 stream preview URL (empty when Twitch hasn't published one yet).viewerCount(number): TwitchStreamOnline: viewers at the time (usually 0 right at go-live).profileImageUrl(string): The broadcaster's avatar, for an embed.messages(TwitchChatMessage[]): Chat messages from the last ~10 minutes (TwitchChatBatch only; empty otherwise).messagesDropped(number): Messages dropped from this batch because the per-guild buffer overflowed.user(TwitchUser | null): The viewer who raided, followed, subscribed, cheered or redeemed (null on the stream triggers, or an anonymous cheer).raid({ viewers: number } | null): TwitchRaid only.subscription(TwitchSubscription | null): TwitchSubscription only.cheer({ bits: number, message: string, isAnonymous: boolean } | null): TwitchCheer only.redemption(TwitchRedemption | null): TwitchRedemption only.hypeTrain({ phase: string, level: number, total: number, goal: number } | null): TwitchHypeTrain only. phase is "begin" or "end". goal is always 0 on "end" (Twitch doesn't send one).adBreak({ durationSeconds: number, isAutomatic: boolean } | null): TwitchAdBreak only.
YouTubeContext
YouTube event data, present on YouTube* triggers.
videoId(string): 11-character YouTube video ID (e.g. "dQw4w9WgXcQ").url(string): Full watch URL ("https://www.youtube.com/watch?v=...").title(string): Video or live stream title.description(string): Video description or snippet.channelId(string): YouTube channel ID starting with UC.channelTitle(string): Display name of the channel.channelUrl(string): Channel URL.thumbnailUrl(string): Thumbnail image URL.publishedAt(string): ISO 8601 publish timestamp.updatedAt(string?): ISO 8601 updated timestamp (YouTubeVideoUpdated only).deletedAt(string?): ISO 8601 deleted timestamp (YouTubeVideoDeleted only).isShort(boolean): True if the video is a YouTube Short, false for standard videos.isLive(boolean): True if currently live broadcast, false otherwise.liveBroadcastContent(string): Broadcast state ("live", "upcoming", "none", "completed").duration(string): ISO 8601 duration (e.g. "PT12M34S").durationSeconds(number): Video duration in whole seconds (0 for live streams).
InteractionContext
Component interaction data, present on ButtonClick, SelectMenu and the ": Modal" triggers.
customId(string)payload(string): Free-form payload set on the button or select menu that triggered this.userId(string)messageId(string): Message the component was attached to (empty for form submissions).channelId(string)componentType(string): "button", "stringSelect", "userSelect", "roleSelect", "channelSelect", "mentionableSelect" or "modal".selected(string[]): SelectMenu: the picked option values, or snowflake IDs for a user/role/channel/mentionable menu. Empty for buttons.values(Record<string, string | string[]>): Form answers keyed by input id (empty for button clicks). A text input or radio group gives a string, a checkbox "true"/"false", a select or checkbox group an array of the picked values.files(Record<string, AttachmentContext[]>): Files uploaded through a form's "file" inputs, keyed by input id.message(CachedMessage | null): The message the clicked button or menu sits on, as Discord sent it with the click, so a paginator or toggle needs no getMessage() call. Null for form submissions.locale(string): The clicking user's Discord language, e.g. "en-GB".guildLocale(string): The server's primary language.reply((options: string | MessageOptions) => Promise<any>): Replies to the interaction (Discord.js syntax).editReply((options: string | MessageOptions) => Promise<any>): Edits the interaction deferred response (Discord.js syntax).followUp((options: string | MessageOptions) => Promise<any>): Sends a follow-up message to the interaction (Discord.js syntax).deleteReply(() => Promise<any>): Deletes the interaction response (Discord.js syntax).
ChannelMessagesManager
Messages in a channel (channel.messages).
fetch((messageId: string) => Promise<CachedMessage>): Fetches one message by ID.fetchMany((options?: any) => Promise<CachedMessage[]>): Fetches recent messages (same options as discord.channels.getMessages).edit((messageId: string, options: MessageOptions) => Promise<CachedMessage>): Edits a message.delete((messageId: string) => Promise<void>): Deletes a message.react((messageId: string, emoji: string) => Promise<void>): Adds a reaction to a message.
ReactionContext
Reaction data, present on the four MessageReaction* triggers.
userId(string)messageId(string)channelId(string)emojiId(string)emojiName(string)emojiAnimated(boolean): Whether the custom emoji is animated (false for unicode emoji).messageAuthorId(string): Who wrote the reacted-to message. MessageReactionAdd only, so empty on the three removal triggers; a starboard uses it to skip self-reactions.burst(boolean): Whether this was a "super reaction". MessageReactionAdd only.
BulkDeleteContext
Bulk delete data, present on MessageBulkDelete events.
messageIds(string[])count(number)channelId(string)messages(CachedMessage[]): The deleted messages, recovered from the 3-hour cache. A SUBSET of messageIds, oldest-first, and empty when a purge cleared older history: Discord itself sends only the IDs.
ReferencedMessage
The message a reply points at, as Discord includes it with the reply.
id(string)authorId(string)authorUsername(string)authorIsBot(boolean)content(string)
MemberFlags
Discord's per-member join flags.
didRejoin(boolean): They were in the server before and left.completedOnboarding(boolean)startedOnboarding(boolean)bypassesVerification(boolean): Exempt from the verification level.
GuildMembersManager
Guild members manager (ctx.guild.members).
fetch((userId: string) => Promise<MemberContext>): Fetches a member by user ID.ban((userId: string, options?: BanOptions | string, deleteDays?: number) => Promise<any>): Bans a user from the guild. Accepts either a BanOptions object or a string reason.unban((userId: string, reason?: string) => Promise<any>): Unbans a user from the guild.kick((userId: string, reason?: string) => Promise<any>): Kicks a member from the guild.timeout((userId: string, durationMinutes: number | null, reason?: string) => Promise<any>): Times out a member for N minutes, or 0/null to remove.
GuildRolesManager
Guild roles manager (ctx.guild.roles).
fetch(() => Promise<RoleInfo[]>): Fetches all roles in the guild.create((options: RoleCreateOptions) => Promise<RoleInfo>): Creates a role in the guild.delete((roleId: string) => Promise<any>): Deletes a role from the guild.
GuildChannelsManager
Guild channels manager (ctx.guild.channels).
fetch((channelId: string) => Promise<ChannelInfo | null>): Fetches a channel by ID, returning a channel object with .send(), .delete() and .messages. This is the discord.js way to message a fixed channel from a trigger that has no ctx.channel.
GuildContext
The server a script runs in, from Mallard's guild cache (ctx.guild).
id(string)name(string)channels(GuildChannelsManager): Guild channels manager (.fetch).members(GuildMembersManager): Guild members manager (.fetch, .ban, .kick, .timeout).roles(GuildRolesManager): Guild roles manager (.fetch, .create, .delete).iconUrl(string): Empty when unset.ownerId(string)memberCount(number): Members in the member cache: exact, refreshed live.premiumTier(number): Boost level, 0-3.boostCount(number)preferredLocale(string)verificationLevel(string)systemChannelId(string)rulesChannelId(string)features(string[])
BotContext
Mallard itself in this server (ctx.bot).
userId(string): Mallard's user ID, e.g. to tell its own messages apart.roleIds(string[])topRolePosition(number): Position of Mallard's highest role. It can only manage members and roles below this; canModerate() checks it for you.
ScriptInfo
The script that is running (ctx.script).
id(string)name(string)triggerType(string)
BudgetContext
This server's hourly budget as the run started (ctx.budget).
tier(string)runtimeMsPerHour(number)runtimeMsRemaining(number): Runtime left in the rolling hour, before this run. Size bulk loops against it.fetchCallsRemaining(number)concurrentSlots(number)
ResolvedData
The users, members, roles, channels and attachments a command option or select menu refers to, keyed by ID, so a user option needs no getMember() call and works for non-members too.
users(Record<string, UserSummary>)members(Record<string, MemberContext>): Those users who are members of this server.roles(Record<string, RoleSummary>)channels(Record<string, ChannelSummary>)attachments(Record<string, AttachmentContext>): Files uploaded through an attachment option.
ScheduledRunContext
The timer behind a Scheduled run (ctx.scheduled).
lastRunAt(string): ISO 8601 time of the previous run (empty on the first). Everything newer than this is new since last time.intervalMinutes(number): 0 for a cron schedule.cron(string): The cron expression (empty for an interval).timeZone(string): IANA time zone the cron runs in, e.g. "Europe/London".
DelayedContext
The runLater() job behind a Delayed run (ctx.delayed).
jobId(string)scheduledBy(string): Name of the script that called runLater().createdAt(string): ISO 8601.dueAt(string): ISO 8601.payload(any): Whatever was passed to runLater(), already parsed.
ReplyContext
The expectReply() wait behind a ReplyReceived run (ctx.reply).
payload(any): Whatever was passed to expectReply(), already parsed.timedOut(boolean): True when no reply came in time; ctx.message is then null.userId(string)channelId(string)scheduledBy(string): Name of the script that called expectReply().
AutoModRuleContext
An AutoMod rule (ctx.autoModRule).
id(string)name(string)enabled(boolean)triggerType(string): "Keyword", "Spam", "KeywordPreset", "MentionSpam" or "MemberProfile".keywords(string[])regexPatterns(string[])allowList(string[])actions(string[]): "BlockMessage", "SendAlertMessage", "Timeout", "BlockMemberInteraction".exemptRoleIds(string[])exemptChannelIds(string[])creatorId(string)
VoiceStatusContext
A voice channel status change (ctx.voiceStatus).
channelId(string)status(string): The new status text (empty when cleared).
ThreadMembersContext
Who joined or left a thread (ctx.threadMembers).
threadId(string)memberCount(number): Discord stops counting at 50.addedUserIds(string[])removedUserIds(string[])
AutoModActionContext
An AutoMod action (ctx.autoModAction).
userId(string)channelId(string)messageId(string)ruleId(string)ruleTriggerType(string)content(string)matchedContent(string)matchedKeyword(string)actionType(string)alertSystemMessageId(string)alertChannelId(string)timeoutDurationSeconds(number)customMessage(string)
TwitchUser
A Twitch viewer on a Twitch event.
id(string)login(string)displayName(string)
TwitchSubscription
A Twitch sub, resub or gift (ctx.twitch.subscription).
kind(string): "sub", "resub" or "gift".tier(string): "1000", "2000" or "3000" (Twitch's tier codes).isGift(boolean): A sub someone else paid for (kind "sub").cumulativeMonths(number): Resub: total months subscribed.streakMonths(number): Resub: current streak (0 when they hid it).message(string): Resub: the message they shared.total(number): Gift: how many subs were gifted in this batch.isAnonymous(boolean): Gift: gifter hidden (ctx.twitch.user is null).
TwitchRedemption
A channel-points redemption (ctx.twitch.redemption).
rewardId(string)rewardTitle(string)cost(number)userInput(string): Text the viewer typed, for rewards that ask for it.status(string): "unfulfilled", "fulfilled" or "canceled".