DEVELOPERS

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 toUseWhat you write
Add a Discord command, like /playtime SteveA pluginOne JavaScript file that BedrockRelay loads inside its pack
Post to Discord when something happens in your own addonScript eventsA 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:

  1. Put health.js in development_behavior_packs/BedrockRelay/scripts/plugins/ on a Minecraft server running BedrockRelay (pack v0.3.0 or newer).
  2. Add its name to scripts/plugins/index.js: export default ["health"];
  3. Restart the Minecraft server. The console shows [BedrockRelay] plugin loaded: Health 1.0.0.
  4. On the dashboard, open that Minecraft server's page and switch Health on under Plugins.
  5. In a connected Discord server, run /health.

Plugin reference

The definition

FieldTypeDescription
idstring, requiredLowercase letters, numbers and dashes, up to 32 characters. Must match the file name. Never change it once published.
namestringShown on the dashboard.
versionstringLike 1.2.0. Raise it with every release; the dashboard offers updates by comparing it.
descriptionstringOne or two sentences, shown on the dashboard.
privacystringShown beside the on/off switch. Required in the catalog if the plugin reveals anything about players.
postsstringSay 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.
postKindsarrayIf 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.
commandsarrayThe Discord commands, below. Up to 10.

A command

FieldTypeDescription
namestring, requiredBecomes /name in Discord. Lowercase, up to 32 characters. relay is reserved.
descriptionstringShown in Discord's command list, up to 100 characters.
optionsarrayUp to 10, below. Required options come first.
publicbooleantrue 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.
confirmstring or trueAsk "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, requiredCalled when someone uses the command. May be async.

Options

typeDiscord showsrun() receives
playerText, suggesting players online now first, then everyone who has played recentlystring
stringTextstring
integerA whole numbernumber
numberA numbernumber
booleanTrue / falseboolean

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. embed is a Discord embed: title, description, color (a number), author, fields, footer, thumbnail, image and url are supported. For a player's head, use thumbnail: { player: "Steve" } or author: { 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.

FieldDescription
requestedByThe Discord display name of whoever ran the command, for your information only.
linkedPlayerTheir 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.

HelperDoes
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

  1. Loading. When the Minecraft server starts, BedrockRelay loads each plugin in index.js on its own. A plugin that throws or is missing is logged and skipped; the others, and the relay, carry on.
  2. 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.
  3. 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.
  4. 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.
  5. Answering. Whatever you return goes back to the gateway, which removes Minecraft colour codes, defuses @everyone, keeps embeds within Discord's limits and accepts only https images. It's shown only to the person who asked, unless the command is public.

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-net or @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=true in server.properties so you can see the pack's messages.
  • On start you'll see plugin loaded: Your Plugin 1.0.0, or plugin "x" was skipped or failed to load with 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 privacy if 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.

EventDirectionPurpose
discord:messageYour addon → DiscordA plain text message.
discord:embedYour addon → DiscordA rich Discord embed.
discord:readyBedrockRelay → your addonTells you BedrockRelay is present and connected.

discord:message

FieldTypeDescription
messagestring, requiredThe text to post. Discord markdown works.
authorstringThe name the post appears under. Defaults to the Minecraft server's name.
picturestring (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, @here and 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