Mallard

Mallard: a Discord bot you write yourself

EasySlashCommand
Ban a member
User id
{{args.user}}
Reason
{{args.reason}}
Delete message days
0
Reply to the command
Content
Banned {{args.user}}. Reason: {{args.reason}}
EasySlashCommand
Set a channel's slowmode
Channel id
The channel this happened in
Seconds
{{args.seconds}}
Reply to the command
Content
Slowmode set to {{args.seconds}} seconds.
EasySlashCommand
Time a member out
User id
{{args.user}}
Reason
{{args.reason}}
Duration minutes
{{args.duration_min}}
Reply to the command
Content
Timed out {{args.user}} for {{args.duration_min}} minutes.
assembled from blocks
ban.jsSlashCommand
var userId = ctx.args.user;
var reason = ctx.args.reason || "No reason provided";

// DM them first. After the ban there is no mutual
// server left to reach them through.
sendDm(userId, "", [{
    title: "Banned",
    description: "Reason: " + reason
}]);

ban(userId, reason, 0);

var target = getMember(userId);
var targetName = target ? target.username : userId;
editResponse("Banned " + targetName + " (" + userId + "). Reason: " + reason);
slowmode.jsSlashCommand
var seconds = parseInt(ctx.args.seconds);

if (isNaN(seconds) || seconds < 0 || seconds > 21600) {
    editResponse("Invalid slowmode. Must be 0 to 21600.");
} else {
    setSlowmode(ctx.channelId, seconds);
    editResponse("Slowmode set to " + seconds + " seconds.");
}
timeout.jsSlashCommand
var userId = ctx.args.user;
var reason = ctx.args.reason || "No reason provided";
var durationMin = parseInt(ctx.args.duration_min);

timeout(userId, reason, durationMin);

// Mod actions go in the guild's own key-value store, one
// record per user, so /notes can read them back later.
var notes = JSON.parse(getKV("mod:notes:" + userId) || "[]");
notes.push({ type: "timeout", userId: userId, reason: reason });
setKV("mod:notes:" + userId, JSON.stringify(notes));

var target = getMember(userId);
var targetName = target ? target.username : userId;
editResponse("Timed out " + targetName + " (" + userId + ") for "
    + durationMin + " minutes. Reason: " + reason);
the same command, written out and doing more
51 triggers79 functions32 seeded commands$0 to start
Free to start, and it ships with 32 scripts like this one. Discord will ask which server, then drop you straight into its dashboard.

beta: pricing and limits still being tuned

What Easy Mode writes for you

Blocks compile to ordinary JavaScript, and you can read it below the canvas while you build. That file is the only thing that ever runs, so a script assembled here gets the same triggers and the same logs as one you typed. When you want the rest of the language, take the file and carry on in it.

EasyGuildMemberAdd
Give a member a role
User id
{{actorId}}
Role id
Verified
Send a message
Channel id
#general
Content
Welcome! Read the rules and say hi.

compiles to

// Give a member a role
addRole(ctx.actorId, "1528016624483958906");

// Send a message
sendMessage("1527981654843457679", "Welcome! Read the rules and say hi.");
the same script, twice

30 action blocks, plus conditions and an early exit, up to 60 of them per script. It deliberately does not cover everything: no loops, no stored data, no fetch, no buttons, and one embed at a time. That is what the right-hand column above is for, and switching to Advanced hands you the file to keep going in.

easy mode is an early preview, and still changing

Look inside the dashboard

Everything below is the actual editing surface, not a mockup.

dashboard / scripts / easy

Blocks, when you would rather not type it

Every block is one function from the same catalogue the docs list, filtered to what your trigger can call. The JavaScript it becomes sits under the canvas as you build.

The Easy Mode canvas: a palette of action blocks grouped by category on the left, and on the right an If block holding a Send an embed block whose fields are filled from the event.
dashboard / scripts

A real editor, not a text box

Monaco with syntax highlighting and autocomplete against the same API the docs describe. Save and it is live, with no deploy step.

The Mallard script editor, with an Available Functions reference above a JavaScript script in a code editor.
dashboard / scripts / trigger

Slash commands you define

Typed options, required flags, and the Discord permission a member needs to see the command. Mallard registers it with Discord for you.

The trigger editor for a slash command, showing three typed command options with their required flags and a required-permissions selector.
dashboard / database

Storage your scripts share

A per-guild key-value store your scripts read and write, plus env values and write-only secrets, all editable from the dashboard.

The dashboard database page listing key-value entries for a guild, including the env row and the write-only secrets row.

What a script looks like

Plain JavaScript against a small API. Discord actions, an HTTP client, per-guild storage, and secrets your moderators never see.

imgspam.js

Catch image-spam automatically

MessageCreate
// Times out repeat image-posters for mods to review, no command needed
const key = `imgspam.${ctx.message.authorId}`;
const recent = JSON.parse(getKV(key) || "[]")
    .filter(e => Date.now() - e.timestamp < 10000);
recent.push({ messageId: ctx.message.id, timestamp: Date.now() });
setKV(key, JSON.stringify(recent), 60);

if (recent.length >= 3 && ctx.message.attachments.length > 0) {
    timeout(ctx.message.authorId, "Image spam", 1440);
    sendMessage(ctx.env.mod_alert_channel_id, "🖼️ Timed out for image spam. Please review.");
}
ai-summary.js

Call an AI without leaking your key

SlashCommand
// ctx.secret.* are opaque {{secret:NAME}} tokens. The real key is
// injected only inside fetch(), never visible to the script (or your mods)
const chat = getMessages(ctx.channelId, 100).reverse()
    .map(function (m) { return m.authorDisplayName + ": " + m.content; })
    .join("\n");

const res = fetch("https://openrouter.ai/api/v1/chat/completions", JSON.stringify({
    method: "POST",
    headers: {
        authorization: "Bearer " + ctx.secret.OpenrouterKey,
        "content-type": "application/json"
    },
    body: JSON.stringify({
        model: "google/gemini-3.1-flash-lite",
        messages: [{ role: "user", content: "Summarize this chat:\n" + chat }]
    })
}));
editResponse(JSON.parse(res.body).choices[0].message.content);
golive.js

Announce when you go live

TwitchStreamOnline
// Twitch events arrive as triggers just like Discord ones
if (ctx.env.twitch_announce_channel_id) {
    sendMessage(ctx.env.twitch_announce_channel_id, "", [{
        title: "🔴 Live now: " + ctx.twitch.title,
        description: "Playing " + ctx.twitch.category
            + " at twitch.tv/" + ctx.twitch.broadcasterLogin
    }]);
}

Browse the full template library

Start with what you already need

Install it, use the commands it comes with, and change them when they get in your way. Add the next one out of blocks, or write it. If you would rather see the whole API first, the docs cover every trigger and function.

Read the docs Go to dashboard

Questions, or want to see what other servers are running? The support server has a scripts hub.

An unhandled error has occurred. Reload 🗙