Mallard

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, either as JavaScript or as blocks that compile to it. This page walks through how it works; the sections below hold the full API reference.

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 on this site. No hosting, no deploy step: saving a script takes effect immediately.

Easy Mode and Advanced Mode

A script's editor offers two modes for its body. Advanced is the code editor above, and is what everything else on this site documents. Easy builds the script from a palette of blocks instead: 30 action blocks drawn from the same function catalogue this reference is generated from, plus an If with an optional otherwise branch and a block that stops the run early. A script may hold up to 60 blocks across every branch, nested at most 3 deep.

An If tests one of two things. Either it compares a value against typed text (is, is not, contains, does not contain, is greater than, is less than, or the one-sided is empty / is not empty, which is how a script checks a setting before using it), or it calls a check: a function that answers yes or no, such as hasRole. Either test can be inverted, so "does not have the role" is the same block with the check flipped.

Easy Mode is not a second runtime. The blocks compile to ordinary JavaScript, shown below the canvas as you build, and that JavaScript is the only artifact that ever runs: nothing on the execution path knows the blocks exist. So a block script has the same triggers, the same filters, the same budgets and the same logs and traces as one written by hand.

The palette is filtered by trigger, so a block calling a function that trigger cannot reach is never offered. Values from the event, the command's own options and the server's settings sit above the canvas as chips: drag one onto a field and it leaves a {{path}} token that compiles to the matching ctx path. A text field takes a token anywhere in what you typed, so Banned <@{{args.user}}> is a field value and compiles to the concatenation you would have written by hand. Channel and role fields are pickers instead, listing your server's real channels and roles, and hold one whole value.

Easy Mode is an early preview and covers a fraction of the API: no loops, no key-value storage, no fetch, no buttons, and embeds only through sendEmbed, which sends one embed with a title, body and colour. Switching a script to Advanced hands you the generated JavaScript to edit and discards the blocks, and there is no way back once it is edited. Anything you are relying on is better written in Advanced mode.

How it works

One process holds the Discord gateway connection and decides what should run; a pool of interchangeable runners does the running:

  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, applies that script's filters, and checks the guild's hourly budgets. Anything admitted is written to a job queue. Because that decision is made once, by the single process that sees every event, the budget counting stays honest no matter which runner picks the job up.
  3. A runner claims the job, usually within milliseconds. Runners are anonymous and no guild belongs to any of them, but only one job per guild is ever in flight across the whole pool, and each guild's jobs are claimed oldest-first, so two scripts in one server never race on the key-value store. Different guilds run concurrently.
  4. 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 all-or-nothing rollback: a script that errors partway keeps whatever actions and writes it already made.
  5. Once the script finishes (or errors, or times out), an execution record is saved with its status, duration, how long it waited to be claimed, and any error, viewable on the guild's Logs page.
Because there's no rollback, scripts that need to do something later (a temp ban's unban, a reminder, …) log that intent to the key-value store and pick it up from a Scheduled script that sweeps for it periodically, rather than "waiting." Every setKV call lands in the database the instant it's called, so writes already made survive a later error in the same run. See the KV store section.
A failed Discord action normally ends the script. The exception is a target that simply isn't reachable: a user who doesn't accept DMs, a member who already left, a message already deleted. Those aren't something your script can control, so sendDm, kick, timeout, unban and deleteMessage return false and your script keeps going. Everything else still stops it; full detail on Subsystems & limits.

Ready to try it?

Add Mallard to your server, then manage its commands and scripts from the dashboard.

Go to Dashboard
An unhandled error has occurred. Reload 🗙