Build on BedrockRelay
Add Discord commands to Minecraft servers with a plugin, or post to Discord from any addon with a script event. Either way, BedrockRelay handles Discord for you: you never need a bot, a token or a webhook.
Two ways to build
| You want to | Use | What you write |
|---|---|---|
Add a Discord command, like /playtime Steve | A plugin | One JavaScript file that BedrockRelay loads inside its pack |
| Post to Discord when something happens in your own addon | Script events | A system.sendScriptEvent call in your existing addon |
Plugins run on the Minecraft server with the ordinary Minecraft Script API. BedrockRelay registers their commands in Discord, checks who may use them, and delivers the answers. Script events need nothing from BedrockRelay except that it's installed.
Your first plugin
A plugin is one file that default-exports a definition: who it is, and the Discord commands it adds, each with a run().
// health.js
import { findPlayer } from "../relay/api.js";
export default {
id: "health",
name: "Health",
version: "1.0.0",
description: "Check a player's health from Discord.",
commands: [
{
name: "health",
description: "Show a player's health",
options: [{ name: "player", type: "player", description: "The player's name", required: true }],
run({ player: name }) {
const player = findPlayer(name);
if (!player) return `**${name}** isn't online right now.`;
const health = player.getComponent("minecraft:health");
return { embed: { title: player.name, description: `❤ ${Math.ceil(health.currentValue)} / ${health.effectiveMax}` } };
},
},
],
};
To try it:
- Put
health.jsindevelopment_behavior_packs/BedrockRelay/scripts/plugins/on a Minecraft server running BedrockRelay (pack v0.3.0 or newer). - Add its name to
scripts/plugins/index.js:export default ["health"]; - Restart the Minecraft server. The console shows
[BedrockRelay] plugin loaded: Health 1.0.0. - On the dashboard, open that Minecraft server's page and switch Health on under Plugins.
- In a connected Discord server, run
/health.
Plugin reference
The definition
| Field | Type | Description |
|---|---|---|
id | string, required | Lowercase letters, numbers and dashes, up to 32 characters. Must match the file name. Never change it once published. |
name | string | Shown on the dashboard. |
version | string | Like 1.2.0. Raise it with every release; the dashboard offers updates by comparing it. |
description | string | One or two sentences, shown on the dashboard. |
privacy | string | Shown beside the on/off switch. Required in the catalog if the plugin reveals anything about players. |
posts | string | Say what your plugin posts by itself with postToDiscord, e.g. "Playtime milestones." The owner then chooses which channels get those posts. Pack v0.4.0 or newer. |
postKinds | array | If your plugin posts several kinds of thing, list them so the owner can switch each off: up to 25 of { id, name, default }. id is lowercase letters, numbers, dashes and underscores; name is the switch's label; default: false starts it switched off. Then pass the kind with each post. Only with posts. Every kind goes to the same channels. Pack v0.4.2 or newer; older packs post every kind. |
commands | array | The Discord commands, below. Up to 10. |
A command
| Field | Type | Description |
|---|---|---|
name | string, required | Becomes /name in Discord. Lowercase, up to 32 characters. relay is reserved. |
description | string | Shown in Discord's command list, up to 100 characters. |
options | array | Up to 10, below. Required options come first. |
public | boolean | true shows the answer to everyone in the channel instead of only the person who asked. Use it for things meant to be shared, like leaderboards, and never for anything private about players. Pack v0.4.0 or newer. |
confirm | string or true | Ask "are you sure?" with Yes and Cancel buttons before running. Options fill in by name: "Kick {player}?". Use it for anything that changes the world. Pack v0.4.0 or newer. |
run(args, context) | function, required | Called when someone uses the command. May be async. |
Options
type | Discord shows | run() receives |
|---|---|---|
player | Text, suggesting players online now first, then everyone who has played recently | string |
string | Text | string |
integer | A whole number | number |
number | A number | number |
boolean | True / false | boolean |
Each option also takes a name (the key in args; server is reserved), a description, and required. An optional option that wasn't given is missing from args.
A string, integer or number option can offer fixed answers with choices: up to 25 of { name, value }, where name is what Discord shows and value is what run() receives (pack v0.4.0 or newer). Still check the value: older packs send the option as plain text.
{ name: "stat", type: "string", description: "Which statistic", required: true,
choices: [{ name: "Playtime", value: "playtime" }, { name: "Deaths", value: "deaths" }] }
What run() returns
- A string: shown as a message.
{ message },{ embed }, or both.embedis a Discord embed:title,description,color(a number),author,fields,footer,thumbnail,imageandurlare supported. For a player's head, usethumbnail: { player: "Steve" }orauthor: { name: "Steve", player: "Steve" }in place of an image link.- Throwing an
Error: its message is shown to the person, so make it helpful.
The second argument, context, tells you about the request. Access has already been checked.
| Field | Description |
|---|---|
requestedBy | The Discord display name of whoever ran the command, for your information only. |
linkedPlayer | Their Minecraft name if they've linked their account with /relay link, otherwise null. Use it so people can leave out their own name: const name = args.player ?? context.linkedPlayer. Pack v0.4.0 or newer. |
Helpers
Import these from "../relay/api.js". Everything else is the normal @minecraft/server API.
| Helper | Does |
|---|---|
findPlayer(name) | An online player by name, ignoring case, or undefined. |
prettyName(typeId) | "minecraft:diamond_sword" → "Diamond Sword". |
postToDiscord({ plugin, kind, message, embed, author }) | Post at any time, not only in reply to a command. Pass your plugin's id as plugin and declare posts, and it goes only to the channels the owner chose for your plugin, and nowhere while it's switched off. Without them, it reaches every channel with the Other addons event. Pass one of your postKinds as kind and it's dropped while the owner has that kind switched off; a post with no kind, or one you didn't declare, always goes. |
How plugins work with the gateway
- Loading. When the Minecraft server starts, BedrockRelay loads each plugin in
index.json its own. A plugin that throws or is missing is logged and skipped; the others, and the relay, carry on. - Reporting. When it connects, the pack tells the BedrockRelay gateway which plugins loaded and what commands they have. It sends names and options, never code.
- Switching on. The Minecraft server's owner switches a plugin on from the dashboard. Only then are its commands registered, and only in the Discord servers that Minecraft server is connected to. At first only Discord administrators can use them; the owner can add roles.
- Running. When someone uses a command, the gateway checks it's allowed and sends it to the pack. Your
run()executes on the next game tick, where you can read and change the world freely. - Answering. Whatever you return goes back to the gateway, which removes Minecraft colour codes, defuses
@everyone, keeps embeds within Discord's limits and accepts onlyhttpsimages. It's shown only to the person who asked, unless the command ispublic.
No answer within about 12 seconds is shown as the Minecraft server being offline. Your plugin never sees a Discord token, channel or role, and doesn't need to.
Best practices
Be quick
run() shares the game tick. Read what you need and return. Avoid scanning large areas or every entity in a dimension; for genuinely heavy work, spread it over ticks with system.runJob and keep it well inside the 12-second limit.
Expect the unexpected
Players go offline between the command and your code. Blocks and entities can be unloaded. Arguments are typed by people. Check before you use anything, and return a clear message rather than throwing a raw error.
Respect players' privacy
If your plugin reveals anything about players (where they are, what they own, when they were online), set privacy to say so plainly. Owners see it next to the switch. Don't collect or post more than the command needs.
Read more than you write
A command that changes the world (teleporting, giving items, changing game rules) should say so in its description, should be something an owner would expect, and should ask first with confirm. Plugins that surprise people won't be listed.
Stay in your lane
- Don't make network requests or import
@minecraft/server-netor@minecraft/server-admin, and don't read server secrets or variables. BedrockRelay already talks to Discord for you. - Don't rely on other plugins, on load order, or on globals. Keep everything in your own file.
- Prefix anything you store, like dynamic properties, scoreboard objectives or tags, with your plugin's id, e.g.
health:last_seen. - Subscribe only to the events you use.
Keep answers short
Prefer an embed with a few fields to a wall of text. Discord cuts fields at 1024 characters, so summarise long lists, e.g. "…and 12 more".
Post sparingly
Only post by yourself for things people would be glad to see, like a milestone, and never for every small event. Declare posts so the owner decides where it goes.
Don't touch the world while loading
Code at the top of your file runs while the world is still starting, when calls like world.getAllPlayers() aren't allowed. Subscribe to events there, and put anything that reads the world inside system.run().
Version carefully
Keep id and command names stable: changing them breaks people's habits and the roles owners have set up. Raise version with every change, using major.minor.patch.
Testing
- Develop on a Minecraft server of your own that runs BedrockRelay, with
content-log-console-output-enabled=trueinserver.propertiesso you can see the pack's messages. - On start you'll see
plugin loaded: Your Plugin 1.0.0, orplugin "x" was skippedorfailed to loadwith the reason. Bedrock also prints its own "Unhandled promise rejection" line for a plugin that throws while loading; that's expected, and the other plugins still load. - Errors thrown in
run()are logged as[BedrockRelay] /command (plugin) failed: …, and the message is shown in Discord. - After changing a plugin, restart the Minecraft server to load the new version.
Publishing to the catalog
Plugins in the catalog appear on every owner's dashboard, ready to install on their Minecraft servers. The catalog lives in the BedrockRelay plugins repository. To submit a plugin, open a pull request that adds one folder:
plugins/health/
plugin.js ← your plugin, exactly as it will be installed
plugin.json ← how it appears in the catalog
discord.png ← optional: what it looks like in Discord
{
"id": "health",
"name": "Health",
"version": "1.0.0",
"author": "Your name",
"description": "Check a player's health from Discord.",
"privacy": "Shows a player's current health.",
"homepage": "https://github.com/you/health",
"minPackVersion": "0.3.0",
"commands": ["/health <player>"],
"screenshots": [
{ "file": "discord.png", "caption": "/health answering in Discord" }
]
}
screenshots is optional: up to four pictures of your plugin in Discord, shown on its catalog card. Each file sits in the plugin's folder and is a PNG, JPEG or WebP under 1 MB. Its caption says what it shows and doubles as the alt text. Crop to the message itself, since the card shows the top of each picture and a click shows it whole.
id must match the folder, and id and version must match what plugin.js declares. Every submission and update is reviewed before it's listed. Reviewers check that it:
- does what its description says, and nothing else
- follows the best practices, including no network access and no secrets
- declares
privacyif it reveals anything about players - has screenshots, if any, that show it as it really looks, with no personal details or other people's names visible unless they agreed
- is readable: no minified or obfuscated code
Owners install exactly the file that was reviewed. To release an update, open a pull request that changes plugin.js and raises version in both files.
Licence. Everything in the catalog is published under the MIT licence, so anyone can use, change and share it. By submitting a plugin, or an update to one, you agree to license it under MIT and confirm you have the right to.
Script events
Any behaviour pack on a Minecraft server running BedrockRelay can post to Discord by firing a script event, with no plugin and no dependency. The message is a JSON string: build an object and pass it through JSON.stringify.
| Event | Direction | Purpose |
|---|---|---|
discord:message | Your addon → Discord | A plain text message. |
discord:embed | Your addon → Discord | A rich Discord embed. |
discord:ready | BedrockRelay → your addon | Tells you BedrockRelay is present and connected. |
discord:message
| Field | Type | Description |
|---|---|---|
message | string, required | The text to post. Discord markdown works. |
author | string | The name the post appears under. Defaults to the Minecraft server's name. |
picture | string (URL) | An image URL for the post's avatar. |
import { system } from "@minecraft/server";
system.sendScriptEvent("discord:message", JSON.stringify({
author: "Treasure Hunt",
message: "**Steve** found the golden chest at the old lighthouse!",
}));
discord:embed
Takes embed (a Discord embed object, required), plus author and picture as above.
system.sendScriptEvent("discord:embed", JSON.stringify({
author: "Weekly Leaderboard",
embed: {
title: "Most blocks mined this week",
color: 0x5de0c7,
fields: [
{ name: "1. Alex", value: "12,408", inline: true },
{ name: "2. Steve", value: "9,771", inline: true },
],
},
}));
Discord's embed limits apply, such as 256 characters for a title and 25 fields. Bedrock limits a script event's message to 2048 characters, so keep embeds compact.
discord:ready
BedrockRelay fires discord:ready once it's connected, then every 200 ticks (about 10 seconds) while it stays connected, so an addon that loads later still hears it. The message is always {}.
let discordAvailable = false;
system.afterEvents.scriptEventReceive.subscribe((event) => {
if (event.id === "discord:ready") discordAvailable = true;
});
You don't have to wait for it before sending: messages sent before BedrockRelay connects are queued (up to 400) and delivered once it does.
What happens to your message
- It's posted in every channel connected to that Minecraft server that has the Other addons event turned on.
- Minecraft colour codes are removed from
message,@everyone,@hereand role mentions are defused, and long text is cut at 1,900 characters. - Malformed JSON is ignored, with a warning in the content log.
Trying it
From the Minecraft server's console (no slash), or in game as an operator:
scriptevent discord:message {"message":"Hello from the console"}
Stability
The plugin definition, the run() contract, the helpers in relay/api.js and the three script events are a published contract. We won't change them in a way that breaks an existing plugin or addon without announcing it on this page first.
Running a Minecraft server rather than building for one? The help page covers everything from setup to troubleshooting.
Read the help