Documentation
Mallard is a Discord moderation bot with a built-in scripting engine: everything the bot does, from slash commands to event reactions to scheduled jobs, is a script you can read, edit, and write yourself from the dashboard. This page walks through how it works and documents the full API available to your scripts.
Overview
Out of the box, Mallard ships with moderation commands: warn, kick, ban (including temp bans), timeout, slowmode, unban, and per-user notes. Each one posts to a mod-log channel and keeps an audit trail. Beyond that, every piece of guild behavior (slash commands, reactions to Discord events, and scheduled/timed jobs) is authored as a "GuildScript": a snippet of JavaScript stored per guild and run through an embedded JS engine, rather than hardcoded per-guild logic in the bot itself.
Scripts are written and managed entirely from the dashboard, using a full code editor with autocomplete against the same API reference documented below. No hosting, no deploy step: saving a script takes effect immediately.
How it works
There's no job queue. Discord events run their matching scripts directly and immediately:
- 1. A Discord gateway event (a message, a member joining, a reaction, …) or a slash command arrives at the bot.
- 2. The bot looks up enabled scripts matching the guild and the trigger type, and dispatches each one fire-and-forget onto a background task. Executions for the same guild are serialized FIFO, so two scripts never race on the key-value store, while different guilds run concurrently.
- 3. The script runs end-to-end through an embedded JavaScript engine (Jint). Every Discord action it calls (sending a message, banning a user, adding a role, …) and every key-value write happens live, in call order, the moment the script calls it. There's no queueing and no all-or-nothing rollback: a script that errors partway keeps whatever actions and writes it already made.
- 4. Once the script finishes (or errors, or times out), an execution record is saved with its status, duration, and any error, viewable on the guild's Logs page.
Scheduled
script that sweeps for it periodically, rather than "waiting."
sendDm,
kick,
timeout,
unban and
deleteMessage
return false
and your script keeps going. Everything else still stops it.
Triggers
Every script is set to run on exactly one of these triggers.
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;
the script editor's inline reference shows which apply to your script's trigger.
| Property | Type | Description |
|---|---|---|
| ctx.guildId | string | The guild's Discord snowflake ID (as a string). |
| ctx.actorId | string | The acting user's Discord snowflake ID (as a string). |
| ctx.channelId | string | The channel's Discord snowflake ID (as a string). |
| ctx.eventType | string | The event type that triggered this script. |
| ctx.args | ScriptArgs | Key/value map of arguments passed to the script (e.g. slash-command options). |
| ctx.env | Record<string, string> | Custom environment variables from 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 execution. |
| ctx.secret | Record<string, string> | Admin-only secrets (API keys, etc.) from the Database page's 'secrets' record. Each key is an OPAQUE placeholder token "{{secret:NAME}}", never the real value; it is substituted server-side only inside fetch() (url, header values, and body). Use it directly, e.g. authorization: "Bearer " + ctx.secret.MyKey. A key appears only when an admin has set a non-empty value, so `if (ctx.secret.MyKey)` tests whether it's configured. Since scripts only ever hold the placeholder, logging/sending/throwing it can't leak the real secret. |
| ctx.fetchCallsRemaining | number | How many fetch() calls remain in the guild's hourly budget when this execution starts. A fetch() beyond this throws, so size data-dependent loops against it. |
| ctx.restActionsRemaining | number | How many Discord actions (sends, bans, role changes, …) remain in the guild's hourly action budget when this execution starts. An action beyond this throws, so size data-dependent loops against it. |
| ctx.message | MessageContext | null | Message data. Present on MessageCreate / MessageUpdate / MessageDelete / DirectMessageCreate, and on MessageCommand (the right-clicked target message). On MessageUpdate, content is the new (edited) text and previousContent is what it said before, so an edit log can show both: previousContent is null when that isn't known, because the pre-edit text comes from the 30-minute message cache and a bot restart empties it. On MessageDelete, content is the deleted text recovered from that same cache (empty if it had aged out). |
| ctx.member | MemberContext | null | Member data. Present on GuildMemberAdd / GuildMemberRemove / GuildMemberUpdate, and on UserCommand (the right-clicked target member; ctx.actorId is the invoker). |
| ctx.reaction | ReactionContext | null | Reaction data. Present on MessageReactionAdd / MessageReactionRemove. |
| ctx.bulkDelete | BulkDeleteContext | null | Bulk delete data. Present on MessageBulkDelete. |
| ctx.voiceState | VoiceStateContext | null | Voice state data. Present on VoiceStateUpdate; channelId is empty when the user disconnected. |
| ctx.interaction | InteractionContext | null | Component interaction data. Present on ButtonClick and on the ": Modal" triggers, where values holds the submitted form's text inputs keyed by input id. |
| ctx.eventJson | string | null | Raw JSON payload of the event. Present for GuildAuditLogEntryCreate and AutoModerationActionExecution. |
| ctx.twitch | TwitchContext | null | Twitch event data. Present on Twitch* triggers; messages is populated only for TwitchChatBatch (all chat from the last ~10 minutes, as one batch). |
| ctx.thread | ThreadContext | null | Thread / forum post data. Present on ThreadCreate/ThreadUpdate/ThreadDelete. isForumPost is true for forum/media posts; 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, empty for non-forum threads and on ThreadDelete). Use getForumTags(parentId) for the full set a post can be tagged with. |
| ctx.ban | BanContext | null | Ban data. Present on GuildBanAdd (banned) / GuildBanRemove (unbanned); who performed it is in the audit log, not here. |
| ctx.invite | InviteContext | null | Invite data. Present on InviteCreate (full invite) / InviteDelete (only code + channelId). |
| ctx.scheduledEvent | ScheduledEventContext | null | Scheduled-event data. Present on GuildScheduledEvent* triggers; Create/Update/Delete carry the full event, UserAdd/UserRemove carry only id + userId (the (un)subscribing user). |
| ctx.pollVote | PollVoteContext | null | Poll-vote data. Present on MessagePollVoteAdd / MessagePollVoteRemove; answerId identifies which poll answer was voted for or retracted. |
| ctx.stage | StageContext | null | Stage-instance data. Present on StageInstanceCreate/Update/Delete; channelId is the stage voice channel. |
| ctx.guildUpdate | GuildUpdateContext | null | Updated guild settings. Present on GuildUpdate (name, description, ownerId). |
| ctx.expressions | ExpressionSetContext | null | Emoji/sticker set data. Present on GuildEmojisUpdate / GuildStickersUpdate; kind is "emojis" or "stickers", with count/names of the set after the change. |
| ctx.appeal | AppealContext | null | Ban appeal data. Present on AppealSubmitted. platform is "Discord" or "Twitch" depending on which identity the submitter proved; submitterId/submitterDisplayName come from that platform (submitterId isn't a Discord snowflake for Twitch submitters, so it isn't also exposed as ctx.actorId). |
Script functions
56 global functions available to scripts, grouped by category. Some are restricted to particular triggers (e.g. moderation actions are SlashCommand-only); the script editor's autocomplete only offers what's valid for the script you're editing.
Moderation (4) ›
ban(userId, reason, deleteMessageDays)
Bans a user from the guild. For a temporary ban, record the unban due time in the KV store and unban from a Scheduled script that sweeps it periodically (see the seeded /ban command and Unban Sweeper for the pattern).
-
userId(string): Discord snowflake ID of the user to ban. -
reason(string): Reason for the ban. -
deleteMessageDays(number): Number of days of messages to delete (0-7).
kick(userId, reason)
Kicks a user from the guild. Returns true if they were kicked, or false if they had already left. Kicking someone who isn't there stops your script rather than failing it.
-
userId(string): Discord snowflake ID of the user to kick. -
reason(string): Reason for the kick.
timeout(userId, reason, durationMinutes)
Times out a user in the guild. Returns true if the timeout was applied, or false if the user had already left the guild.
-
userId(string): Discord snowflake ID of the user to time out. -
reason(string): Reason for the timeout. -
durationMinutes(number): Duration of the timeout in minutes.
unban(userId, reason)
Unbans a user from the guild. Returns true if a ban was lifted, or false if the user wasn't banned in the first place.
-
userId(string): Discord snowflake ID of the user to unban. -
reason(string): Reason for the unban.
Channel (8) ›
setSlowmode(channelId, seconds)
Sets the slowmode delay for a channel.
-
channelId(string): Discord snowflake ID of the channel. -
seconds(number): Slowmode delay in seconds (0 to disable).
renameChannel(channelId, name)
Renames a channel or category.
-
channelId(string): Discord snowflake ID of the channel or category. -
name(string): The new name.
getForumTags(channelId): ForumTag[]
Returns the tags defined on a forum or media channel (the set a post can be tagged with), each as { id, name, emojiName, emojiId, moderated }. Pair with ctx.thread.tags (the tags actually applied to a new post) on a ThreadCreate script, or call 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.
createChannel(name, type, parentId?, topic?, kvKey?)
Creates a channel in the guild. When kvKey is set, the new channel's ID is written to that KV key so the script can read it back with getKV immediately.
-
name(string): Name for the new channel. -
type(string): Channel kind: "text", "voice", "category", "announcement", "forum", "media", or "stage". -
parentId(string, optional): Category ID to nest under (empty for none). -
topic(string, optional): Channel topic/description (empty for none). -
kvKey(string, optional): KV key to store the new channel ID under (empty to skip).
deleteChannel(channelId)
Deletes a channel (or thread) in the guild.
-
channelId(string): Discord snowflake ID of the channel to delete.
modifyChannel(channelId, name, topic)
Renames and/or re-topics a channel. Pass an empty string for a field to leave it unchanged.
-
channelId(string): Discord snowflake ID of the channel. -
name(string): New name (empty to leave unchanged). -
topic(string): New topic/description (empty to clear).
setChannelTopic(channelId, topic)
Sets a channel's topic/description.
-
channelId(string): Discord snowflake ID of the channel. -
topic(string): The new topic (empty to clear).
listChannels(): ChannelSummary[]
Lists the guild's cached channels (id + name). Cheap cached read (no REST), like listMembers.
Messaging (6) ›
sendMessage(channelId, content, embeds?, buttons?, pings?)
Sends a message to a channel. embeds and buttons are plain arrays, no JSON.stringify. An embed is {title, description, colorArgb, authorName, authorIconUrl, footerText, footerIconUrl, timestamp, imageUrl}; a button is {label, style ("primary"|"secondary"|"success"|"danger"), payload, handler}. handler names the script that runs when the button is clicked. A "Button Click" script replies, a "Button Click: Modal" script opens its form instead. payload comes back to the handling script as ctx.interaction.payload (max 50 chars).
-
channelId(string): Discord snowflake ID of the channel. -
content(string): Message content (empty string for embed-only). -
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 for a silent mention.
sendDm(userId, content, embeds?, buttons?, pings?)
Sends a direct message to a user. DMs always carry an embed attributing them to your server. Returns true if it was delivered, or false if the user doesn't accept DMs from your server, has blocked the bot, or shares no mutual guild. That's their privacy setting, so it won't stop your script. Check the result if you want to mention it (the seeded /ban does: it DMs the appeal link before banning, then tells the moderator when the DM didn't land).
-
userId(string): Discord snowflake ID of the user. -
content(string): Message content (empty string for embed-only). -
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 actually notify. Defaults to true; pass false for a silent mention.
editResponse(content, embeds?, buttons?, pings?)
Edits the bot's reply to the interaction. The reply is already showing a "thinking…" state by the time the script runs, so this fills it in. Call it as the last step. 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. Valid in every interaction trigger.
-
content(string): Message content (empty string for embed-only). -
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 actually notify. Defaults to true; pass false for a silent mention.
sendWebhookMessage(channelId, username, avatarUrl, content, embeds?, pings?)
Sends a message through a channel webhook with a custom username and avatar (persona messages). The webhook is created on first use. Webhook messages cannot carry buttons.
-
channelId(string): Discord snowflake ID of the channel. -
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 actually notify. Defaults to true; pass false for a silent mention.
sendPoll(channelId, question, answersJson, durationHours, allowMultiselect?)
Sends a native Discord poll to a channel. answersJson is a JSON array of 1-10 answer strings.
-
channelId(string): Discord snowflake ID of the channel. -
question(string): The poll question. -
answersJson(string): JSON array of answer strings (1-10). -
durationHours(number): How long the poll runs, in hours (defaults to 24). -
allowMultiselect(boolean, optional): Whether voters can pick multiple answers. Defaults to false.
endPoll(channelId, messageId)
Immediately closes a poll the bot posted.
-
channelId(string): Discord snowflake ID of the channel. -
messageId(string): Discord snowflake ID of the poll message.
Messages (12) ›
getMessage(channelId, messageId): CachedMessage | null
Fetches a single message from Discord live over REST, or null if it no longer exists. Counts as one rate-limited REST action (see ctx.restActionsRemaining). 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.
Returns CachedMessage | null: The message, or null when it doesn't exist.
getMessages(channelId, max): CachedMessage[]
Fetches a channel's most recent messages from Discord live over REST, newest first (up to 100). Counts as one rate-limited REST action (see ctx.restActionsRemaining). The channel must belong to this guild.
-
channelId(string): Discord snowflake ID of the channel (must be in this guild). -
max(number): Maximum messages to return (1-100; capped at 100).
Returns CachedMessage[]: Newest-first array of messages (empty when the channel has none).
deleteMessage(channelId, messageId)
Deletes a single message. Returns true if it was deleted, or false if it was already gone.
-
channelId(string): Discord snowflake ID of the channel. -
messageId(string): Discord snowflake ID of the message.
bulkDelete(channelId, count)
Deletes the most recent N messages in a channel (1-100). Messages older than 14 days are deleted individually, which is slower.
-
channelId(string): Discord snowflake ID of the channel. -
count(number): Number of recent messages to delete (1-100).
addReaction(channelId, messageId, emoji)
Adds a reaction to a message as the bot. Emoji is a unicode emoji ("🦆") or a custom emoji as "name:id".
-
channelId(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)
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 an empty string.
-
channelId(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): User whose reaction to remove, or empty string for all users.
clearReactions(channelId, messageId)
Removes every reaction from a message.
-
channelId(string): Discord snowflake ID of the channel. -
messageId(string): Discord snowflake ID of the message.
pinMessage(channelId, messageId)
Pins a message in its channel.
-
channelId(string): Discord snowflake ID of the channel. -
messageId(string): Discord snowflake ID of the message.
unpinMessage(channelId, messageId)
Unpins a message in its channel.
-
channelId(string): Discord snowflake ID of the channel. -
messageId(string): Discord snowflake ID of the message.
publishMessage(channelId, messageId)
Publishes (crossposts) a message in an announcement channel.
-
channelId(string): Discord snowflake ID of the announcement channel. -
messageId(string): Discord snowflake ID of the message.
editMessage(channelId, messageId, content, embeds?, buttons?, pings?)
Edits a message the bot previously sent in a channel. The edit fully replaces the message: embeds and buttons you don't pass are removed.
-
channelId(string): Discord snowflake ID of the channel. -
messageId(string): Discord snowflake ID of the message to edit. -
content(string): New message content (empty string for embed-only). -
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.
Threads (7) ›
createThread(channelId, name, isPrivate, userIds, initialMessage, kvKey)
Creates a thread under a channel, optionally adding initial members and an opening message. The new thread's ID is written to the KV store under kvKey (as a JSON string) before this call returns, so getKV(kvKey) can read it immediately.
-
channelId(string): Discord snowflake ID of the parent channel. -
name(string): Thread name. -
isPrivate(boolean): Create a private thread. -
userIds(string): Comma-separated user IDs to add to the thread, or empty string. -
initialMessage(string): Message posted into the thread after creation, or empty string. -
kvKey(string): KV key that receives the created thread's ID, or empty string.
addThreadMember(threadId, userId)
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)
Removes a user from a thread.
-
threadId(string): Discord snowflake ID of the thread. -
userId(string): Discord snowflake ID of the user.
setForumTags(threadId, tagIdsJson)
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.
-
threadId(string): Discord snowflake ID of the forum post (thread). -
tagIdsJson(string): JSON array of tag ID strings to apply.
archiveThread(threadId, archived)
Archives or unarchives a thread / forum post.
-
threadId(string): Discord snowflake ID of the thread. -
archived(boolean): true to archive, false to unarchive.
lockThread(threadId, locked)
Locks or unlocks a thread / forum post (locked threads can't get new messages).
-
threadId(string): Discord snowflake ID of the thread. -
locked(boolean): true to lock, false to unlock.
deleteThread(threadId)
Deletes a thread / forum post.
-
threadId(string): Discord snowflake ID of the thread to delete.
Members (9) ›
hasRole(userId, roleId): boolean
Returns true when the user currently has the given role. Backed by the bot's guild-member cache (kept live by member gateway events and re-paged hourly); 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.
Returns 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 kept live from gateway events and fully re-paged hourly (requires the bot's Server Members privileged intent). Page with the last userId of the previous call.
-
afterUserId(string): Return members with a user ID greater than this; empty string starts from the beginning. -
limit(number): Page size (1-500; 0 defaults to 100).
Returns MemberSummary[]: Array of cached members (empty when the cache has not been populated yet).
getMember(userId): MemberSummary | null
Returns one cached guild member (userId, username, nickname, joinedAt, roleIds), or null if not cached. Cheap cached read, no REST.
-
userId(string): Discord snowflake ID of the member.
Returns MemberSummary | null: The member, or null when not cached.
listRoles(): RoleSummary[]
Lists the guild's cached roles (id + name). Cheap cached read (no REST).
Returns RoleSummary[]: Array of the guild's roles.
setNickname(userId, nickname)
Sets or clears a member's nickname (empty string resets it to their username).
-
userId(string): Discord snowflake ID of the member. -
nickname(string): The new nickname (empty to reset).
moveMember(userId, channelId)
Moves a member to a voice channel (they must already be connected to voice).
-
userId(string): Discord snowflake ID of the member. -
channelId(string): Voice channel to move them into.
disconnectMember(userId)
Disconnects a member from voice.
-
userId(string): Discord snowflake ID of the member.
muteMember(userId, muted)
Server-mutes or unmutes a member in voice.
-
userId(string): Discord snowflake ID of the member. -
muted(boolean): true to mute, false to unmute.
deafenMember(userId, deafened)
Server-deafens or undeafens a member in voice.
-
userId(string): Discord snowflake ID of the member. -
deafened(boolean): true to deafen, false to undeafen.
Events (3) ›
createScheduledEvent(optionsJson, kvKey?)
Creates a guild scheduled event. optionsJson is a JSON object: {name, description, entityType ("external"|"voice"|"stage"), channelId, location, startsAt (ISO 8601), endsAt (ISO 8601)}. External events need location + endsAt; voice/stage events need channelId. When kvKey is set, the new event's ID is written there.
-
optionsJson(string): JSON event definition (see description). -
kvKey(string, optional): KV key to store the new event ID under (empty to skip).
modifyScheduledEvent(eventId, optionsJson)
Edits an existing scheduled event. optionsJson uses the same shape as createScheduledEvent, plus an optional status ("active"|"completed"|"canceled") to transition it -- e.g. flip a newly-created event to "active" so it shows as happening now, or "completed" when it ends. Only the fields you set change.
-
eventId(string): Discord snowflake ID of the scheduled event. -
optionsJson(string): JSON of the fields to change.
deleteScheduledEvent(eventId)
Deletes a scheduled event.
-
eventId(string): Discord snowflake ID of the scheduled event.
HTTP (1) ›
fetch(url, optionsJson?): FetchResponse
Performs an HTTP request to an external API or feed and returns {status, headers, body} (body is a string; 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 15s via optionsJson.timeoutMs), 1 MB response cap, 30 requests/minute per guild plus an hourly budget (see ctx.fetchCallsRemaining), and private/internal addresses are blocked. optionsJson is a JSON string: {method, headers (accept, content-type, authorization, user-agent, client-id, api-key, anthropic-version, and any "x-*" header), body, timeoutMs}. 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. -
optionsJson(string, optional): JSON-serialised options ({method, headers, body}), or empty string for a plain GET.
Returns FetchResponse: Object with status (number), headers (Record<string,string>) and body (string).
Roles (2) ›
addRole(userId, roleId)
Adds a role to a guild member.
-
userId(string): Discord snowflake ID of the user. -
roleId(string): Discord snowflake ID of the role.
removeRole(userId, roleId)
Removes a role from a guild member.
-
userId(string): Discord snowflake ID of the user. -
roleId(string): Discord snowflake ID of the role.
KV Store (3) ›
getKV(key): string | null
Retrieves a value from the guild's key-value store. The reserved system keys "env" and "secrets" are not accessible here. Read them through ctx.env / ctx.secret instead.
-
key(string): Key to look up.
Returns string | null: The stored JSON string, or null if not found / expired.
setKV(key, jsonValue, ttlSeconds)
Stores a value in the guild's key-value store, overwriting any existing value. Visible to getKV immediately within this execution, and persisted as it is called. The reserved system keys "env" and "secrets" cannot be written (they are managed on the dashboard's Database page).
-
key(string): Key to store under. -
jsonValue(string): JSON-serialised value to store. -
ttlSeconds(number): Time-to-live in seconds; use 0 for no expiry.
deleteKV(key)
Deletes a key from the guild's key-value store. The reserved system keys "env" and "secrets" cannot be deleted.
-
key(string): Key to delete.
Utility (1) ›
log(message)
Logs a message to the bot's server log.
-
message(string): Message to log.
Other subsystems
fetch()
The only network access scripts have. HTTPS-only by default, standard ports only,
GET/POST/PUT/PATCH/DELETE/HEAD only, a 5s default timeout (up to 15s via
timeoutMs), and a 1 MB response cap.
Private/internal addresses are blocked (SSRF-guarded). Limited to 30
requests/minute per guild, plus an hourly budget by tier. That budget is exposed as
ctx.fetchCallsRemaining so a loop can size itself
against what's left.
Environment variables
Guild-level configuration lives in a single key-value record with the key
env, whose value is a JSON object edited as one blob
on the Database page. Its keys are exposed to every script as
ctx.env.* (e.g.
{"mod_log_channel_id":"123"} becomes
ctx.env.mod_log_channel_id).
Secrets
Sensitive values like API keys live in a separate secrets
record that only guild administrators can view or edit; mods with script
access cannot. Scripts never receive the real values: each key is exposed as an opaque
placeholder token {{secret:NAME}} via
ctx.secret.*, and the real value is substituted server-side
only inside fetch() (in the URL, header values, or body).
Use it directly, e.g. authorization: "Bearer " + ctx.secret.MyKey.
A key is present only once an admin sets a non-empty value, so
if (ctx.secret.MyKey) tests whether it's configured.
Because a script only ever holds the placeholder, logging it, sending it in a message, or
throwing it can never leak the real secret.
Key-value store
Each guild has its own key-value store
(getKV/setKV/deleteKV) with optional per-entry expiry, browsable and
editable as JSON on the Database page. Storage is capped per guild by tier.
Message cache
An in-memory cache of every message seen in the last 30 minutes, scoped per guild.
It is used to recover best-effort deleted-message content on the
MessageDelete trigger.
getMessage() and
getMessages() do not read it; they fetch live from
Discord over REST (each counting as one rate-limited action), so they return complete,
authoritative history rather than only what the bot recently saw.
Twitch
Link a Twitch channel on the General page to enable the
TwitchStreamOnline,
TwitchStreamOffline, and
TwitchChannelUpdate triggers. Just entering the
broadcaster's channel name is enough, no authorization needed. Those events dispatch
immediately and populate ctx.twitch with
broadcasterId/broadcasterLogin, plus title/category on a channel update or
startedAt on going live.
TwitchChatBatch additionally needs a chat-reader
authorization from the streamer or one of their moderators. Chat is buffered per
guild (1000 messages per window; once full, the oldest plain message is dropped to
make room; redemptions and bit cheers are only evicted if literally everything
buffered is one of those. The drop count is reported as
ctx.twitch.messagesDropped) and flushed as one
execution every ~10 minutes via ctx.twitch.messages,
an array of { chatterId, chatterLogin, chatterDisplayName, text, rewardId, bits, sentAt },
never per message, so it can't burn through the hourly execution budget. Nightbot's
messages are filtered out before they reach a script. A channel-point text
redemption arrives as a normal chat message with
rewardId set, and a bit cheer with
bits set, which is how redemption/cheer flows (e.g. a
shoutout command) are scripted without needing broadcaster-only scopes.
Status chips (Pending / Active / Error / Reauthorize Required / Not Connected) on
the General page reflect the current connection state; a revoked authorization or a
reader losing mod status flips the status and is cleaned up automatically within
about a minute.
Tiers & limits
Every guild has a tier (Free, Plus, Pro, or Ultra) that caps hourly script runtime,
hourly Discord actions and fetch calls, per-script length, total
key-value storage, and how often a Scheduled (timer) script may run: the minimum
interval is 30 minutes on Free, 15 on Plus, 10 on Pro, and 5 on Ultra. The
Discord-action budget (sends, bans, role changes, …) is
exposed as ctx.restActionsRemaining; an action beyond
it throws and the script's remaining actions don't run, so size data-dependent loops
against it the same way you would with
ctx.fetchCallsRemaining. Usage against all these caps
is shown on the guild's General page. There is also an hourly cap on the number of
script executions, but it is a runaway backstop set well above normal use rather than
a budget to plan around — runtime and Discord actions are what bind first.
Ready to try it?
Add Mallard to your server, then manage its commands and scripts from the dashboard.