The ctx object
Every script has a global ctx available with details about the event that
triggered it. Not every property is populated on every trigger; see
Triggers for exactly which. The dashboard's script
editor autocompletes only the properties valid for the script you're editing.
Top-level properties
26 properties on ctx itself.
The guild's Discord snowflake ID (as a string).
Available on: All triggersThe acting user's Discord snowflake ID (as a string).
Available on: 29 triggers (see Triggers)The acting user's own member data (username, display name, nickname, avatar URL, roles and join date), so you don't have to call getMember(ctx.actorId) yourself. Available on every trigger that has a ctx.actorId. On commands, buttons and modals it comes straight from Discord and is always current; on other events it is read from the member cache, so it is null for a user the bot hasn't cached (and its discriminator is empty, a field Discord has retired anyway). Note ctx.actor is always the person who acted, whereas on UserCommand ctx.member is the right-clicked target.
Available on: 29 triggers (see Triggers)The channel's Discord snowflake ID (as a string).
Available on: 31 triggers (see Triggers)The event type that triggered this script.
Available on: All triggersKey/value map of arguments passed to the script.
Available on: All triggersCustom environment variables set via the Database page: the single 'env' KV record holds a JSON object whose keys are exposed here (e.g. {"mod_log_channel_id":"123"} becomes ctx.env.mod_log_channel_id). Read-only during script execution.
Available on: All triggersAdmin-only secrets (API keys, etc.) from the Database page's 'secrets' record. Each key maps to an OPAQUE placeholder token "{{secret:NAME}}" rather than the real value, which is substituted server-side only inside fetch() (in the url, header values, and body). Use it directly, e.g. fetch(url, JSON.stringify({headers:{authorization:"Bearer " + ctx.secret.MyKey}})). A key is present only when an admin has set a non-empty value, so `if (ctx.secret.MyKey)` safely tests whether it's configured. Because scripts only ever hold the placeholder, logging it, sending it in a message, or throwing it can never leak the real secret.
Available on: All triggersHow many fetch() calls remain in the guild's hourly budget when this execution starts. A fetch() beyond this throws, so size data-dependent loops against it.
Available on: All triggersMessage data. Present on MessageCreate/MessageUpdate/MessageDelete/DirectMessageCreate events and on MessageCommand (the right-clicked target message). On MessageUpdate, content is the edited (new) version and previousContent is what it said before, or null when that isn't known, since the pre-edit text comes from the 30-minute message cache, which a restart empties. On MessageDelete, content is the deleted text recovered from that same cache.
Available on: MessageCommand, MessageCommand: Modal, MessageCreate, MessageUpdate, MessageDelete, DirectMessageCreateMember data. Present on GuildMemberAdd/GuildMemberRemove/GuildMemberUpdate events. Also present on UserCommand, where it is the right-clicked target member (ctx.actorId is the invoker).
Available on: UserCommand, UserCommand: Modal, GuildMemberAdd, GuildMemberRemove, GuildMemberUpdateReaction data. Present on MessageReactionAdd/MessageReactionRemove events.
Available on: MessageReactionAdd, MessageReactionRemove, MessageReactionRemoveAll, MessageReactionRemoveEmojiBulk delete data. Present on MessageBulkDelete events.
Available on: MessageBulkDeleteVoice state data. Present on VoiceStateUpdate events; channelId is empty when the user disconnected.
Available on: VoiceStateUpdateComponent interaction data. Present on ButtonClick and on the ": Modal" triggers, where values holds the submitted form's text inputs keyed by input id.
Available on: SlashCommand: Modal, MessageCommand: Modal, UserCommand: Modal, ButtonClick, ButtonClick: ModalThe event's payload as a JSON string. Present for GuildAuditLogEntryCreate and AutoModerationActionExecution. Mallard's own summary, not Discord's raw gateway object: keys are camelCase and snowflakes are strings. AutoModerationActionExecution: userId, channelId, messageId, ruleId, ruleTriggerType, content, matchedContent, matchedKeyword, actionType, alertSystemMessageId, alertChannelId, timeoutDurationSeconds, customMessage (the last four follow actionType: SendAlertMessage fills the alert id + channel, Timeout the duration, BlockMessage the custom message). GuildAuditLogEntryCreate: id, actionType, userId, targetId, reason, changes (array of {key, oldValue, newValue}, where key is Discord's snake_case name and the values keep the JSON type Discord sent), options (action-specific extras such as roleName/channelId/count, null when the action carries none).
Available on: GuildAuditLogEntryCreate, AutoModerationActionExecutionTwitch event data. Present on Twitch* triggers; messages is populated only for TwitchChatBatch (all chat messages from the last ~10 minutes, dispatched as one batch, never per message).
Available on: TwitchStreamOnline, TwitchStreamOffline, TwitchChannelUpdate, TwitchChatBatchThread / forum post data. Present on ThreadCreate/ThreadUpdate/ThreadDelete events. isForumPost is true when the thread lives in a forum or media channel; parentId is the channel it was created under, ctx.channelId is the thread itself, and tags holds the forum tags applied to the post (resolved to names, and empty for non-forum threads and on ThreadDelete).
Available on: ThreadCreate, ThreadUpdate, ThreadDeleteBan data. Present on GuildBanAdd (user was banned) and GuildBanRemove (user was unbanned). Who performed it is in the audit log, not here.
Available on: GuildBanAdd, GuildBanRemoveInvite data. Present on InviteCreate (full invite) and InviteDelete (only code + channelId are available).
Available on: InviteCreate, InviteDeleteScheduled-event data. Present on the GuildScheduledEvent* triggers. On the Create/Update/Delete triggers the full event is populated; on UserAdd/UserRemove only id and userId are set (the (un)subscribing user).
Available on: GuildScheduledEventCreate, GuildScheduledEventUpdate, GuildScheduledEventDelete, GuildScheduledEventUserAdd, GuildScheduledEventUserRemovePoll-vote data. Present on MessagePollVoteAdd/MessagePollVoteRemove; answerId identifies which poll answer was voted for or retracted.
Available on: MessagePollVoteAdd, MessagePollVoteRemoveStage-instance data. Present on StageInstanceCreate/Update/Delete; channelId is the stage voice channel.
Available on: StageInstanceCreate, StageInstanceUpdate, StageInstanceDeleteUpdated guild settings. Present on GuildUpdate.
Available on: GuildUpdateEmoji/sticker set data. Present on GuildEmojisUpdate/GuildStickersUpdate; kind is "emojis" or "stickers", and count/names describe the set after the change.
Available on: GuildEmojisUpdate, GuildStickersUpdateSubmitted form data. Present on FormSubmitted, fired when anyone submits one of the guild's public forms. platform is "Discord", "Twitch" or "Anonymous" depending on the form's identity requirement; submitterId/submitterDisplayName come from that platform (submitterId is not a Discord snowflake for Twitch submitters, so it isn't also exposed as ctx.actorId).
Available on: FormSubmittedNested types
The full field list for every object type referenced above.
AttachmentContext
An attachment on a message.
fileName(string)contentType(string)size(number)url(string)
MessageContext
Message data, present on message-related events.
id(string)content(string)authorId(string)authorUsername(string)authorDisplayName(string): Discord display name (global_name), falling back to username when unset.authorAvatarUrl(string)channelId(string)attachments(AttachmentContext[])referencedMessageId(string): ID of the message this one replies to (empty when not a reply).isForwarded(boolean): True when the message is a forward.previousContent(string | null): On MessageUpdate, the content before the edit, or null when it isn't known (any other trigger, or a message not in the 30-minute cache).createdAt(string): ISO 8601 time the message was posted. On MessageDelete this comes from the 30-minute cache, so it is empty for an older message.editedAt(string | null): ISO 8601 time of the last edit, or null if it has never been edited.authorIsBot(boolean): Whether the author is a bot or webhook (only ever true with the allowBots filter on).mentionedUserIds(string[]): User IDs the message mentions; a reply's implicit ping counts.mentionedRoleIds(string[])mentionsEveryone(boolean): Whether it pinged @everyone/@here, and the author had permission to.isPinned(boolean)
MemberContext
Member data. ctx.member on member-related events, and ctx.actor anywhere there is a ctx.actorId.
userId(string)username(string)displayName(string): Nickname, else global display name, else username.discriminator(string): Legacy; Discord retired discriminators. Empty on cache-backed ctx.actor.nickname(string)avatarUrl(string): Server avatar, else global avatar, else Discord's default. Never empty.roleIds(string[])joinedAt(string): ISO 8601 join timestamp (empty when unknown).isBot(boolean | null): The fields below are null when the member came from Mallard's member cache (getMember/listMembers, and ctx.actor outside commands), which does not store them. Null means "this source can't say", not false.premiumSince(string | null): ISO 8601 time they started boosting, or null if they never have.timeoutUntil(string | null): ISO 8601 timeout expiry. A PAST time means an expired timeout, so compare it against now rather than testing for null.isPending(boolean | null): True while they still have membership screening (the rules gate) to pass.serverMuted(boolean | null)serverDeafened(boolean | null)
VoiceStateContext
Voice state data, present on VoiceStateUpdate events. Discord sends the whole state on every change and never says what changed, so compare against your own stored copy to tell a channel move from someone starting a stream.
userId(string)channelId(string): Voice channel the user is now in; empty when they disconnected.selfMuted(boolean)selfDeafened(boolean)serverMuted(boolean): Muted by a moderator, so they cannot unmute themselves.serverDeafened(boolean)streaming(boolean): Screen-sharing / "going live".video(boolean): Camera on.suppressed(boolean): On a stage channel, in the audience rather than speaking.requestedToSpeakAt(string): ISO 8601 stage "request to speak" time (empty when there is none).
ForumTag
A forum tag, applied to a post (ctx.thread.tags) or defined on a forum (getForumTags).
id(string)name(string)emojiName(string): Unicode emoji on the tag (empty when none or when the tag uses a custom emoji).emojiId(string): Custom emoji ID on the tag (empty when none or when the tag uses a unicode emoji).moderated(boolean): Whether only moderators can add/remove this tag.
ThreadContext
New thread / forum post data, present on ThreadCreate events.
id(string): ID of the new thread / forum post (same as ctx.channelId).name(string): The thread / forum post title.ownerId(string): User who created it (same as ctx.actorId).parentId(string): Channel the thread was created under (the forum channel for a forum post).parentType(string): Parent channel kind: "Forum", "MediaForum", "Announcement", "Text", or "Unknown".isForumPost(boolean): True when the parent is a forum/media channel, meaning this thread is a forum post.tags(ForumTag[]): Forum tags applied to this post, resolved to names (empty for non-forum threads).archived(boolean): Whether the thread is archived (closed). ThreadUpdate is the only place archiving and reopening are visible. False on ThreadDelete.locked(boolean): Locked: only moderators can unarchive it.autoArchiveDuration(number): Idle minutes before auto-archive: 60, 1440, 4320 or 10080.archivedAt(string): ISO 8601 time of the last archive/unarchive (empty when unknown).messageCount(number): Messages in the thread; Discord stops counting at 50 on older threads.memberCount(number): Members who joined the thread; Discord stops counting at 50.
BanContext
Ban data, present on GuildBanAdd / GuildBanRemove events.
userId(string): The banned (GuildBanAdd) or unbanned (GuildBanRemove) user's ID.username(string)displayName(string): Global display name, falling back to the username. There is no nickname: by the time this fires they are not a member any more.avatarUrl(string): Avatar URL, never empty (falls back to Discord's default).isBot(boolean)
FormContext
Submitted form data, present on FormSubmitted events.
formId(string): The form's GUID, as it appears in its public URL.formName(string): The form's name, e.g. "Ban Appeal".submissionId(string): This submission's GUID, matching the id stored in forms:submissions:<formId>.platform(string): Which identity the submitter proved: "Discord", "Twitch", or "Anonymous" on a form that requires none.submitterId(string): The submitter's ID on that platform (not a Discord snowflake for Twitch submitters). Empty when anonymous.submitterDisplayName(string): Empty when anonymous.answers({ [label: string]: string }): Each field's answer, keyed by the field's label exactly as it read when the form was submitted. Labels are unique within a form. A field left blank is present with an empty string.answersText(string): Every answer pre-rendered as "**Label**\nvalue" blocks, for pasting straight into a message or embed. The only way to show answers from an Easy Mode block, which cannot index into answers.
InviteContext
Invite data, present on InviteCreate / InviteDelete events.
code(string): The invite code (the part after discord.gg/).channelId(string)inviterId(string): Inviter's user ID (empty on InviteDelete).maxUses(number): Max uses before expiry (0 = unlimited; 0 on InviteDelete).maxAge(number): Lifetime in seconds (0 = never; 0 on InviteDelete).temporary(boolean)expiresAt(string): ISO 8601 expiry time (empty when it never expires or on InviteDelete).uses(number): Times used. Always 0 on InviteCreate; an invite tracker counts with getInvites().createdAt(string): ISO 8601 creation time (empty on InviteDelete).inviterUsername(string): The inviter's username (empty on InviteDelete).inviterDisplayName(string): The inviter's display name, falling back to username (empty on InviteDelete).
ScheduledEventContext
Scheduled-event data, present on the GuildScheduledEvent* triggers.
id(string)name(string)description(string)channelId(string): Voice/stage channel (empty for external-location events).location(string): Physical location for external events (empty otherwise).creatorId(string): Event creator's user ID (empty when unknown or on user add/remove).entityType(string): Entity kind: "StageInstance", "Voice", or "External".status(string): Lifecycle status: "Scheduled", "Active", "Completed", or "Canceled".startsAt(string): ISO 8601 scheduled start time.endsAt(string): ISO 8601 scheduled end time (empty when open-ended).userId(string): The (un)subscribing user (GuildScheduledEventUserAdd/Remove only; empty otherwise).userCount(number): How many members have RSVP'd (0 on the user add/remove triggers).privacyLevel(string): Always "GuildOnly"; Discord has never shipped another value.coverImageUrl(string): Cover image URL (empty when it has none).
PollVoteContext
Poll-vote data, present on MessagePollVoteAdd / MessagePollVoteRemove events.
userId(string)messageId(string)channelId(string)answerId(number): ID of the poll answer that was voted for / removed.
StageContext
Stage-instance data, present on StageInstanceCreate/Update/Delete events.
id(string)channelId(string): The stage voice channel this instance is live in.topic(string)privacyLevel(string): Privacy level: "Public" or "GuildOnly".discoverableDisabled(boolean): Hidden from Stage Discovery.
GuildUpdateContext
The server's settings as they stand after a GuildUpdate. Discord sends the whole guild and never says which field moved, so to detect "the name changed" keep your own copy in the KV store and compare.
name(string)description(string)ownerId(string)iconUrl(string): Empty when unset.bannerUrl(string)vanityUrlCode(string): The discord.gg vanity code (empty when the server has none).premiumTier(number): Boost level, 0-3.boostCount(number)verificationLevel(string): "None", "Low", "Medium", "High" or "VeryHigh".explicitContentFilter(string): "Disabled", "MembersWithoutRoles" or "AllMembers".nsfwLevel(string): "Default", "Explicit", "Safe" or "AgeRestricted".preferredLocale(string): The server's primary locale, e.g. "en-GB".features(string[]): Feature flags, e.g. "COMMUNITY", "ANIMATED_BANNER".afkChannelId(string): Empty when unset.systemChannelId(string)rulesChannelId(string)
ExpressionSetContext
Emoji/sticker set data, present on GuildEmojisUpdate / GuildStickersUpdate events.
kind(string): Which set changed: "emojis" or "stickers".count(number): Number of items in the set after the change.names(string[]): Names of the items in the set after the change.ids(string[]): IDs of the items, positionally matching names. Discord sends the whole set rather than a delta, so store this and diff it next time to learn what was added or removed; names alone cannot tell a rename from a replacement.
TwitchChatMessage
A single Twitch chat message inside a TwitchChatBatch.
chatterId(string)chatterLogin(string)chatterDisplayName(string)text(string)rewardId(string): Channel-points reward id when the message was a text-input redemption (empty otherwise).bits(number): Number of bits cheered with this message (0 otherwise).sentAt(string): ISO 8601 timestamp the message was received.
TwitchContext
Twitch event data, present on Twitch* triggers.
broadcasterId(string)broadcasterLogin(string)title(string): Stream/channel title (TwitchChannelUpdate only; empty otherwise).category(string): Game/category name (TwitchChannelUpdate only; empty otherwise).startedAt(string): ISO 8601 stream start time (TwitchStreamOnline only; empty otherwise).messages(TwitchChatMessage[]): Chat messages from the last ~10 minutes (TwitchChatBatch only; empty otherwise).messagesDropped(number): Messages dropped from this batch because the per-guild buffer overflowed.
InteractionContext
Component interaction data, present on ButtonClick and the ": Modal" triggers.
customId(string)payload(string): Free-form payload set on the button that triggered this.userId(string)messageId(string): Message the component was attached to (empty for form submissions).channelId(string)values(Record<string, string>): Modal text-input values keyed by input id (empty for button clicks).
ReactionContext
Reaction data, present on MessageReactionAdd events.
userId(string)messageId(string)channelId(string)emojiId(string)emojiName(string)emojiAnimated(boolean): Whether the custom emoji is animated (false for unicode emoji).messageAuthorId(string): Who wrote the reacted-to message. MessageReactionAdd only, so empty on the three removal triggers; a starboard uses it to skip self-reactions.burst(boolean): Whether this was a "super reaction". MessageReactionAdd only.
BulkDeleteContext
Bulk delete data, present on MessageBulkDelete events.
messageIds(string[])count(number)channelId(string)messages(CachedMessage[]): The deleted messages, recovered from the 30-minute cache. A SUBSET of messageIds, oldest-first, and empty when a purge cleared older history: Discord itself sends only the IDs.