Script functions
162 global functions available to scripts, grouped by category. Almost all are available on every trigger (the interaction replies only where there is an interaction to answer); the dashboard's autocomplete only offers what's valid for the script you're editing. Where Discord requires Mallard to hold a permission, it is listed, and the Scripts page warns when Mallard's role lacks one a script needs.
Functions
Moderation(13)
ban(userId, reason, deleteMessageDays?): boolean
Bans a user from the guild (members and non-members alike). For a temporary ban, queue the unban with runLater() and a Delayed script that calls unban(), rather than sweeping a list on a timer.
Mallard needs: BanUsersuserId(string): Discord snowflake ID of the user to ban.reason(string): Reason for the ban.deleteMessageDays(number, optional): Days of their messages to delete, 0-7 (fractions allowed, e.g. 0.5 for 12 hours). Values outside 0-7 are clamped. Defaults to 0.
boolean: Always true once the ban is applied.kick(userId, reason): boolean
Kicks a user from the guild. Returns false (rather than failing the script) if they had already left.
Mallard needs: KickUsersuserId(string): Discord snowflake ID of the user to kick.reason(string): Reason for the kick.
boolean: true if the user was kicked, false if they weren't in the guild.timeout(userId, reason, durationMinutes): boolean
Times out a user in the guild. Returns false (rather than failing the script) if they had already left. The duration is in minutes but may be fractional, so parseDuration("30s") (0.5) works; Discord's maximum is 28 days (40320 minutes), and a longer value throws. 0 ends a timeout, like untimeout().
Mallard needs: ModerateUsersuserId(string): Discord snowflake ID of the user to time out.reason(string): Reason for the timeout.durationMinutes(number): Duration in minutes (fractions allowed, up to 40320).
boolean: true if the timeout was applied, false if the user wasn't in the guild.unban(userId, reason): boolean
Unbans a user from the guild. Returns false (rather than failing the script) if they weren't banned.
Mallard needs: BanUsersuserId(string): Discord snowflake ID of the user to unban.reason(string): Reason for the unban.
boolean: true if a ban was lifted, false if the user wasn't banned.untimeout(userId, reason): boolean
Clears a member's timeout early. Prefer this over timeout(userId, reason, 0): both end the timeout, but only this clears the expiry, so the member stops reading as timed-out in ctx.member.timeoutUntil and in Discord's UI.
Mallard needs: ModerateUsersuserId(string): Discord snowflake ID of the member.reason(string): Audit log reason.
boolean: False when the user is no longer a member of the server.getBans(limit?): BanSummary[]
Returns the server's ban list (userId, username, displayName, reason), newest first. Live REST read.
Mallard needs: BanUserslimit(number, optional): How many to return (1-1000; defaults to 100).
BanSummary[]: Array of bans, capped at the requested limit.isBanned(userId): boolean
Whether a user is currently banned from the server. Cheaper and clearer than scanning getBans(). Live REST read.
Mallard needs: BanUsersuserId(string): Discord snowflake ID of the user.
boolean: True when the user is banned.bulkBan(userIds, deleteMessageSeconds?, reason?): BulkResult
Bans up to 200 users in one call, the raid-cleanup primitive: far faster than 200 ban() calls. Returns who was banned and who couldn't be (already banned, or above Mallard in the role list).
Mallard needs: BanUsers, ManageGuilduserIds(string[]): Up to 200 user IDs.deleteMessageSeconds(number, optional): Seconds of their messages to delete (0-604800, i.e. up to 7 days). Defaults to 0.reason(string, optional): Audit log reason.
BulkResult: {succeeded, failed} user IDs.pauseInvites(hours, reason?): void
Pauses joining through invites for up to 24 hours (Discord's own raid-mode switch, shown to members as "invites paused"). 0 resumes them. The other pause (DMs) is left as it is.
Mallard needs: ManageGuildhours(number): How long to pause (0-24; fractions allowed). 0 resumes.reason(string, optional): Audit log reason.
pauseDms(hours, reason?): void
Stops members who aren't friends DMing each other through the server for up to 24 hours, which cuts off a raid's DM spam. 0 resumes. Moderators and bots are unaffected.
Mallard needs: ManageGuildhours(number): How long to pause (0-24; fractions allowed). 0 resumes.reason(string, optional): Audit log reason.
setVerificationLevel(level, reason?): void
Changes the server's verification level, raid mode's other half: "high" makes new accounts wait 10 minutes in the server before talking.
Mallard needs: ManageGuildlevel(string): "none", "low", "medium", "high" or "veryHigh".reason(string, optional): Audit log reason.
getAuditLog(query?): AuditLogEntry[]
Reads recent audit-log entries, newest first: who banned someone, who deleted a channel. Filter with { actionType: "GuildUserBanAdd", userId, targetId, limit }.
Mallard needs: ViewAuditLogquery({ actionType?: string, userId?: string, targetId?: string, limit?: number }, optional): actionType uses the names in AuditLogEntry.actionType; userId is who acted; limit is 1-100 (default 25).
AuditLogEntry[]: Matching entries.canModerate(userId): boolean
Whether Mallard can act on a member at all: they aren't the owner, and their highest role sits below Mallard's. Check it before ban/kick/timeout/role changes to give a clear message instead of a failed call.
userId(string): Discord snowflake ID of the member.
boolean: True when Mallard outranks them.Channel(19)
setSlowmode(channelId, seconds, reason?): void
Sets the slowmode delay for a channel.
Mallard needs: ManageChannelschannelId(string): Discord snowflake ID of the channel.seconds(number): Slowmode delay in seconds (0 to disable, up to 21600).reason(string, optional): Audit log reason.
getForumTags(channelId): ForumTag[]
Returns the tags defined on a forum or media channel, meaning the set a post can be tagged with, resolved to id/name/emoji. Pair with ctx.thread.tags (the tags actually applied to a new post) on a ThreadCreate script, or call it from any trigger. Live read (not cached); throws if the channel is not a forum/media channel.
channelId(string): Discord snowflake ID of the forum or media channel.
ForumTag[]: Array of the forum's available tags.createChannel(name, type, parentIdOrOptions?, topic?, reason?): string
Creates a channel in the guild and returns its snowflake ID. Pass an options object as the third argument to set everything at once, including permission overwrites, which is how a private ticket channel is made in a single step: createChannel("ticket-42", "text", { parentId, overwrites: [{ targetId: ctx.guildId, deny: ["ViewChannel"] }, { targetId: ctx.actorId, targetIsRole: false, allow: ["ViewChannel", "SendMessages"] }] }).
Mallard needs: ManageChannelsname(string): Name for the new channel.type(string): Channel kind: "text", "voice", "category", "announcement", "forum", "media", or "stage".parentIdOrOptions(string | ChannelOptions, optional): Category ID to nest under (empty for none), or an options object: {parentId, topic, position, nsfw, slowmode, userLimit, bitrate, overwrites: [{targetId, targetIsRole?, allow?, deny?}]}.topic(string, optional): Channel topic/description (empty for none).reason(string, optional): Audit log reason.
string: The created channel's snowflake ID.deleteChannel(channelId, reason?): void
Deletes a channel (or thread) in the guild.
Mallard needs: ManageChannelschannelId(string): Discord snowflake ID of the channel to delete.reason(string, optional): Audit log reason.
modifyChannel(channelId, nameOrOptions, topic?, reason?): void
Changes a channel. The short form renames and/or re-topics it: an empty name leaves the name alone, while an empty topic CLEARS the topic (pass null or omit it to leave the topic alone). Pass an options object as the second argument to change anything else: modifyChannel(id, { parentId, position, userLimit, bitrate, nsfw, slowmode, syncPermissions: true }). Works on threads too (name, slowmode, autoArchiveMinutes).
Mallard needs: ManageChannelschannelId(string): Discord snowflake ID of the channel.nameOrOptions(string | ChannelOptions): New name (empty to leave unchanged), or an options object: {name, topic, parentId, position, nsfw, slowmode, userLimit, bitrate, syncPermissions, autoArchiveMinutes}.topic(string | null, optional): New topic (empty to clear, null/omitted to keep).reason(string, optional): Audit log reason.
setChannelTopic(channelId, topic, reason?): void
Sets a channel's topic/description.
Mallard needs: ManageChannelschannelId(string): Discord snowflake ID of the channel.topic(string): The new topic (empty to clear).reason(string, optional): Audit log reason.
listChannels(): ChannelSummary[]
Lists the guild's cached channels (id, name, type, category, position; no threads). Cheap cached read (no REST), like listMembers. type and parentId fill in when the bot next reconnects for channels cached before this field existed.
ReturnsChannelSummary[]: Array of the guild's channels.renameChannel(channelId, name, reason?): void
Renames a channel or category. Does nothing when the channel already has that name, so a scheduled rename (a member-count channel, say) doesn't spend Discord's limit of about 2 renames per 10 minutes per channel.
Mallard needs: ManageChannelschannelId(string): Discord snowflake ID of the channel or category.name(string): The new name.reason(string, optional): Audit log reason.
getChannel(channelId): ChannelInfo | null
Returns one channel's detail (name, type, topic, parent, position, nsfw, slowmode, user limit, bitrate), or null when it doesn't exist. Read from the bot's channel cache, which gateway events keep current; a REST read only for a channel the cache hasn't fully recorded yet.
channelId(string): Discord snowflake ID of the channel.
ChannelInfo | null: The channel, or null when it no longer exists.setChannelPermission(channelId, targetId, allow, deny, reason?, targetIsRole?): boolean
Sets or clears one permission overwrite on a channel: the primitive behind a lockdown or raid-mode script, since slowmode alone cannot stop posting. Permissions named in neither list are left to inherit. Passing BOTH lists empty removes the overwrite entirely, which is how a lockdown is lifted, and is different from denying nothing. To lock a channel, deny ["SendMessages"] for the @everyone role, whose ID is the server's own ID (ctx.guildId).
Mallard needs: ManageRoleschannelId(string): Discord snowflake ID of the channel.targetId(string): Role ID (or user ID when targetIsRole is false). Use ctx.guildId for @everyone.allow(string[]): Permission names to explicitly allow, e.g. ["SendMessages"]. Empty for none. Discord's own spelling ("SEND_MESSAGES") works too.deny(string[]): Permission names to explicitly deny. Empty for none.reason(string, optional): Audit log reason.targetIsRole(boolean, optional): True (the default) when targetId is a role; false for a per-member overwrite.
boolean: True once applied.getEmojis(): EmojiSummary[]
Returns the server's custom emoji (id, name, animated, available, mention, imageUrl). The mention field is the "<:name:id>" form you paste into message content. Live REST read.
ReturnsEmojiSummary[]: Array of the server's custom emoji.deleteEmoji(emojiId, reason?): boolean
Deletes a custom emoji from the server.
Mallard needs: ManageGuildExpressionsemojiId(string): Discord snowflake ID of the emoji.reason(string, optional): Audit log reason.
boolean: False when the emoji was already gone.getChannelPermissions(channelId): PermissionOverwrite[]
Lists a channel's permission overwrites: which roles and members are explicitly allowed or denied what. Useful to check whether a channel is locked, or to copy its permissions.
channelId(string): The channel.
PermissionOverwrite[]: The channel's overwrites.setVoiceStatus(channelId, status): void
Sets the status line shown under a voice channel's name (up to 500 characters), e.g. "🎮 Game night". Empty clears it.
Mallard needs: SetVoiceChannelStatuschannelId(string): The voice channel.status(string): The status text (empty to clear).
createEmoji(name, mediaId, reason?): EmojiSummary
Creates a custom emoji from an image on the server's Media page (PNG, JPEG, WebP or GIF, 256 KB at most) and returns it. Only the Media page is a source: Mallard never fetches an arbitrary URL for this.
Mallard needs: CreateGuildExpressionsname(string): Emoji name (2-32 letters, numbers or underscores).mediaId(string): ID of an image on the Media page.reason(string, optional): Audit log reason.
EmojiSummary: The created emoji.getIntegrations(): IntegrationSummary[]
Lists the server's integrations: bots, Twitch/YouTube subscription roles and other connections. Pair with the GuildIntegrationsUpdate trigger.
Mallard needs: ManageGuild ReturnsIntegrationSummary[]: The server's integrations.startStage(channelId, topic, options?): string
Starts a stage in a stage channel with a topic and returns the stage instance's ID. options.notify pings @everyone that the stage started.
Mallard needs: ManageChannels, MuteUsers, MoveUserschannelId(string): The stage channel.topic(string): What the stage is about (1-120 characters).options({ notify?: boolean }, optional): Extras.
string: The stage instance ID.modifyStage(channelId, topic): void
Changes the topic of a live stage.
Mallard needs: ManageChannels, MuteUsers, MoveUserschannelId(string): The stage channel.topic(string): The new topic.
endStage(channelId): boolean
Ends the live stage in a stage channel.
Mallard needs: ManageChannels, MuteUsers, MoveUserschannelId(string): The stage channel.
boolean: False when no stage was live.Threads(11)
setForumTags(threadId, tagIds, reason?): void
Replaces the forum tags applied to a forum/media post. tagIdsJson is a JSON array of tag ID strings (e.g. from getForumTags or ctx.thread.tags); an empty array clears all tags.
Mallard needs: ManageThreadsthreadId(string): Discord snowflake ID of the forum post (thread).tagIds(string[] | string): Tag IDs to apply, as an array or a JSON array string.reason(string, optional): Audit log reason.
archiveThread(threadId, archived, reason?): void
Archives or unarchives a thread / forum post.
Mallard needs: ManageThreadsthreadId(string): Discord snowflake ID of the thread.archived(boolean): true to archive, false to unarchive.reason(string, optional): Audit log reason.
lockThread(threadId, locked, reason?): void
Locks or unlocks a thread / forum post (locked threads can't get new messages).
Mallard needs: ManageThreadsthreadId(string): Discord snowflake ID of the thread.locked(boolean): true to lock, false to unlock.reason(string, optional): Audit log reason.
deleteThread(threadId, reason?): void
Deletes a thread / forum post.
Mallard needs: ManageThreadsthreadId(string): Discord snowflake ID of the thread to delete.reason(string, optional): Audit log reason.
createThread(channelId, name, isPrivate, userIds?, initialMessage?, options?): string
Creates a thread under a channel, optionally adding initial members and an opening message. Returns the created thread's snowflake ID. On a forum or media channel this creates a post, using initialMessage (or the name, when empty) as its body; createForumPost() gives more control there. To start a thread from an existing message, use createThreadFromMessage().
Mallard needs: CreatePublicThreads, CreatePrivateThreadschannelId(string): Discord snowflake ID of the parent channel.name(string): Thread name.isPrivate(boolean): Create a private thread.userIds(string | string[], optional): User IDs to add, as an array or a comma-separated string (empty for none).initialMessage(string, optional): Message posted into the thread after creation, or empty string.options({ autoArchiveMinutes?: number, slowmode?: number, tagIds?: string[], reason?: string }, optional): autoArchiveMinutes is 60, 1440, 4320 or 10080; tagIds applies to forum posts.
string: The created thread's snowflake ID.addThreadMember(threadId, userId): void
Adds a user to a thread.
threadId(string): Discord snowflake ID of the thread.userId(string): Discord snowflake ID of the user.
removeThreadMember(threadId, userId): void
Removes a user from a thread.
threadId(string): Discord snowflake ID of the thread.userId(string): Discord snowflake ID of the user.
createForumPost(forumId, title, content, options?): string
Creates a post in a forum or media channel and returns its thread ID. The body is a string or a MessageOptions object, so a post can carry embeds, buttons and files; options.tagIds applies forum tags (see getForumTags()).
Mallard needs: SendMessages, CreatePublicThreadsforumId(string): The forum or media channel.title(string): The post title.content(string | MessageOptions): The opening message.options({ tagIds?: string[], autoArchiveMinutes?: number, slowmode?: number, reason?: string }, optional): Extras.
string: The new post's thread ID.createThreadFromMessage(channelId, messageId, name, options?): string
Starts a thread from an existing message (the "Create Thread" item on a message) and returns the thread ID. Auto-thread every post in a channel from a MessageCreate script.
Mallard needs: CreatePublicThreadschannelId(string): The channel the message is in.messageId(string): The message to start from.name(string): Thread name (1-100 characters).options({ autoArchiveMinutes?: number, slowmode?: number, reason?: string }, optional): Extras.
string: The new thread's ID.listActiveThreads(parentId?): ThreadSummary[]
Lists the server's active (unarchived) threads and forum posts, optionally only those under one channel. For stale-ticket sweeps and forum housekeeping.
parentId(string, optional): Only threads in this channel or forum.
ThreadSummary[]: The active threads.getThreadMembers(threadId): string[]
Returns the user IDs who have joined a thread (up to 1000).
threadId(string): The thread.
string[]: Member user IDs.Messaging(15)
sendMessage(channelId, content, embeds?, buttons?, pings?, replyToMessageId?): string
Sends a message to a channel and returns its ID, so you can edit, pin or react to it later (store the ID in the KV store for a panel you keep updated). Pass embeds and/or buttons to attach them; both are plain arrays, no JSON.stringify needed. For select menus, files, stickers or @silent, pass a MessageOptions object as the second argument instead of the content: sendMessage(channelId, { content, selects, files, silent: true }).
Mallard needs: SendMessageschannelId(string): Discord snowflake ID of the channel.content(string | MessageOptions): Message content (empty string for embed-only), or a MessageOptions object carrying everything.embeds(EmbedData[], optional): Up to 10 embeds. A single embed object is also accepted.buttons(ButtonData[], optional): Buttons to attach, up to 25 (5 per row).pings(boolean, optional): Whether @mentions in the content actually notify (ping). Defaults to true; pass false to send a silent mention (still shows as a tag, no notification).replyToMessageId(string, optional): Send this as a reply to a message in the same channel. If that message has since been deleted, the message still posts, just without the reply. To repost a message elsewhere instead, use forwardMessage.
string: The sent message's snowflake ID.sendEmbed(channelId, title, description, colorArgb?): string
Sends a single embed to a channel, as a shorthand for sendMessage with one embed object, and returns the message ID. The description is the body text and is required, since Discord rejects an embed carrying neither a title nor a description; pass an empty string as the title for none. colorArgb is the stripe down the left edge as a decimal number (e.g. 15548997 for red). For several embeds, fields, an author or footer, a timestamp or an image, use sendMessage.
Mallard needs: SendMessages, EmbedLinkschannelId(string): Discord snowflake ID of the channel.title(string): Embed title (empty string for none).description(string): Embed body text.colorArgb(number, optional): Colour as a decimal number (omit for none).
string: The sent message's snowflake ID.forwardMessage(sourceChannelId, messageId, targetChannelId): string
Forwards an existing message into another channel, the same way the Discord client's Forward does: the original is shown as a snapshot, with no content of your own attached. Returns the forward's message ID. Both channels must belong to this guild. Discord refuses to forward polls, calls, and system messages, and refuses any message whose content the bot can't read (error 160014); either ends the script unless caught, so guard with getMessage first if the source might be gone. To add a comment, send it as a separate sendMessage.
Mallard needs: SendMessages, ReadMessageHistorysourceChannelId(string): Discord snowflake ID of the channel holding the message to forward (must be in this guild).messageId(string): Discord snowflake ID of the message to forward.targetChannelId(string): Discord snowflake ID of the channel to forward it into (must be in this guild).
string: The forward's message ID.forwardDm(targetChannelId): string
Forwards the DM that triggered this script into one of this guild's channels or threads, exactly as the Discord client's Forward does: the original appears as a snapshot carrying its text, attachments and embeds, so nothing has to be copied or re-linked. Returns the forward's message ID. DirectMessageCreate only, and it deliberately takes no source arguments: the DM being forwarded is always the one that triggered the run. Like forwardMessage it carries no content of its own (Discord refuses that, error 160011), so send the details embed and any buttons as a separate sendMessage. A target thread that has been deleted or archived fails the call, which ends the script unless you catch it.
targetChannelId(string): Discord snowflake ID of the channel or thread to forward the DM into (must be in this guild).
string: The forward's message ID.sendDm(userId, content, embeds?, buttons?, pings?): boolean
Sends a direct message to a member of this server. DMs always carry an embed attributing them to this server. Only current members can be messaged (DM before you kick or ban, as the seeded /ban does); anyone else returns false. Whether a user accepts DMs is their own privacy setting, so a refused DM also returns false rather than failing the script. Check the result if you want to say so.
userId(string): Discord snowflake ID of the user.content(string | MessageOptions): Message content (empty string for embed-only), or a MessageOptions object.embeds(EmbedData[], optional): Up to 10 embeds. A single embed object is also accepted.buttons(ButtonData[], optional): Buttons to attach, up to 25 (5 per row).pings(boolean, optional): Whether @mentions in the content actually notify (ping). Defaults to true; pass false to send a silent mention.
boolean: true if the DM was delivered; false if the user isn't a member of this server, doesn't accept DMs from it, or has blocked the bot.editResponse(content, embeds?, buttons?, pings?): stringinteraction triggers only
Edits the bot's reply to the interaction and returns its message ID. The reply is already showing a "thinking…" state by the time the script runs, so this fills it in. Each call fully replaces the message: embeds and buttons you don't pass are removed. Whether only the invoking user sees it is the script's Ephemeral setting. On a button/select trigger set to "Update the clicked message", there is no new reply: this rewrites the message the button or menu sits on (pagination, toggles). For a second message, use followUp().
content(string | MessageOptions): Message content (empty string for embed-only), or a MessageOptions object.embeds(EmbedData[], optional): Up to 10 embeds. A single embed object is also accepted.buttons(ButtonData[], optional): Buttons to attach, up to 25 (5 per row).pings(boolean, optional): Whether @mentions in the content actually notify (ping). Defaults to true; pass false to send a silent mention.
string: The response message's snowflake ID.sendWebhookMessage(channelId, username, avatarUrl, content, embeds?, pings?, options?): string
Sends a message through a channel webhook with a custom username and avatar (persona messages) and returns its ID, which editWebhookMessage()/deleteWebhookMessage() take. The webhook is created on first use and belongs to Mallard, so its messages can carry buttons and select menus like any other. channelId may be a thread, in which case the webhook is created on its parent channel and the message is posted into the thread; to start a new forum post instead, pass the forum's ID with options.threadName.
Mallard needs: ManageWebhookschannelId(string): Discord snowflake ID of the channel, or a thread.username(string): Display name for the webhook message.avatarUrl(string): Avatar image URL (empty string for the default).content(string): Message content.embeds(EmbedData[], optional): Up to 10 embeds. A single embed object is also accepted.pings(boolean, optional): Whether @mentions in the content actually notify (ping). Defaults to true; pass false to send a silent mention.options({ threadName?: string, tagIds?: string[], buttons?: ButtonData[], selects?: SelectMenuData[], files?: FileData[], silent?: boolean }, optional): Extras: threadName (with a forum channelId) starts a new post, tagIds tags it, plus components, files and @silent.
string: The sent message's snowflake ID.sendPoll(channelId, question, answers, durationHours?, allowMultiselect?): string
Sends a native Discord poll to a channel and returns its message ID (what endPoll() and getPollResults() take). answers is an array of 1-10 answers, each a string or { text, emoji }; a JSON array string still works.
Mallard needs: SendPollschannelId(string): Discord snowflake ID of the channel.question(string): The poll question.answers((string | { text: string, emoji?: string })[] | string): The answers (1-10).durationHours(number, optional): How long the poll runs, in hours (1-768; defaults to 24).allowMultiselect(boolean, optional): Whether voters can pick multiple answers. Defaults to false.
string: The poll message's snowflake ID.endPoll(channelId, messageId): void
Immediately closes a poll the bot posted.
channelId(string): Discord snowflake ID of the channel.messageId(string): Discord snowflake ID of the poll message.
followUp(content, options?): stringinteraction triggers only
Sends an additional message to the interaction, after the first reply, and returns its ID. Unlike editResponse() it can be ephemeral or public whatever the script's Ephemeral setting: followUp("Only you can see this", { ephemeral: true }). Up to 15 minutes after the interaction.
content(string | MessageOptions): Content, or a MessageOptions object.options(MessageOptions, optional): Extras when content is a string, e.g. { ephemeral: true, embeds }.
string: The follow-up message's snowflake ID.deleteResponse(): voidinteraction triggers only
Deletes the interaction's reply (the "thinking…" message or what editResponse() put there), e.g. when the real result went out as a followUp() or a channel message. On an "Update the clicked message" trigger this deletes the clicked message itself.
triggerTyping(channelId): void
Shows "Mallard is typing…" in a channel for about 10 seconds, or until the next message it sends. Cosmetic, for replies that take a while (an AI call through fetch()).
Mallard needs: SendMessageschannelId(string): Discord snowflake ID of the channel.
editWebhookMessage(channelId, messageId, content, embeds?): void
Edits a message sendWebhookMessage() posted. Like editMessage it replaces the message wholesale.
Mallard needs: ManageWebhookschannelId(string): The channel (or thread) it was posted in.messageId(string): The ID sendWebhookMessage() returned.content(string | MessageOptions): New content, or a MessageOptions object.embeds(EmbedData[], optional): Replacement embeds.
deleteWebhookMessage(channelId, messageId): boolean
Deletes a message sendWebhookMessage() posted.
Mallard needs: ManageWebhookschannelId(string): The channel (or thread) it was posted in.messageId(string): The ID sendWebhookMessage() returned.
boolean: False when it was already gone.getStickers(): StickerSummary[]
Returns the server's stickers, whose IDs MessageOptions.stickerIds takes. Live REST read.
ReturnsStickerSummary[]: The server's stickers.Messages(18)
getMessage(channelId, messageId): CachedMessage | null
Fetches a single message, or null if it no longer exists. A message the bot saw in the last 3 hours comes from its message cache; anything older, and any poll, is fetched live over REST, and that call's time counts toward the run like any other. The returned message's reactions field is a per-emoji count/me summary (see getReactionUsers for who reacted). The channel must belong to this guild.
channelId(string): Discord snowflake ID of the channel (must be in this guild).messageId(string): Discord snowflake ID of the message.
CachedMessage | null: The message, or null when it doesn't exist.getMessages(channelId, max?, options?): CachedMessage[]
Fetches a channel's messages from Discord live over REST, newest first (up to 100 per call). With no options it returns the most recent ones; pass { before: messageId } to page further back, { after: messageId } to get newer ones, or { around: messageId } for context around a message. Each message's reactions field is a per-emoji count/me summary. The channel must belong to this guild.
Mallard needs: ReadMessageHistorychannelId(string): Discord snowflake ID of the channel (must be in this guild).max(number, optional): Maximum messages to return (1-100; defaults to 100).options({ before?: string, after?: string, around?: string }, optional): Page from a message ID instead of the newest message.
CachedMessage[]: Newest-first array of messages (empty when the channel has none).deleteMessage(channelId, messageId, reason?): boolean
Deletes a single message. Returns false (rather than failing the script) if it was already gone.
Mallard needs: ManageMessageschannelId(string): Discord snowflake ID of the channel.messageId(string): Discord snowflake ID of the message.reason(string, optional): Audit log reason.
boolean: true if the message was deleted, false if it no longer existed.bulkDelete(channelId, count, reason?): number
Deletes the most recent N messages in a channel (1-100) and returns how many went. Messages older than 14 days are deleted individually, which is slower. To delete only some (one user's, or ones containing a word), use purge().
Mallard needs: ManageMessages, ReadMessageHistorychannelId(string): Discord snowflake ID of the channel.count(number): Number of recent messages to delete (1-100).reason(string, optional): Audit log reason.
number: How many messages were deleted.addReaction(channelId, messageId, emoji): void
Adds a reaction to a message as the bot. Emoji is a unicode emoji ("🦆") or a custom emoji as "name:id".
Mallard needs: AddReactions, ReadMessageHistorychannelId(string): Discord snowflake ID of the channel.messageId(string): Discord snowflake ID of the message.emoji(string): Unicode emoji or custom emoji as "name:id".
removeReaction(channelId, messageId, emoji, userId?): void
Removes reactions for an emoji from a message: one user's reaction when userId is set, or every user's reaction for that emoji when userId is empty or omitted.
Mallard needs: ManageMessageschannelId(string): Discord snowflake ID of the channel.messageId(string): Discord snowflake ID of the message.emoji(string): Unicode emoji or custom emoji as "name:id".userId(string, optional): User whose reaction to remove, or empty/omitted for all users.
clearReactions(channelId, messageId): void
Removes every reaction from a message.
Mallard needs: ManageMessageschannelId(string): Discord snowflake ID of the channel.messageId(string): Discord snowflake ID of the message.
getReactionUsers(channelId, messageId, emoji, max?, options?): string[]
Returns the user IDs who reacted to a message with a specific emoji, up to 1000 per call (paged from Discord 100 at a time), which is enough for most giveaways; pass { after: lastUserId } to continue past that. Discord's endpoint is per-emoji, so read the emoji list off getMessage()'s reactions summary first. The channel must belong to this guild.
Mallard needs: ReadMessageHistorychannelId(string): Discord snowflake ID of the channel.messageId(string): Discord snowflake ID of the message.emoji(string): Unicode emoji or custom emoji as "name:id".max(number, optional): Maximum users to return (1-1000; defaults to 100).options({ after?: string, burst?: boolean }, optional): after: continue from this user ID; burst: list super-reactions instead of normal ones.
string[]: User IDs who reacted with that emoji (empty if none).pinMessage(channelId, messageId, reason?): void
Pins a message in its channel.
Mallard needs: PinMessageschannelId(string): Discord snowflake ID of the channel.messageId(string): Discord snowflake ID of the message.reason(string, optional): Audit log reason.
unpinMessage(channelId, messageId, reason?): void
Unpins a message in its channel.
Mallard needs: PinMessageschannelId(string): Discord snowflake ID of the channel.messageId(string): Discord snowflake ID of the message.reason(string, optional): Audit log reason.
publishMessage(channelId, messageId): void
Publishes (crossposts) a message in an announcement channel.
Mallard needs: SendMessages, ManageMessageschannelId(string): Discord snowflake ID of the announcement channel.messageId(string): Discord snowflake ID of the message.
editMessage(channelId, messageId, content, embeds?, buttons?, pings?): void
Edits a message the bot previously sent in a channel (keep the ID sendMessage returned). The edit fully replaces the message: embeds, buttons and menus you don't pass are removed. Pass a MessageOptions object as the third argument for select menus or files.
channelId(string): Discord snowflake ID of the channel.messageId(string): Discord snowflake ID of the message to edit.content(string | MessageOptions): New message content (empty string for embed-only), or a MessageOptions object.embeds(EmbedData[], optional): Up to 10 embeds. A single embed object is also accepted.buttons(ButtonData[], optional): Buttons to attach, up to 25 (5 per row).pings(boolean, optional): Whether @mentions in the new content notify. Defaults to true.
getPins(channelId): PinnedMessage[]
Returns a channel's pinned messages (id, channelId, authorId, content). Live REST read.
channelId(string): Discord snowflake ID of the channel.
PinnedMessage[]: Array of the channel's pinned messages.searchMessages(query): SearchResult
Searches the whole server's message history, the way Discord's search bar does: by text, author, channel, mention, or what the message has (links, files, images, embeds, polls). Returns up to 25 matches per call plus the total. Discord builds the search index lazily, so a server that has never been searched may first throw "still indexing"; catch it and try again later.
Mallard needs: ReadMessageHistoryquery(SearchQuery): {content?, authorId?, channelId?, mentions?, has?: "link"|"file"|"image"|"video"|"embed"|"poll"|"sticker", pinned?, sortBy?: "timestamp"|"relevance", limit? (1-25), offset?}
SearchResult: The total match count and the matching messages.purge(channelId, options): number
Deletes recent messages in a channel that match a filter, the classic /purge: purge(channelId, { count: 50, authorId }) removes that user's last 50. Scans back through up to 500 recent messages to find up to count matches (1-100). Returns how many were deleted.
Mallard needs: ManageMessages, ReadMessageHistorychannelId(string): Discord snowflake ID of the channel.options(PurgeOptions): {count, authorId?, contains?, bots? (only bot/webhook messages), attachments? (only ones with files), before?, reason?}
number: How many messages were deleted.deleteMessages(channelId, messageIds, reason?): number
Deletes specific messages from one channel in as few calls as possible (Discord's bulk delete for anything under 14 days old, one by one otherwise). Returns how many were deleted; IDs already gone are skipped.
Mallard needs: ManageMessageschannelId(string): Discord snowflake ID of the channel.messageIds(string[]): Up to 100 message IDs.reason(string, optional): Audit log reason.
number: How many messages were deleted.getPollResults(channelId, messageId): PollData | null
Reads a poll's current counts per answer (exact once isFinalized is true).
Mallard needs: ReadMessageHistorychannelId(string): Discord snowflake ID of the channel.messageId(string): The poll message's ID.
PollData | null: The poll, or null when the message has none.getPollVoters(channelId, messageId, answerId, max?): string[]
Returns the user IDs who voted for one poll answer (up to 1000).
Mallard needs: ReadMessageHistorychannelId(string): Discord snowflake ID of the channel.messageId(string): The poll message's ID.answerId(number): The answer's id (see getPollResults()).max(number, optional): Maximum voters to return (1-1000; defaults to 100).
string[]: Voter user IDs.Members(21)
hasRole(userId, roleId): boolean
Returns true when the user currently has the given role. Backed by the bot's guild-member cache (loaded from the gateway whenever the bot connects and kept live by member events); a user not in the cache returns false.
userId(string): Discord snowflake ID of the user to check.roleId(string): Discord snowflake ID of the role. An empty string returns false rather than throwing, so an unconfigured role ID (e.g. from ctx.env) can be checked safely.
boolean: True when the user has the role; false if not, or if they aren't a cached member.listMembers(afterUserId?, limit?): MemberSummary[]
Returns a page of cached guild members ordered by user ID. The member cache is loaded in full from the gateway whenever the bot connects and kept live by member events (requires the bot's Server Members privileged intent). Page with the last userId of the previous call.
afterUserId(string, optional): Return members with a user ID greater than this; empty or omitted starts from the beginning.limit(number, optional): Page size (1-500; defaults to 100).
MemberSummary[]: Array of cached members (empty when the cache has not been populated yet).hasPermission(userId, permission): boolean
Whether a member holds a server-level permission, computed the way Discord does: the @everyone role plus every role they hold, with Administrator and server ownership each granting everything. Use this instead of hard-coding moderator role IDs. NOTE: this is server-level and does NOT apply channel permission overwrites, so it answers "may they do this in general", not "may they do this in #channel" (hasChannelPermission() does that). Like hasRole(), an uncached member returns false.
userId(string): Discord snowflake ID of the member.permission(string): Permission name, e.g. "ManageMessages", "BanMembers", "KickMembers", "ModerateMembers", "ManageRoles", "ManageChannels", "Administrator". Discord's own spelling ("BAN_MEMBERS") works too.
boolean: True when the member holds the permission.createInvite(channelId, maxAgeSeconds, maxUses, temporary, reason?, options?): InviteSummary
Creates an invite to a channel and returns it. options.roleIds makes it a community invite that gives those roles to whoever joins with it.
Mallard needs: CreateInstantInvitechannelId(string): Channel the invite should point at.maxAgeSeconds(number): Lifetime in seconds (0 = never expires).maxUses(number): Use limit (0 = unlimited).temporary(boolean): Whether joiners get temporary membership (kicked on disconnect unless given a role).reason(string, optional): Audit log reason.options({ roleIds?: string[], unique?: boolean }, optional): roleIds: roles granted on join; unique: always mint a new code.
InviteSummary: The created invite, including its code.deleteInvite(code, reason?): boolean
Revokes one of this server's invites by its code. An invite belonging to another server is refused.
Mallard needs: ManageChannelscode(string): The invite code (the part after discord.gg/).reason(string, optional): Audit log reason.
boolean: False when the invite was already gone.getInvites(): InviteSummary[]
Returns the server's active invites, including how many times each has been used. This is the only way to read a use count, and Discord only reports uses when Mallard has the Manage Server permission (without it every uses is 0). To attribute a join, ctx.member.inviteCode on GuildMemberAdd is simpler. Live REST read.
Mallard needs: ManageGuild ReturnsInviteSummary[]: Array of the server's active invites.getMember(userId): MemberSummary | null
Returns one cached guild member (userId, username, displayName, nickname, avatarUrl, joinedAt, roleIds), or null if not cached. Cheap cached read, no REST. For the user who triggered the script, ctx.actor already holds this, so no call is needed.
userId(string): Discord snowflake ID of the member.
MemberSummary | null: The member, or null when not cached.listRoles(): RoleSummary[]
Lists the guild's cached roles (id + name). Cheap cached read (no REST).
ReturnsRoleSummary[]: Array of the guild's roles.setNickname(userId, nickname, reason?): void
Sets or clears a member's nickname (empty string resets it to their username).
Mallard needs: ManageNicknamesuserId(string): Discord snowflake ID of the member.nickname(string): The new nickname (empty to reset).reason(string, optional): Audit log reason.
moveMember(userId, channelId, reason?): void
Moves a member to a voice channel (they must already be connected to voice).
Mallard needs: MoveUsers, ConnectuserId(string): Discord snowflake ID of the member.channelId(string): Voice channel to move them into.reason(string, optional): Audit log reason.
disconnectMember(userId, reason?): void
Disconnects a member from voice.
Mallard needs: MoveUsersuserId(string): Discord snowflake ID of the member.reason(string, optional): Audit log reason.
muteMember(userId, muted, reason?): void
Server-mutes or unmutes a member in voice.
Mallard needs: MuteUsersuserId(string): Discord snowflake ID of the member.muted(boolean): true to mute, false to unmute.reason(string, optional): Audit log reason.
deafenMember(userId, deafened, reason?): void
Server-deafens or undeafens a member in voice.
Mallard needs: DeafenUsersuserId(string): Discord snowflake ID of the member.deafened(boolean): true to deafen, false to undeafen.reason(string, optional): Audit log reason.
getUser(userId): UserSummary | null
Looks up any Discord user by ID, member of this server or not: name, avatar, account age. Live REST read.
userId(string): Discord snowflake ID of the user.
UserSummary | null: The user, or null when the ID doesn't exist.fetchMember(userId): MemberContext | null
Reads a member live from Discord rather than the member cache, with every field (boost, timeout, pending, flags). For a member getMember() doesn't have yet, or when those fields matter. Null when they aren't in the server.
userId(string): Discord snowflake ID of the member.
MemberContext | null: The member, or null.searchMembers(query, limit?): MemberSummary[]
Finds members whose username or nickname starts with the query, e.g. for a text command that takes a name.
query(string): The start of a username or nickname.limit(number, optional): Maximum to return (1-100; defaults to 25).
MemberSummary[]: Matching members.getMembersWithRole(roleId, limit?): MemberSummary[]
Lists cached members who hold a role, e.g. to strip a role from everyone or count staff. Cache read, no REST.
roleId(string): Discord snowflake ID of the role.limit(number, optional): Maximum to return (1-1000; defaults to 100).
MemberSummary[]: The members holding the role.getVoiceMembers(channelId): VoiceMember[]
Lists who is in a voice or stage channel right now, from the live voice-state cache. The basis for join-to-create channels (delete when empty), voice XP and event attendance.
channelId(string): The voice or stage channel.
VoiceMember[]: Everyone connected to that channel.getVoiceState(userId): VoiceMember | null
Where a member is in voice right now, or null when they aren't connected.
userId(string): Discord snowflake ID of the member.
VoiceMember | null: Their voice state.hasChannelPermission(userId, channelId, permission): boolean
Whether a member holds a permission in one specific channel, applying the channel's overwrites the way Discord does (roles, then @everyone and member overwrites). The per-channel counterpart of hasPermission().
userId(string): Discord snowflake ID of the member.channelId(string): The channel.permission(string): Permission name, e.g. "SendMessages", "ViewChannel".
boolean: True when they have it in that channel.setBotProfile(options): { nickname: string, avatarUrl: string }
Changes how Mallard looks in this server only: nickname, avatar, banner and bio. Seasonal avatars on a cron schedule, or branding per event. Images come from the server's Media page. Pro and Ultra only, at most once every 10 minutes; the audit log names the script. Pass nick: "" to go back to the bot's own name.
Mallard needs: ChangeNicknameoptions({ nick?: string, avatarMediaId?: string, bannerMediaId?: string, bio?: string }): Only what you pass changes. nick up to 32 characters, bio up to 190; image IDs from the Media page.
{ nickname: string, avatarUrl: string }: The profile as Discord stored it.AI(2)
hasAi(): boolean
Returns true if the guild has an OpenRouter AI integration configured on the Integrations page.
Returnsboolean: true if AI is configured, false otherwise.aiChat(promptOrOptions, options?): AiChatResponse
Sends a chat completion request to OpenRouter using the server's configured OpenRouter API key. Pass a prompt string or an options object ({ prompt, messages, model, maxTokens, temperature, reasoning, webSearch, webFetch, datetime, imageGen, advisor, subagent, tools, timeoutMs }). Default server tools configured on the Integrations page run automatically unless overridden with shorthand boolean flags (e.g. webSearch: false) or tools: []. Returns { content, text, model, finishReason, usage }.
promptOrOptions(string | AiChatOptions): A prompt string, or an options object specifying messages, model, tools, or limits.options(AiChatOptions, optional): Optional options object when the first parameter is a prompt string.
AiChatResponse: Object with content (string), text (string), model (string), finishReason (string), and usage stats.HTTP(1)
fetch(url, options?): FetchResponse
Performs an HTTP request to an external API or feed and returns {status, headers, body} (body is a string, so parse JSON/XML yourself). Restrictions: https only (http via "fetch_allow_http" in the env record), standard ports only, GET/POST/PUT/PATCH/DELETE/HEAD only, 5s default timeout (up to 30s via options.timeoutMs), 1 MB response cap, 30 requests/minute per guild plus an hourly budget (see ctx.fetchCallsRemaining), and private/internal addresses are blocked. options is an object or JSON string: {method, headers (accept, content-type, authorization, user-agent, client-id, api-key, anthropic-version, and any "x-*" header; user-agent defaults to MallardBot/1.0), body, timeoutMs}. HEAD returns an empty body, handy for checking a URL exists. Any "{{secret:NAME}}" placeholder from ctx.secret (admin-only) in the url, a header value, or the body is replaced with the real secret just before the request is sent.
url(string): Absolute https URL to request.options(object | string, optional): Request options ({method, headers, body, timeoutMs}), as an object or JSON string, or omitted for a plain GET.
FetchResponse: Object with status (number), headers (Record<string,string>) and body (string).Roles(10)
addRole(userId, roleId, reason?): void
Adds a role to a guild member.
Mallard needs: ManageRolesuserId(string): Discord snowflake ID of the user.roleId(string): Discord snowflake ID of the role.reason(string, optional): Audit log reason.
removeRole(userId, roleId, reason?): void
Removes a role from a guild member.
Mallard needs: ManageRolesuserId(string): Discord snowflake ID of the user.roleId(string): Discord snowflake ID of the role.reason(string, optional): Audit log reason.
createRole(name, colorArgb?, hoist?, mentionable?, reason?): RoleInfo
Creates a role and returns it. The new role has NO permissions: a script that could mint a permission it doesn't itself hold would be a privilege-escalation path, so permissions stay something a human sets in Discord.
Mallard needs: ManageRolesname(string): Role name.colorArgb(number, optional): Colour as a number (e.g. 0xED4245 / 15548997 for red). Omit for no colour.hoist(boolean, optional): Show the role separately in the member list.mentionable(boolean, optional): Let anyone @mention the role.reason(string, optional): Audit log reason.
RoleInfo: The created role.modifyRole(roleId, name?, colorArgb?, hoist?, mentionable?, reason?): boolean
Renames or recolours a role. Omitted parameters are left unchanged. Permissions are not settable here, for the same reason as createRole().
Mallard needs: ManageRolesroleId(string): Discord snowflake ID of the role.name(string, optional): New name, or omit to keep it.colorArgb(number, optional): New colour as a number, or omit to keep it.hoist(boolean, optional): Show separately in the member list.mentionable(boolean, optional): Let anyone @mention it.reason(string, optional): Audit log reason.
boolean: False when the role no longer exists.deleteRole(roleId, reason?): boolean
Deletes a role from the server.
Mallard needs: ManageRolesroleId(string): Discord snowflake ID of the role.reason(string, optional): Audit log reason.
boolean: False when the role was already gone.setRoles(userId, roleIds, reason?): boolean
Replaces a member's entire role set in one call. Prefer this over a loop of addRole() / removeRole(): those are one action each against the hourly budget and leave the member in visible intermediate states, whereas this is a single action and a single change. Roles Discord manages itself (bot and integration roles, the booster role) cannot be set and must be left in the list as they are.
Mallard needs: ManageRolesuserId(string): Discord snowflake ID of the member.roleIds(string[]): The complete set of role IDs the member should end up with.reason(string, optional): Audit log reason.
boolean: False when the user is no longer a member of the server.getRole(roleId): RoleInfo | null
Reads one role live, with its permissions and position.
roleId(string): Discord snowflake ID of the role.
RoleInfo | null: The role, or null when it no longer exists.getRoleMemberCounts(): Record<string, number>
How many members hold each role, straight from Discord without walking the member list. Keyed by role ID (@everyone is left out).
ReturnsRecord<string, number>: Role ID → member count.addRoleToMany(userIds, roleId, reason?): BulkResult
Adds one role to many members at once (up to 500), running the calls in parallel, which is far quicker than a loop of addRole(). Returns who got it and who didn't (left the server, or above Mallard).
Mallard needs: ManageRolesuserIds(string[]): Members to give the role to.roleId(string): The role.reason(string, optional): Audit log reason.
BulkResult: {succeeded, failed} user IDs.removeRoleFromMany(userIds, roleId, reason?): BulkResult
Removes one role from many members at once (up to 500), in parallel. Pair with getMembersWithRole() to clear a role from everyone.
Mallard needs: ManageRolesuserIds(string[]): Members to take the role from.roleId(string): The role.reason(string, optional): Audit log reason.
BulkResult: {succeeded, failed} user IDs.KV Store(9)
getKV(key, options?): string | null
Retrieves a value from the guild's key-value store. Reads don't lock, so two runs at once can read the same value and the later setKV() overwrites the earlier one. For counters use incrKV(); for any other read-modify-write use updateKV(), or pass { lock: true } here, which holds the key until this run ends so no other run can change it in between. The reserved system key "env" is not accessible here; read it through ctx.env instead.
key(string): Key to look up.options({ lock?: boolean }, optional): lock: take the key's lock before reading, for a safe read-modify-write.
string | null: The stored JSON string, or null if not found / expired.setKV(key, value, ttlSeconds?): void
Stores a value in the guild's key-value store, overwriting any existing value. A string is stored as-is (it should be JSON, e.g. from JSON.stringify); any other value (object, array, number, boolean) is JSON-encoded for you. Written the moment this call returns, so it is visible to getKV right away and kept even if the script errors or times out afterward. The reserved system key "env" cannot be written (it is managed on the dashboard's Database page).
key(string): Key to store under.value(any): A JSON string, or any value to be JSON-encoded.ttlSeconds(number, optional): Time-to-live in seconds; 0 or omitted for no expiry.
deleteKV(key): void
Deletes a key from the guild's key-value store. The reserved system key "env" cannot be deleted.
key(string): Key to delete.
listKV(prefix?, limit?, afterKey?): string[]
Lists the server's stored keys, optionally only those starting with a prefix, in alphabetical order. Returns KEYS, not values: a store can run to megabytes, so getKV() the ones you want, or use listKVEntries()/getKVMany(). This is what makes namespaced keys ("warns:123") usable as a collection. Page past the limit with afterKey. Expired keys and the reserved "env" row are never listed.
prefix(string, optional): Only keys starting with this. Empty or omitted lists everything.limit(number, optional): How many to return (1-1000; defaults to 100).afterKey(string, optional): Return keys after this one (the last key of the previous page).
string[]: Array of matching keys.updateKV(key, fn, ttlSeconds?): any
Safely read-modify-writes one key: takes the key's lock, calls fn with the current value (parsed from JSON, or null when missing), and stores what fn returns (undefined leaves it unchanged, null deletes it). No other run can change the key in between, so concurrent runs never lose each other's updates. Returns the stored value.
key(string): Key to update.fn((current: any) => any): Computes the new value from the current one.ttlSeconds(number, optional): Time-to-live for the new value (0 or omitted: none).
any: The new value (parsed).incrKV(key, by?, ttlSeconds?): number
Atomically adds to a number stored under a key (missing counts as 0) and returns the new total. The safe way to count: XP, message counts, ticket numbers.
key(string): Key holding the number.by(number, optional): Amount to add (may be negative; defaults to 1).ttlSeconds(number, optional): Time-to-live to (re)apply (0 or omitted: keep none).
number: The new total.getKVMany(keys): Record<string, string | null>
Reads many keys in one call, e.g. every key listKV() returned. Missing keys come back null.
keys(string[]): Up to 1000 keys.
Record<string, string | null>: Key → stored JSON string (or null).ttlKV(key): number | null
How many seconds a key has left before it expires: null when it never expires, -1 when it doesn't exist.
key(string): The key.
number | null: Seconds remaining.listKVEntries(prefix?, limit?, afterKey?): KvEntry[]
Like listKV() but returns each key's value and expiry too, for small collections (a leaderboard, a warn list) you'd otherwise read key by key.
prefix(string, optional): Only keys starting with this.limit(number, optional): How many to return (1-500; defaults to 100).afterKey(string, optional): Return keys after this one.
KvEntry[]: Keys with their values.Utility(13)
log(...values): void
Logs a line. It appears in the run's trace on the Logs page: expand any execution to see your log output interleaved with the Discord, fetch and database calls the script made, in the order they happened. Takes any number of arguments like console.log, and objects and arrays are printed as JSON; console.log/info/warn/error work too (warn and error are marked as such). Traces are kept for 8 days and are visible to anyone with dashboard access to this server, so don't log anything you wouldn't want kept.
...values(any[]): What to log.
formatTimestamp(isoOrEpoch, style?, timeZone?): string
Turns an ISO 8601 timestamp (any of the ctx date fields), a Date or a unix-seconds number into Discord's timestamp markup, which renders in each reader's own timezone and language. This is the best way to show a time in a message: pass a timeZone to read what someone typed as their local time. No Discord call, so it costs nothing.
isoOrEpoch(string): An ISO 8601 timestamp (e.g. ctx.member.joinedAt) or unix seconds.style(string, optional): One of t (short time), T (long time), d (short date), D (long date), f (short date+time), F (long date+time), R (relative, e.g. "3 hours ago"). Defaults to R.timeZone(string, optional): An IANA zone such as "Europe/London". With it, a time without an offset ("2026-10-02 20:00") or a phrase parseTime() understands ("fri 8pm", "tomorrow 9am") is read as local time there, so formatTimestamp(ctx.args.when, "F", "Europe/London") turns what an organiser typed into a time every reader sees in their own zone.
string: Markup such as "<t:1700000000:R>".parseDuration(text): number
Parses a human duration into MINUTES, so it feeds straight into timeout(). Accepts a run of amount+unit pairs using s/m/h/d/w, e.g. "10m", "2h30m", "7d". A bare number is read as minutes. Seconds give a fraction ("30s" is 0.5), which timeout() accepts. Throws when it can't parse, so a bad slash-command argument fails loudly rather than timing someone out for the wrong span. Multiply by 60 for runLater()'s seconds. No Discord call.
text(string): A duration such as "10m", "2h30m" or "7d".
number: The duration in minutes (fractional for seconds).snowflakeTime(id): string
Turns any Discord ID (user, message, channel) into the ISO 8601 time it was created. A user's ID gives their account age. No Discord call.
id(string): A Discord snowflake ID.
string: ISO 8601 timestamp.randomInt(min, max): number
A cryptographically secure random whole number between min and max, both included. Use it instead of Math.random() for giveaways and anything people might dispute.
min(number): Smallest possible result.max(number): Largest possible result.
number: The number.randomUUID(): string
A random UUID (v4), for ticket IDs and unique keys.
Returnsstring: e.g. "3b241101-e2bb-4255-8caf-4136c566a962".base64Encode(text): string
Encodes text (as UTF-8) in base64, e.g. for a Basic auth header: "Basic " + base64Encode(user + ":" + pass). Up to 64 KB.
text(string): The text.
string: Base64 text.base64Decode(text): string
Decodes base64 (standard or URL-safe, padding optional) back to UTF-8 text.
text(string): Base64 text.
string: The decoded text.sha256(text): string
The SHA-256 hash of some text (UTF-8, up to 64 KB), as lowercase hex.
text(string): The text.
string: 64 hex characters.hmacSha256(key, data, encoding?): string
Signs text with HMAC-SHA256, as signed APIs require. The key may be a secret placeholder (ctx.secret.MyKey): it's resolved on the server, so the real secret never enters the script. Also checks an incoming webhook's own signature scheme.
key(string): The signing key, usually ctx.secret.NAME.data(string): The text to sign (up to 64 KB).encoding(string, optional): "hex" (default) or "base64".
string: The signature.encodeQuery(params): string
Builds a URL query string from an object: encodeQuery({ q: "duck facts", page: 2 }) is "q=duck%20facts&page=2". An array value repeats the key; null and undefined are skipped.
params(Record<string, any>): Keys and values.
string: The query string, without a leading ?.parseQuery(text): Record<string, string | string[]>
Reads a query string (or a whole URL, or a form-urlencoded body) into an object. A key that repeats becomes an array.
text(string): "a=1&b=2", "?a=1" or a URL.
Record<string, string | string[]>: Keys and values.splitMessage(text, maxLength?): string[]
Splits long text into chunks that fit in one message (2000 characters by default), breaking at line ends, then spaces, where it can. Send each chunk with sendMessage().
text(string): The text.maxLength(number, optional): Chunk size (100-4000; defaults to 2000).
string[]: The chunks.Events(5)
listScheduledEvents(): ScheduledEventContext[]
Returns the server's scheduled events, the read that createScheduledEvent() / modifyScheduledEvent() / deleteScheduledEvent() never had. Same shape as ctx.scheduledEvent. Live REST read.
ReturnsScheduledEventContext[]: Array of the server's scheduled events.createScheduledEvent(options): string
Creates a guild scheduled event. options is an object (or JSON string): {name, description, entityType ("external"|"voice"|"stage"), channelId, location, startsAt (ISO 8601), endsAt (ISO 8601), recurrence, coverMediaId, reason}. External events need location + endsAt; voice/stage events need channelId. recurrence makes it repeat: {frequency: "daily"|"weekly"|"monthly"|"yearly", interval?, byWeekday?: ["monday", …]} (Discord allows daily, weekly on one day, every other week, monthly on a date or weekday, yearly). coverMediaId is an image from the Media page. Returns the created event's snowflake ID.
Mallard needs: CreateEventsoptions(object | string): The event definition (see description).
string: The created scheduled event's snowflake ID.modifyScheduledEvent(eventId, options): void
Edits an existing scheduled event. options uses the same shape as createScheduledEvent, plus an optional status ("active"|"completed"|"canceled") to transition it: flip a newly-created event to "active" so it shows as happening now, or "completed" when it ends. Only the fields you set change.
Mallard needs: ManageEventseventId(string): Discord snowflake ID of the scheduled event.options(object | string): The fields to change.
deleteScheduledEvent(eventId, reason?): void
Deletes a scheduled event.
Mallard needs: ManageEventseventId(string): Discord snowflake ID of the scheduled event.reason(string, optional): Audit log reason.
getScheduledEventUsers(eventId, max?): string[]
Returns the user IDs who RSVP'd to a scheduled event (up to 1000), e.g. to ping them 15 minutes before it starts.
eventId(string): The scheduled event.max(number, optional): Maximum to return (1-1000; defaults to 100).
string[]: Interested user IDs.Scheduling(7)
runLater(scriptName, delaySeconds, payload?): string
Runs another script (one with the Delayed trigger) after a delay, with a payload it reads from ctx.delayed.payload: the way to unban after 7 days, remove a temp role, send a reminder or end a giveaway. Returns a job ID for cancelLater(). The delay can be up to 90 days; a server can have at most 1000 pending jobs (Free: 100). The run is budgeted like any other when it comes due.
scriptName(string): Name of a script with the Delayed trigger.delaySeconds(number): How long to wait (1 second to 90 days).payload(any, optional): Anything JSON-serialisable (up to 4 KB).
string: The job's ID.emit(eventName, payload?): boolean
Sends a named event to every enabled CustomEvent script subscribed to it, with a payload they read from ctx.event.payload. The emitter needn't know who listens. Subscribers run within a couple of seconds, each budgeted like any other run. A run can emit 5 events, a server 60 a minute, and events can chain at most 3 deep.
eventName(string): e.g. "ticket.closed" (letters, digits and . _ : -).payload(any, optional): Anything JSON-serialisable (up to 16 KB).
boolean: True once the event is queued.require(name): any
Loads a Library script and returns its module.exports. Each library runs once per run however often it's required. Pass the name as a plain string: libraries are looked up when the run is queued, so require(someVariable) can't find one.
name(string): Name of a script with the Library trigger.
any: The library's module.exports.cancelLater(jobId): boolean
Cancels a pending runLater() job.
jobId(string): The ID runLater() returned.
boolean: False when the job had already run or didn't exist.listLater(scriptName?): PendingJob[]
Lists the server's pending runLater() jobs, soonest first.
scriptName(string, optional): Only jobs for this script.
PendingJob[]: Pending jobs.expectReply(userId, channelId, scriptName, timeoutSeconds, payload?): void
Waits for a user's next message in a channel and hands it to a ReplyReceived script, with a payload: the building block for step-by-step setups ("Which channel should I post in?"). If they don't answer within the timeout, the same script runs with ctx.reply.timedOut true. A new wait for the same user and channel replaces the old one.
userId(string): Whose reply to wait for.channelId(string): Where (a channel or thread in this server).scriptName(string): Name of a script with the ReplyReceived trigger.timeoutSeconds(number): How long to wait (5 seconds to 24 hours).payload(any, optional): Anything JSON-serialisable, read back as ctx.reply.payload.
cancelReply(userId, channelId): boolean
Stops waiting for a user's reply in a channel.
userId(string): The user.channelId(string): The channel.
boolean: False when there was no wait.AutoMod(5)
listAutoModRules(): AutoModRule[]
Lists the server's AutoMod rules (Discord's own filter, which blocks a message before anyone sees it and costs no runtime). createdByBot marks the ones Mallard's scripts made. Live REST read.
Mallard needs: ManageGuild ReturnsAutoModRule[]: The rules.getAutoModRule(ruleId): AutoModRule | null
One AutoMod rule.
Mallard needs: ManageGuildruleId(string): The rule's ID.
AutoModRule | null: The rule, or null when it doesn't exist.createAutoModRule(options): string
Creates an AutoMod rule: setup wizards, a /raidmode toggle, word lists synced from the key-value store. Discord allows 6 keyword rules and one each of spam, preset, mentionSpam and memberProfile. Rules that would match every message (an empty-matching regex, the keyword "*") are refused. The audit log names the script. A run can create, change or delete 5 rules.
Mallard needs: ManageGuildoptions(AutoModRuleOptions): e.g. { name: "No invites", trigger: "keyword", keywords: ["discord.gg/*"], actions: [{ type: "block", message: "No invite links" }] }.
string: The new rule's ID.modifyAutoModRule(ruleId, options): void
Changes an AutoMod rule. Only the options you pass change; pass enabled: false to switch a rule off. A rule's trigger can't be changed.
Mallard needs: ManageGuildruleId(string): The rule's ID.options(AutoModRuleOptions): What to change.
deleteAutoModRule(ruleId, reason?): boolean
Deletes an AutoMod rule.
Mallard needs: ManageGuildruleId(string): The rule's ID.reason(string, optional): Audit log reason.
boolean: False when it was already gone.Leaderboards(8)
zIncr(set, member, by?): number
Adds to a member's score in a sorted set (a missing member starts at 0) and returns the new score. Atomic, so any number of runs can add at once without a lock: the way to keep XP, message counts or invite counts for a leaderboard. Each member costs its length plus 16 bytes of storage; a set holds up to 10,000 members on Free (25k Plus, 50k Pro, 100k Ultra) and a server can have 100 sets.
set(string): The set's name, e.g. "xp".member(string): Who or what is scored, usually a user ID (up to 100 characters).by(number, optional): Amount to add (may be negative; defaults to 1).
number: The member's new score.zSet(set, member, score): number
Sets a member's score outright, adding the member if needed.
set(string): The set's name.member(string): The member.score(number): The score.
number: The score.zGet(set, member): number | null
A member's score.
set(string): The set's name.member(string): The member.
number | null: The score, or null when the member isn't in the set.zRank(set, member, options?): number | null
A member's position, counting from 0 at the highest score (so rank + 1 is their place on the leaderboard). Pass { asc: true } to count from the lowest instead.
set(string): The set's name.member(string): The member.options({ asc?: boolean }, optional): Ascending order instead of descending.
number | null: 0-based rank, or null when the member isn't in the set.zRange(set, start?, stop?, options?): string[] | { member: string, score: number }[]
Members by rank, highest score first: zRange("xp", 0, 9, { withScores: true }) is the top ten. At most 100 per call; page with start/stop.
set(string): The set's name.start(number, optional): First rank (0-based; defaults to 0).stop(number, optional): Last rank, inclusive (defaults to start + 9).options({ asc?: boolean, withScores?: boolean }, optional): asc: lowest first. withScores: return { member, score } objects.
string[] | { member: string, score: number }[]: Members, or members with scores.zRemove(set, member): boolean
Removes a member from a set, e.g. when they leave the server.
set(string): The set's name.member(string): The member.
boolean: False when the member wasn't there.zCount(set): number
How many members a set has.
set(string): The set's name.
number: The count (0 for a set that doesn't exist).zDelete(set): boolean
Deletes a whole set, e.g. to reset a monthly leaderboard, and frees its storage.
set(string): The set's name.
boolean: False when there was no such set.Dates & time zones(5)
parseTime(text, timeZone?, from?): string | null
Reads a time the way people type it: "20:00", "8pm", "tomorrow 9am", "fri 8:30pm", "noon", "in 2h30m" or "2026-10-02 20:00", as local time in a zone. A time of day alone means its next occurrence. Pair it with formatTimestamp() so everyone sees it in their own zone, or with runLater() for a reminder (seconds = (Date.parse(iso) - Date.now()) / 1000).
text(string): What the user typed.timeZone(string, optional): IANA zone the text is local to (default UTC).from(string, optional): ISO time to count from (default now).
string | null: The moment as an ISO 8601 UTC string, or null when the text isn't a time.zonedTime(iso?, timeZone?): ZonedTime
The wall-clock time in a zone: year, month (1-12), day, hour, minute, second, weekday ("Monday"), offsetMinutes and a local ISO string. Scripts only get UTC Dates (there's no Intl), so this is how to ask "is it the weekend in New York?". Daylight saving is handled.
iso(string, optional): ISO time, Date or unix seconds (default now).timeZone(string, optional): IANA zone such as "America/New_York" (default UTC).
ZonedTime: The local time parts.fromZonedTime(parts, timeZone?): string
The opposite of zonedTime(): a local date and time in a zone as a UTC ISO string. A time skipped when the clocks go forward moves forward by the jump; a time repeated when they go back takes the earlier one.
parts({ year: number, month: number, day: number, hour?: number, minute?: number, second?: number } | string): The local time as parts, or as text like "2026-10-02 20:00".timeZone(string, optional): IANA zone (default UTC).
string: ISO 8601 UTC string.formatDate(iso, timeZone?, pattern?): string
Formats a time as text in a zone, in English, for places Discord's timestamp markup doesn't render (embed footers, channel names, KV keys). Tokens: yyyy, MMMM, MMM, MM, M, dddd, ddd, dd, d, HH, H, hh, h, mm, ss, tt (AM/PM), zzz (offset); text in 'single quotes' is kept as is. For messages, prefer formatTimestamp().
iso(string): ISO time, Date or unix seconds (default now).timeZone(string, optional): IANA zone (default UTC).pattern(string, optional): Format pattern (default "yyyy-MM-dd HH:mm").
string: The formatted text.nextOccurrence(cron, timeZone?, from?): string | null
The next time a five-field cron expression fires in a zone, e.g. nextOccurrence("0 20 * * 5", "Europe/London") for "next Friday 20:00 London time".
cron(string): minute hour day month weekday.timeZone(string, optional): IANA zone (default UTC).from(string, optional): ISO time to search after (default now).
string | null: ISO 8601 UTC string, or null when it never fires again.Data types
Shapes referenced by the functions above as parameters or return values. See the ctx object for ForumTag and every ctx.*-nested type.
AutoModRuleOptions
An AutoMod rule to create, or the parts of one to change.
- name?: string
- trigger?: "keyword" | "spam" | "preset" | "mentionSpam" | "memberProfile" (What the rule looks for (default "keyword"). Set when creating only.)
- event?: "messageSend" | "memberUpdate" (memberUpdate for memberProfile rules (names and bios); messageSend otherwise.)
- keywords?: string[] (Up to 1,000 of at most 60 characters; * is a wildcard ("bad*").)
- regex?: string[] (Up to 10 Rust-syntax patterns of at most 260 characters.)
- allow?: string[] (Words that never trigger the rule.)
- presets?: ("profanity" | "sexualContent" | "slurs")[] (Discord's own lists, for trigger "preset".)
- mentionLimit?: number (mentionSpam: unique mentions per message (1-50).)
- raidProtection?: boolean (mentionSpam: also catch mention raids.)
- actions?: { type: "block" | "alert" | "timeout" | "blockInteraction", message?: string, channelId?: string, seconds?: number }[] (block (with an optional 150-character message), alert (post to channelId), timeout (seconds, up to 4 weeks), blockInteraction. Default: block.)
- exemptRoles?: string[] (Up to 20 roles the rule ignores.)
- exemptChannels?: string[] (Up to 50 channels the rule ignores.)
- enabled?: boolean (Default true.)
- reason?: string (Audit log reason; the script's name is added.)
AutoModRule
An AutoMod rule as listAutoModRules() returns it.
- id: string
- name: string
- creatorId: string
- createdByBot: boolean (True for rules Mallard's scripts created.)
- trigger: string
- event: string
- enabled: boolean
- keywords: string[]
- regex: string[]
- allow: string[]
- presets: string[]
- mentionLimit: number | null
- raidProtection: boolean
- actions: { type: string, message: string, channelId: string, seconds: number | null }[]
- exemptRoles: string[]
- exemptChannels: string[]
FeedItem
One entry in a feed.
- id: string (The item's guid/id.)
- title: string
- link: string
- published: string (ISO 8601 (empty when the feed doesn't say).)
- updated: string (ISO 8601 (empty when the feed doesn't say).)
- author: string
- summary: string (Plain text with HTML removed, at most 2,000 characters.)
- imageUrl: string (An image from the item's enclosure or media tags (empty when none).)
- categories: string[]
ZonedTime
A moment as the wall clock shows it in one time zone (zonedTime()).
- year: number
- month: number (1-12.)
- day: number
- hour: number (0-23.)
- minute: number
- second: number
- weekday: string ("Monday" … "Sunday".)
- offsetMinutes: number (The zone's offset from UTC at that moment, e.g. 60 for BST.)
- iso: string (Local ISO 8601 with offset, e.g. "2026-10-02T20:00:00+01:00".)
- timeZone: string
CachedMessage
A message returned by getMessage()/getMessages() (fetched live over REST).
- id: string
- channelId: string
- authorId: string
- authorUsername: string
- authorDisplayName: string (Global display name, falling back to the username.)
- authorAvatarUrl: string
- content: string
- attachments: AttachmentContext[]
- referencedMessageId: string
- isForwarded: boolean
- createdAt: string (ISO 8601 creation timestamp.)
- editedAt: string | null (ISO 8601 time of the last edit, or null if never edited.)
- authorIsBot: boolean
- mentionedUserIds: string[]
- mentionedRoleIds: string[]
- mentionsEveryone: boolean
- isPinned: boolean
- type: string (Discord's message type (see MessageContext.type).)
- flags: string[]
- webhookId: string
- stickers: StickerSummary[]
- poll: PollData | null
- reactions: MessageReaction[]
- embeds: EmbedData[] (The message's embeds, in the same shape sendMessage()/editMessage() accept, so you can pass them straight back into editMessage() to keep an embed while changing only the buttons (or vice versa). Round-trips everything Mallard can write; only video embeds and the provider line are dropped.)
- buttons: ButtonData[] (The message's buttons, link buttons included. Buttons the bot itself attached round-trip with their handler; another bot's come back with a label only.)
- selects: SelectMenuData[] (The message's select menus, so a handler can re-send them with a new default or disabled state. Only menus Mallard sent carry their handler.)
MessageReaction
A per-emoji reaction summary on a CachedMessage (a count, not the reactor list; see getReactionUsers()).
- emojiId: string | null (Custom emoji ID (null for a unicode emoji).)
- emojiName: string (Emoji name, or the unicode character itself for a unicode emoji.)
- animated: boolean
- count: number
- me: boolean (Whether the bot itself has reacted with this emoji.)
MemberSummary
A cached guild member returned by listMembers() / getMember().
- userId: string
- username: string
- displayName: string (Nickname, else global display name, else username.)
- nickname: string
- avatarUrl: string (Server avatar, else global avatar, else Discord's default. Never empty.)
- joinedAt: string (ISO 8601 join timestamp (empty when unknown).)
- roleIds: string[]
- createdAt: string (ISO 8601 account creation time.)
RoleSummary
A cached guild role returned by listRoles(), and the role entries of ctx.resolved.
- id: string
- name: string
- colorArgb: number (0 means no colour.)
- position: number (Higher outranks lower.)
- hoist: boolean
- mentionable: boolean
- managed: boolean (A bot, integration or booster role: nobody can add or remove it, so skip these.)
- permissions: string[] (Permission names the role grants, e.g. "BanUsers".)
ChannelSummary
A cached guild channel returned by listChannels(), and the channel entries of ctx.resolved.
- id: string
- name: string
- type: string ("Text", "Voice", "Category", "Announcement", "Forum", "MediaForum", "Stage" or "Unknown" (empty until the bot next reconnects and records it).)
- parentId: string (The category it sits under (empty at top level).)
- position: number
BanSummary
One entry of the server's ban list, returned by getBans().
- userId: string
- username: string
- displayName: string (Global display name, falling back to the username.)
- reason: string (The reason recorded with the ban (empty when none was given).)
RoleInfo
A role, returned by createRole() and modifyRole(). Richer than the cache-backed RoleSummary that listRoles() returns, which has only id and name.
- id: string
- name: string
- colorArgb: number (Colour as a number; 0 means no colour (Discord renders default grey).)
- position: number (Where it sits in the role list. Higher outranks lower.)
- hoist: boolean (Shown separately in the member list.)
- mentionable: boolean
- managed: boolean (True for a role Discord manages itself (a bot's, an integration's, the booster role). Nobody can add or remove one, so skip these.)
- permissions: string[] (Permission names the role grants.)
- unicodeEmoji: string (The role icon when it is a unicode emoji (empty otherwise).)
ChannelInfo
One channel's detail, returned by getChannel().
- id: string
- name: string
- type: string ("Text", "Voice", "Category", "Announcement", "Forum", "MediaForum", "Stage", "Thread" or "Unknown".)
- topic: string (Empty when unset or when the kind has no topic.)
- parentId: string (The category it sits under, or the parent channel for a thread (empty at top level).)
- position: number
- nsfw: boolean
- slowmodeSeconds: number (Slowmode in seconds (0 when off).)
- userLimit: number (Voice/stage user limit (0 = none or not a voice channel).)
- bitrate: number (Voice bitrate in bits/s (0 when not a voice channel).)
EmojiSummary
A custom emoji in the server, returned by getEmojis().
- id: string
- name: string
- animated: boolean
- available: boolean (False when the server lost boosts and this emoji is over its new limit.)
- mention: string (The "<:name:id>" form you paste into message content to render it.)
- imageUrl: string (The image URL, for an embed.)
InviteSummary
One of the server's active invites, returned by getInvites().
- code: string
- channelId: string
- inviterId: string (Who created it (empty when Discord doesn't say, e.g. a vanity URL).)
- inviterUsername: string
- uses: number (Times used so far: the field an invite tracker diffs.)
- maxUses: number (Max uses before it expires (0 = unlimited).)
- maxAge: number (Lifetime in seconds (0 = never expires).)
- temporary: boolean
- createdAt: string (ISO 8601 creation time (empty when unknown).)
- expiresAt: string (ISO 8601 expiry (empty when it never expires).)
PinnedMessage
A pinned message returned by getPins().
- id: string
- channelId: string
- authorId: string
- content: string
FetchResponse
Result of a fetch() call.
- status: number
- headers: Record<string, string>
- body: string (Raw response body as a string, so parse JSON/XML yourself.)
AiChatUsage
Token usage stats for an AI completion.
- promptTokens: number
- completionTokens: number
- totalTokens: number
AiChatResponse
Result of an aiChat() call.
- content: string (Generated assistant text response.)
- text: string (Convenient alias for content.)
- model: string (The model used to generate the response.)
- finishReason: string (Why the completion finished (e.g. "stop", "length").)
- usage?: AiChatUsage (Token counts consumed by this completion.)
AiChatMessage
A chat message in an AI conversation.
- role: string (Message role: "system", "user", or "assistant".)
- content: string (Message text content.)
AiChatOptions
Options for an aiChat() request.
- prompt?: string (Single user prompt (convenience shorthand for messages).)
- messages?: AiChatMessage[] (Conversation history messages.)
- model?: string (Model identifier. Defaults to the guild's configured default (google/gemini-3.5-flash-lite).)
- maxTokens?: number (Maximum tokens to generate (default: 4096).)
- temperature?: number (Sampling temperature 0.0-2.0 (default: 0.7).)
- reasoning?: object | string (Reasoning effort: { effort: "low" | "medium" | "high" } or "low" | "medium" | "high".)
- webFetch?: boolean (Fetch and extract content from URLs in the prompt (openrouter:web_fetch).)
- webSearch?: boolean (Search the web for real-time information (openrouter:web_search).)
- datetime?: boolean (Provide current date, time, and timezone to the model (openrouter:datetime).)
- imageGen?: boolean (Allow model to generate images from prompts (openrouter:image_generation).)
- advisor?: boolean (Allow model to consult a stronger model for guidance (openrouter:advisor).)
- subagent?: boolean (Allow model to delegate subtasks to a worker model (openrouter:subagent).)
- tools?: any[] (Array of tool specifications. Pass [] to disable all tools.)
- timeoutMs?: number (Request timeout in milliseconds (default: 25000, max: 28000).)
EmbedData
A rich embed. Every field is optional, so set only what you need.
- title?: string
- url?: string (Makes the title a link.)
- description?: string
- colorArgb?: number (Colour of the left-hand bar, as a decimal number (e.g. 0xED4245 / 15548997 for red).)
- authorName?: string
- authorIconUrl?: string
- authorUrl?: string (Makes the author line a link.)
- footerText?: string
- footerIconUrl?: string
- timestamp?: string (ISO-8601 timestamp, e.g. new Date().toISOString().)
- imageUrl?: string
- thumbnailUrl?: string (The small image in the top-right corner.)
- fields?: EmbedField[] (Up to 25 name/value pairs, e.g. a stats card. Inline fields sit up to three to a row.)
EmbedField
One name/value pair on an embed.
- name: string (Up to 256 characters.)
- value: string (Up to 1024 characters.)
- inline?: boolean (Share a row with neighbouring inline fields.)
ButtonData
A button attached to a message: a handler button that runs a script, or a link button (url).
- label: string
- style?: "primary" | "secondary" | "success" | "danger" | "link" (A button with a url is always a link button, whatever this says.)
- payload?: string (Passed to the handling script as ctx.interaction.payload (max 50 chars).)
- handler?: string (Name of the script that runs when the button is clicked. A "Button Click" script replies; a "Button Click: Modal" script opens its form instead.)
- url?: string (Makes it a link button that opens this URL instead of running a script (no handler or payload).)
- emoji?: string (A unicode emoji ("🦆") or a custom one as "name:id", shown before the label. A label may be empty when there is an emoji.)
- disabled?: boolean (Greys the button out so it can't be clicked. Defaults to false. Common pattern for a one-time action: the click handler re-sends the same buttons with this one set true, so only the first click goes through.)
SelectMenuData
A select menu attached to a message. Each menu takes a whole row, and a message holds at most 5 rows in total (buttons fill rows five at a time).
- handler: string (Name of the "Select Menu" (or "Select Menu: Modal") script that runs when someone picks.)
- type?: "string" | "user" | "role" | "channel" | "mentionable" (What the menu lists. "string" (the default) shows your options; the others let the user pick server members, roles or channels.)
- options?: SelectOption[] (The choices of a string menu (1-25).)
- payload?: string (Passed as ctx.interaction.payload (max 50 chars).)
- placeholder?: string (Grey text shown before anything is picked.)
- minValues?: number (Fewest picks allowed (default 1; 0 lets them clear it).)
- maxValues?: number (Most picks allowed (default 1, up to 25).)
- channelTypes?: string[] (Channel menus only: limit to kinds such as "text", "voice", "forum", "category".)
- defaultValues?: string[] (User/role/channel menus only: IDs pre-selected when the menu shows.)
- disabled?: boolean
SelectOption
One choice in a string select menu.
- label: string
- value: string (What ctx.interaction.selected will contain.)
- description?: string
- emoji?: string (Unicode emoji or "name:id".)
- default?: boolean (Pre-selected.)
MessageOptions
Everything a message can carry, as one object. Every send function takes this in place of its content argument (sendMessage(channelId, { content, embeds, selects, silent })), which is the only way to attach select menus, files, stickers or flags.
- content?: string (Up to 2000 characters.)
- embeds?: EmbedData[] | EmbedData (Up to 10.)
- buttons?: ButtonData[] (Up to 25, five to a row.)
- selects?: SelectMenuData[] (Each menu takes its own row.)
- pings?: boolean (Whether mentions notify (default true).)
- replyTo?: string (Message ID in the same channel to reply to (sendMessage only).)
- silent?: boolean (Deliver without a notification sound, like typing @silent. Unlike pings:false, mentions still highlight.)
- suppressEmbeds?: boolean (Hide link previews.)
- files?: FileData[] (Attachments, up to 10.)
- stickerIds?: string[] (Up to 3 of the server's stickers (see getStickers()).)
- ephemeral?: boolean (followUp() only: show it to the invoking user alone.)
- layout?: LayoutBlock[] (A Components V2 layout: containers with an accent colour, sections with a thumbnail or button beside the text, image galleries, separators, button rows and menus. Can't be combined with content, embeds, buttons, selects or stickers, and a message sent with a layout keeps it for good (an edit can't turn it back into a plain message). Up to 40 components and 4,000 characters of text. Works with sendMessage, sendDm, editMessage, editResponse and followUp, not webhooks.)
LayoutBlock
One block of a layout. type picks the rest: "container" (children, accentColor, spoiler), "text" (text, markdown allowed), "section" (text: 1-3 strings, accessory), "separator" (divider, spacing), "gallery" (items), "buttons" (1-5 buttons) or "select" (select). Containers hold every type but another container.
- type: "container" | "text" | "section" | "separator" | "gallery" | "buttons" | "select"
- text?: string | string[] (text: the markdown. section: 1-3 strings shown stacked.)
- children?: LayoutBlock[] (container: the blocks inside.)
- accentColor?: number (container: the bar colour, e.g. 0x5865F2.)
- spoiler?: boolean (container: blur it until clicked.)
- accessory?: { thumbnail?: string, description?: string, spoiler?: boolean, button?: ButtonData } (section: an image URL (or attachment://name) or one button beside the text.)
- divider?: boolean (separator: draw a line (default true).)
- spacing?: "small" | "large" (separator: gap size (default small).)
- items?: { url: string, description?: string, spoiler?: boolean }[] (gallery: 1-10 images (https:// or attachment://name).)
- buttons?: ButtonData[] (buttons: a row of 1-5.)
- select?: SelectMenuData (select: one menu.)
FileData
A file to attach. Give either content (text you build in the script, e.g. a transcript or CSV) or mediaId (an image from the server's Media page).
- name: string (File name with extension, e.g. "transcript.txt".)
- content?: string (Text content, sent as UTF-8 (up to 1 MB).)
- mediaId?: string (ID of an image on the server's Media page.)
- spoiler?: boolean (Blur it until clicked.)
- description?: string (Alt text.)
StickerSummary
A sticker, on a message or from getStickers().
- id: string
- name: string
- format: string ("Png", "Apng", "Lottie" or "Gif".)
- description: string (getStickers() only.)
- available: boolean (getStickers() only: false when over the boost limit.)
PollData
A native poll, on a message or from getPollResults().
- question: string
- answers: PollAnswer[]
- allowMultiselect: boolean
- expiresAt: string (ISO 8601 (empty when unknown).)
- isFinalized: boolean (True once the poll has closed and the counts are exact.)
- totalVotes: number
PollAnswer
One answer of a poll, with its running count.
- id: number (The answerId that votes and getPollVoters() use.)
- text: string
- emoji: string (Unicode emoji or "name:id" (empty when none).)
- count: number
- meVoted: boolean
UserSummary
A Discord user, member of this server or not (getUser(), ctx.resolved.users).
- userId: string
- username: string
- displayName: string (Global display name, falling back to the username.)
- avatarUrl: string (Never empty.)
- bannerUrl: string (Empty when unset or unknown.)
- isBot: boolean
- createdAt: string (ISO 8601 account creation time.)
- primaryGuildTag: string (The server tag they display (empty when none).)
AuditLogEntry
One audit-log entry (ctx.auditLog, getAuditLog()).
- id: string
- actionType: string (NetCord's name for the action, e.g. "GuildUserBanAdd", "ChannelDelete", "RoleUpdate".)
- userId: string (Who did it (empty when Discord doesn't say).)
- targetId: string (What it was done to (a user, channel, role… per actionType).)
- reason: string
- changes: { key: string, oldValue: any, newValue: any }[] (What changed; key is Discord's snake_case field name.)
- options: Record<string, any> | null (Action-specific extras, e.g. channelId, count, roleName.)
- createdAt: string (ISO 8601.)
SearchResult
Result of searchMessages().
- total: number (Matches Discord found in all (more than returned).)
- messages: CachedMessage[] (Newest first unless sorted by relevance.)
BulkResult
Result of a bulk operation: which IDs it worked for and which it didn't.
- succeeded: string[]
- failed: string[]
VoiceMember
Someone in a voice channel (getVoiceMembers(), getVoiceState()).
- userId: string
- channelId: string
- selfMuted: boolean
- selfDeafened: boolean
- serverMuted: boolean
- serverDeafened: boolean
- streaming: boolean
- video: boolean
- suppressed: boolean
- joinedAt: string (ISO 8601 time Mallard saw them join this channel (empty when unknown).)
PermissionOverwrite
One permission overwrite on a channel (getChannelPermissions()).
- targetId: string (Role or member ID; the server's own ID is @everyone.)
- type: string ("role" or "member".)
- allow: string[]
- deny: string[]
ThreadSummary
An active thread (listActiveThreads()).
- id: string
- name: string
- parentId: string
- ownerId: string
- archived: boolean
- locked: boolean
- messageCount: number
- memberCount: number
- createdAt: string (ISO 8601.)
- lastMessageId: string (Compare with snowflakeTime() to find idle threads.)
KvEntry
A key with its value (listKVEntries()).
- key: string
- value: string (The stored JSON string.)
- expiresAt: string (ISO 8601 expiry (empty when it never expires).)
PendingJob
A runLater() job still waiting (listLater()).
- jobId: string
- scriptName: string
- dueAt: string (ISO 8601.)
- scheduledBy: string
IntegrationSummary
An integration installed on the server (getIntegrations()).
- id: string
- name: string
- type: string ("twitch", "youtube", "discord" (a bot) or "guild_subscription".)
- enabled: boolean
- accountName: string
ChannelOptions
Settings for createChannel()/modifyChannel(). Every field is optional.
- name?: string
- topic?: string
- parentId?: string (Category to move it under (empty string: top level).)
- position?: number
- nsfw?: boolean
- slowmode?: number (Seconds (0-21600).)
- userLimit?: number (Voice/stage only (0 = none, up to 99).)
- bitrate?: number (Voice only, in bits/s (8000-96000, more with boosts).)
- autoArchiveMinutes?: number (Threads: 60, 1440, 4320 or 10080.)
- syncPermissions?: boolean (modifyChannel: reset its permissions to match its category.)
- overwrites?: { targetId: string, targetIsRole?: boolean, allow?: string[], deny?: string[] }[] (Permission overwrites. On createChannel they are the channel's whole set; on modifyChannel they replace it.)
- reason?: string (Audit log reason.)
SearchQuery
What searchMessages() looks for. Combine as many as you like.
- content?: string (Words in the message.)
- authorId?: string
- channelId?: string
- mentions?: string (A user ID the message mentions.)
- has?: string ("link", "file", "image", "video", "embed", "poll" or "sticker".)
- pinned?: boolean
- sortBy?: string ("timestamp" (default, newest first) or "relevance".)
- limit?: number (1-25 (default 25).)
- offset?: number (Skip this many results, to page.)
PurgeOptions
What purge() deletes.
- count: number (Most messages to delete (1-100).)
- authorId?: string (Only this user's messages.)
- contains?: string (Only messages containing this text (case-insensitive).)
- bots?: boolean (Only messages from bots and webhooks.)
- attachments?: boolean (Only messages with files.)
- before?: string (Start scanning before this message ID.)
- reason?: string (Audit log reason.)