Custom commands
Build your own ,commands with variables, conditionals, actions and a fully sandboxed Script API.
What custom commands are
Custom commands let admins create their own ,commands. They are created and managed only on the dashboard under Server > Custom Commands, gated by the custom_commands feature flag, and run in Discord via ,<name>. A custom command can never shadow a built-in command.
Standard
response + actionsA response template rendered with variables, inline functions and conditionals, plus an optional sequence of actions built on the dashboard.
Script
sandboxed JSSandboxed JavaScript with the full exceed API. The script enqueues actions that run afterward and pass every guardrail.
Safety model
always enforced- A custom command runs with the bot's permissions, but every action passes guardrails before it executes.
- Role changes respect role hierarchy via
role_guard. The bot can never act above its own highest role. allowedMentionsis locked, so a command can never@everyoneor mass-mention.- Moderation actions are triple-gated: the invoker must hold the matching Discord permission, the bot must hold it, and the bot must outrank the target.
- Output and action caps apply, and there is no recursion, so commands cannot loop into themselves.
Variables
Used in Standard responses and the {embed} / $v{} engine. Each group exposes the same fields where it makes sense.
Invoker
{user.*}Fields
Target & random user
{target.*} / {random_user.*}{target.*} is the first mention or id in the args and falls back to the invoker. {random_user.*} is a random human member. Both expose the same fields as the invoker.
Server
{server.*}Fields
Channel
{channel.*}Fields
Message & misc
{message.*}Tokens
Arguments
Tokens
Indices are zero-based.
Storage
{db.<key>}A saved per-server value. Set it from the dashboard, or use {set:k|v} for scratch storage during a render.
Last.fm
{lastfm.*}Auto-filled from the runner's linked Last.fm account. The member must link Last.fm with the bot's lastfm command first. {lastfm.nowplaying} is true or false, and {lastfm.status} is Now playing or Last played.
Fields
Inline functions
These work anywhere in a response and render in place.
Math & strings
{math:1+2*3} | Safe arithmetic with + - * / % and parentheses. |
{upper:t} {lower:t} {title:t} | Change case. |
{len:t} {reverse:t} {trim:t} {urlencode:t} | Length, reverse, trim, URL-encode. |
{replace:src|find|with} | Replace a substring. |
{substring:src|start|end} | Slice a string. |
{repeat:src|n} | Repeat a string n times. |
{lpad:src|len|char} | Left-pad to a length. |
{round:n|digits} {abs:n} | Round to digits, absolute value. |
Randomness
{choose:a|b|c} | Pick a random option. |
{random:lo-hi} | Random integer in a range. |
{dice:NdM} | Roll N dice with M sides. |
Scratch storage & time
{set:key|value} | Store a scratch value; renders empty. |
{get:key} | Read a scratch value. |
{time} {date} {now} {unix} | Current time, date, datetime and unix seconds. |
{timestamp:R} | A Discord timestamp. Styles: t T d D f F R. |
External data
These tokens pull live data from outside services. Argument tokens take values after a colon and are cached for about a minute; the no-argument tokens refresh quickly. Any lookup that fails renders an empty string.
Crypto
{crypto:<coin>}{crypto:<coin>} | USD price of a coin, e.g. {crypto:bitcoin} or {crypto:eth}. |
{crypto:<coin>:change} | 24h percent change. |
{crypto:<coin>:marketcap} | Market cap in USD. |
Currency
{currency:<amount>:<from>:<to>}{currency:<amount>:<from>:<to>} | Convert between currencies, e.g. {currency:100:USD:EUR}. |
Minecraft
{minecraft:<name>}{minecraft:<name>} | The player's UUID. |
{minecraft:<name>:name} | The player's current name. |
{minecraft:<name>:avatar} | Avatar image URL. |
{minecraft:<name>:skin} | Skin image URL. |
{minecraft:<name>:body} | Body render image URL. |
Weather
{weather:<city>}{weather:<city>} | Short summary, e.g. 19.5°C in London. |
{weather:<city>:temp} | Temperature in Celsius. |
{weather:<city>:humidity} | Humidity percentage. |
{weather:<city>:wind} | Wind speed. |
{weather:<city>:city} | Resolved city name. |
GitHub
{github:<user>} / {github:<owner>/<repo>}{github:<user>} | Follower count, e.g. {github:torvalds}. |
{github:<user>:repos} | Public repo count. |
{github:<user>:name} | Display name. |
{github:<user>:bio} | Profile bio. |
{github:<user>:avatar} | Avatar image URL. |
{github:<user>:url} | Profile URL. |
{github:<owner>/<repo>} | Star count, e.g. {github:facebook/react}. |
{github:<owner>/<repo>:forks} | Fork count. |
{github:<owner>/<repo>:language} | Primary language. |
{github:<owner>/<repo>:issues} | Open issue count. |
{github:<owner>/<repo>:description} | Repo description. |
{github:<owner>/<repo>:url} | Repo URL. |
Pokemon
{pokemon:<name>}{pokemon:<name>} | Name and types, e.g. {pokemon:pikachu}. |
{pokemon:<name>:id} | Pokedex number. |
{pokemon:<name>:types} | Type list. |
{pokemon:<name>:height} | Height. |
{pokemon:<name>:weight} | Weight. |
{pokemon:<name>:sprite} | Sprite image URL. |
Reference
{dictionary} / {urban} / {npm}{dictionary:<word>} | First definition. Alias: {define:<word>}. |
{urban:<term>} | Top Urban Dictionary definition. |
{npm:<package>} | Latest version, e.g. {npm:react}. |
{npm:<package>:description} | Package description. |
{npm:<package>:license} | Package license. |
{npm:<package>:homepage} | Package homepage URL. |
Random
no argumentsThese take no arguments. Drop the bare token in and it resolves on each run.
{advice} | A random piece of advice. |
{joke} | A random safe joke. |
{cat} | A random cat image URL. |
{dog} | A random dog image URL. |
Conditionals
Branch your response with if / elseif / else blocks.
Syntax
{if <condition>}...{elseif <condition>}...{else}...{/if}Operators
Example
{if {args.length} > 0}Hello {args.0}{else}Hello there{/if}Actions
Standard advanced mode runs a sequence of actions built on the dashboard. Each one passes the same guardrails before it fires.
Standard actions
reply | Reply to the invoking message. |
send_embed | Send a rendered embed. |
send_to_channel | Send a message to another channel. |
dm_user | Direct-message a user. |
react | React to the invoking message. |
pin_message | Pin the invoking message. |
delete_invoking_message | Delete the message that ran the command. |
add_role / remove_role / toggle_role | Manage a role on the user. |
require_role | Gate the command behind a role. |
counter | Increment, decrement or set a counter. |
set_var / db_set | Set a render variable or persist a stored value. |
wait | Pause the sequence for 0 to 10 seconds. |
if | Branch with nested then / else action lists. |
stop | Halt the sequence early. |
buttons | Up to 5: role-toggle, link, or run another command. |
Moderation actions
triple-gatedThe invoker must hold the matching Discord permission, the bot must hold it, and the bot must outrank the target.
timeout_user | Time a member out. |
kick_user | Kick a member. |
ban_user | Ban a member. |
set_nickname | Change a member's nickname. |
purge | Bulk-delete recent messages. |
Script API
Script-type commands run sandboxed JavaScript. Everything is on the global exceed object; standard Math / JSON / String / Array / Date are available. The script enqueues actions that run after it and pass every guardrail.
Sandbox limits
Enforced
Context
read-onlyexceed.user / exceed.target / exceed.randomUser / exceed.bot | Each carries { id, name, username, tag, mention, nick, display, avatar, createdAt, joinedAt, joinDays, accountAge, isBooster, color, topRole, roles[], roleNames[], isBot }. |
exceed.server | { id, name, memberCount, humanCount, botCount, ownerId, owner, createdAt, boostCount, boostTier, roleCount, channelCount, emojiCount, vanity, icon, description }. |
exceed.channel | { id, name, mention, topic, nsfw, type, categoryId, createdAt }. |
exceed.message | { id, content, link }. |
exceed.guildRoles[] | { id, name, mention, color, position, mentionable, hoist, memberCount } for every role. |
exceed.guildChannels[] | { id, name, mention, type, categoryId } for every channel. |
exceed.guildEmojis[] | { id, name, animated, mention } for every custom emoji. |
exceed.args[] / exceed.argString / exceed.mentions[] | Parsed arguments, the raw argument string, and resolved mentions. |
exceed.prefix / exceed.commandName / exceed.now | The active prefix, command name and current unix time. |
Storage
exceed.store.get(key) | Read a guild-scoped value. |
exceed.store.set(key, value) | Write a guild-scoped value that persists across runs. |
exceed.store.setUser(key, value) | Write a value scoped to the invoking user. |
Lookups
exceed.getRole(id) | Resolve a role by id. |
exceed.findRole(name) | Resolve a role by name. |
exceed.getChannel(id) | Resolve a channel by id. |
exceed.findChannel(name) | Resolve a channel by name. |
exceed.findEmoji(name) | Resolve a custom emoji by name. |
exceed.hasRole(id) | True if the invoker holds the role. |
exceed.targetHasRole(id) | True if the target holds the role. |
Messaging actions
exceed.reply(text) | Reply to the invoking message. |
exceed.send(text) | Send a plain message to the channel. |
exceed.embed(text) | Send the text rendered as an embed. |
exceed.sendTo(channelId, text) | Send a message to another channel. |
exceed.dm(text) | Direct-message the invoker. |
exceed.react(emoji) | React to the invoking message. |
exceed.pin() | Pin the invoking message. |
exceed.deleteInvoke() | Delete the invoking message. |
exceed.button({ label, style, roleId | url | run }) | Attach a button: role-toggle, link, or run another command (max 5). |
Role actions
exceed.addRole(id) | Add a role to the invoker. |
exceed.removeRole(id) | Remove a role from the invoker. |
exceed.toggleRole(id) | Toggle a role on the invoker. |
exceed.requireRole(id) | Gate the command; stop unless the invoker holds the role. |
Moderation actions
mod-gatedexceed.timeout(seconds, reason) | Time the target out. |
exceed.kick(reason) | Kick the target. |
exceed.ban(reason, deleteDays) | Ban the target. |
exceed.setNick(name) | Set the target's nickname. |
exceed.purge(count) | Bulk-delete recent messages. |
State actions
exceed.setVar(name, value) | Set a render variable for later tokens. |
exceed.counter(name, op, by) | Apply a counter op (inc / dec / set). |
exceed.incr(name, by) | Increment a counter. |
exceed.decr(name, by) | Decrement a counter. |
exceed.setCounter(name, value) | Set a counter to an exact value. |
Flow actions
exceed.wait(seconds) | Pause the action queue (0 to 10 seconds). |
exceed.stop() | Halt the command and run no further actions. |
String helpers
exceed.str.*Methods
Number helpers
exceed.num.*Methods
Format helpers
exceed.fmt.*Methods
Time helpers
exceed.time.*Methods
Random helpers
exceed.rng.*Methods
Array helpers
exceed.arr.*Methods
Top-level shortcuts
Also available
Examples
Realistic commands you can adapt to your own server.
Standard variable command
,welcomeWelcome {user.mention} to {server.name}! You are member #{server.membercount}.Conditional
,roll{user.name} rolled {random:1-100}{if {random:1-100} > 95} (critical!){/if}Now playing
,np{lastfm.status}: {lastfm.track} by {lastfm.artist}
Scrobbles: {lastfm.scrobbles}Crypto price
,btcBitcoin is at ${crypto:bitcoin} ({crypto:bitcoin:change}% today).Script: react and assign a role
exceed.reply("Hi " + exceed.user.name);
exceed.toggleRole("ROLE_ID");Script: helpers
var msg = exceed.fmt.bold(exceed.str.title(exceed.argString));
exceed.reply(msg + " rolled " + exceed.rng.dice(2, 6));Script: persistent storage
var c = Number(exceed.store.get("count") || 0) + 1;
exceed.store.set("count", c);
exceed.reply("Used " + exceed.num.ordinal(c) + " time.");