BirdLibraryApi
● Paper Plugin · GraalJS / LuaJ / GraalPy Scripting Engine

Write real Minecraft plugins in JavaScript, Lua, or Python — not a pseudo-language

BirdLibraryApi is a Paper plugin with GraalJS, LuaJ, and GraalPy embedded inside it. Just drop a .js, .lua, or .py file into the plugins/BirdApi/ folder and you can write events, commands, schedulers, and call the Bukkit API directly through the Bird object — like Skript, but without Skript's ceiling.

Engines: GraalJS · LuaJ · GraalPy
Supports: Paper 1.20.4+
Requires: JDK 17+ / Maven
Per-file reload: no server restart needed
01

Installation

From source code to your first working script

  1. Compile the plugin You need Maven and JDK 17 or later. Then run the build in the project folder.
    terminal
    cd BirdLibraryApi
    mvn clean package
    You'll get a single target/BirdLibraryApi.jar file, roughly 60–80MB, since GraalJS and GraalPy are both fully embedded (LuaJ adds under 1MB) — no extra libraries needed on the server.
  2. Drop in the jar and run it once Copy BirdLibraryApi.jar into plugins/ and start the server once. The plugin will automatically create the plugins/BirdApi/ folder.
    Check pom.xml before running pom.xml pins paper-api to version 1.20.4-R0.1-SNAPSHOT — if your server runs a different version, update the version number in pom.xml to match before building.
  3. Add your first script Drop BirdApi-examples/example.js, example.lua, or example.py (or your own file) into plugins/BirdApi/ — see section 12 for the Python/Lua-specific syntax.
  4. Load the script Run /birdlib reload (requires the birdlib.reload permission, granted to op by default), or just restart the server.
    Example chat output

Reloading a single file

You don't have to reload everything every time — you can reload a single file. Other running files aren't affected; their events/commands/tasks keep working.

in-game chat
/birdlib reload            → reload every script (.js, .lua and .py)
/birdlib reload example    → reload example.lua if it exists, else example.py, else example.js
/birdlib reload example.js → same thing, with filename tab-completion

The old /birdapi command still works — it's an alias of /birdlib

02

Core Concepts

Understand these before writing real scripts

Variables available in every script

VariableWhat it is
BirdThe main object — events, commands, scheduler, players, persistent storage, everything lives here
BukkitThe org.bukkit.Bukkit class — call static methods directly
serverAn instance of org.bukkit.Server
pluginAn instance of the BirdLibraryApi plugin itself
Java.type("...")A GraalJS feature — import any Java class on the server's classpath (.js only)
java.type("...")The GraalPy equivalent, used via import java (.py only)

One file = one isolated scope

Each .js file runs in its own GraalJS context — var x in one file will never collide with var x in another file. Each .lua file likewise gets its own LuaJ Globals, and each .py file gets its own GraalPy context. You're free to split things into as many files as you like, in any mix of the three languages.

Why not just keep using Skript

Skript is great for small, quick edits without needing to know how to program. But as a project grows, you hit a real ceiling. This table lays out the comparison clearly:

SkriptBirdLibraryApi
LanguageAn English-like DSL with no real function/closure/class supportFull ECMAScript — functions, closures, classes, Array/Math/JSON
Bukkit API accessOnly what core + addons expose; new features wait on addonsJava.type(...) calls any class instantly, no addon needed
Cross-file communicationEssentially one namespace; splitting files gets awkwardBird.on/emit fires events across files directly
Persistent dataThe variable system works, but it's an opaque binary format, hard to edit externallysetData/getData writes plain .properties files you can read by eye
Reload safetyReloading a large script set is often all-or-nothingEach file loads/unloads independently; listeners/commands/tasks are tracked and auto-cleared
DebuggingErrors are in Skript's own language; addon stack traces are often unreadableReal JS/Java stack traces; use console.log/Bird.log anywhere
Internet accessNeeds a separate HTTP addon, most of which are unmaintainedBuilt-in Bird.fetch(...) plus Bird.sendDiscordWebhook(...)
The trade-offSkript needs no programming knowledge at all, while BirdLibraryApi assumes you already know JavaScript, Lua, or Python — if you do, this removes Skript's ceiling entirely. See section 12 for the Python/Lua specifics.
03

Events

Bird.onEvent(eventClassName, [priority,] callback)

Register a Bukkit event listener using its fully-qualified class name — no need to edit plugin.yml at all.

welcome.js
Bird.onEvent("org.bukkit.event.player.PlayerJoinEvent", function (event) {
    var player = event.getPlayer();
    event.setJoinMessage("&a[+] " + player.getName() + " joined");
    player.sendMessage("&bWelcome! (powered by BirdLibraryApi)");
});

// Priority can be set (LOWEST, LOW, NORMAL, HIGH, HIGHEST, MONITOR)
Bird.onEvent("org.bukkit.event.block.BlockBreakEvent", "HIGH", function (event) {
    var Material = Java.type("org.bukkit.Material");
    if (event.getBlock().getType() === Material.BEDROCK) {
        event.setCancelled(true);
    }
});
Chat the player sees when joining
If an error occurs while a handler runse.g. a NullPointerException in your own code, the system catches it and logs it to console along with the file/event that failed, without affecting other handlers or crashing the server
04

Commands

Bird.onCommand(name, [permission,] callback, [tabComplete])

Register commands dynamically — no need to declare them in plugin.yml at all. Permission and tab-completion are both optional.

goto.js
Bird.onCommand("goto", "example.goto", function (sender, label, args) {
    if (args.length < 1) {
        sender.sendMessage("&cUsage: /goto <player>");
        return;
    }
    var target = Bird.getPlayer(args[0]);
    if (target === null) {
        sender.sendMessage("&cPlayer not found: " + args[0]);
        return;
    }
    sender.teleport(target.getLocation());
}, function (sender, alias, args) {
    // Suggest online player names for the first argument
    if (args.length === 1) {
        return java.util.Arrays.asList(Bird.getPlayerNames());
    }
    return java.util.Collections.emptyList();
});

All available overload forms:

Call formResult
onCommand(name, callback)No permission, no tab-complete
onCommand(name, permission, callback)Requires a permission to use
onCommand(name, callback, tabComplete)Has tab-complete, no permission required
onCommand(name, permission, callback, tabComplete)Both features included
05

Scheduler

Wraps the Bukkit scheduler for you — tasks are automatically cancelled when the script is unloaded/reloaded

countdown.js
Bird.runTask(function () { /* runs next tick, on the main thread */ });
Bird.runTaskLater(function () { /* runs once, after N ticks */ }, 40);

var id = Bird.runTaskTimer(function () { /* runs repeatedly every N ticks */ }, 0, 20);
Bird.cancelTask(id);

// Off the main thread — never touch the Bukkit API (player/block) in here
Bird.runTaskAsync(function () { /* heavy work, HTTP calls, file I/O */ });

20 ticks = 1 second. Every time you call Bird.runTask*, the system tracks it automatically; when the script is reloaded, the task is cancelled automatically — no leftover tasks to worry about.

Synchronous waiting with sleep()

Usable only inside Bird.runTaskAsync(...), because sleeping on the main thread would freeze the whole server. (Named sleep/waitTicks instead of wait because Java's Object already has a wait() method for thread locks — that would definitely cause confusion.)

sequence.js
Bird.onCommand("sequence", function (sender, label, args) {
    Bird.runTaskAsync(function () {
        Bird.tell(sender, "&e3...");
        Bird.sleep(1);
        Bird.tell(sender, "&e2...");
        Bird.sleep(1);
        Bird.tell(sender, "&e1...");
        Bird.sleep(1);
        Bird.runTask(function () {
            // Back on the main thread — safe to touch world/player
            Bird.tell(sender, "&aGO!");
        });
    });
});
06

Players & Messages

Chat, action bar, title, player status

Sending messages: tell (private) vs broadcast (everyone)

chat.js
Bird.tell(player, "&7This message is just for you");
Bird.tell("Notch", "&7Hey Notch!");           // true if online, false if not found

Bird.broadcastChat("&aEvent starting now!");
Bird.broadcastChat("&7[Staff] ...", "example.staff"); // only sent to those with this permission

Action bar & Title

The action bar is a small message above the hotbar; the title is large text in the center of the screen. Timing values are in ticks (20 ticks = 1 second) — leave them out to use Minecraft's defaults (fade-in 0.5s / stay 3.5s / fade-out 1s).

effects.js
Bird.sendActionBar(player, "&c⚠ Low health!");
Bird.broadcastTitle("&6&lBOSS DEFEATED", "&7Well done, everyone!");
Bird.broadcastTitle("&e&lWAVE 3", "", 5, 40, 10); // custom fadeIn/stay/fadeOut
Example of actual rendered colors (color codes are converted automatically)

Commonly used player utilities

MethodWhat it does
Bird.getHealth / setHealthRead/set health — setHealth clamps automatically so it never exceeds max health
Bird.getFood / setFoodHunger level, 0–20
Bird.addPotionEffect(p, name, sec, amp)Apply a potion effect like "SPEED" for sec seconds at level amp (0 = level I)
Bird.getNearbyPlayers(p, radius)Online players in the same world within radius blocks
Bird.getPing / getPlayerUUIDLatency (ms) and the UUID, which stays stable even if the name changes
Bird.setGameMode / getGameModeSet/read the game mode; accepts a string name like "CREATIVE"
07

Items, Sound & Effects

Give items, play sounds, spawn particles, strike lightning

kit.js
Bird.giveItem(player, "DIAMOND_SWORD", 1, "&b&lFrost Blade",
    ["&7A blade forged in ice", "&7+5 Attack Damage"]);

Bird.playSound(player, "ENTITY_PLAYER_LEVELUP");
Bird.spawnParticle(player, "HAPPY_VILLAGER", 15);

Bird.strikeLightning("world", 100, 64, -230, true); // true = effect only, no damage

Block detection

blocks.js
var looking = Bird.getTargetBlock(player, 10);   // the block being looked at, up to 10 blocks away
var underfoot = Bird.getBlockPlayerIsOn(player); // the block under the player's feet

if (underfoot.getType().name() === "LAVA") {
    Bird.tell(player, "&cYou're standing in lava!");
}

Cooldowns

Good for limiting how often a player can use a command/skill — cooldowns can be shared across scripts via the same key.

cooldown.js
if (Bird.hasCooldown("kit", sender)) {
    var left = Bird.getCooldownRemaining("kit", sender);
    Bird.tell(sender, "&cWait another " + Math.ceil(left) + "s");
} else {
    Bird.setCooldown("kit", sender, 300); // 5 minutes
    // ... give the item ...
}
08

Persistent Storage

Two systems: small key/value pairs and full files

Key/Value: setData / getData

Good for small bits of data like counters, toggles, timestamps — stored as a plugins/BirdApi/data/<scriptname>.properties file, loaded back automatically every time the script starts. Values are always stored as strings, so convert numbers yourself with parseInt/parseFloat.

join-counter.js
var key = "joins_" + player.getUniqueId();
var count = parseInt(Bird.getData(key, "0")) + 1;
Bird.setData(key, String(count));

Bird.getData("missingKey");             // → null
Bird.getData("missingKey", "default");  // → "default"
Bird.removeData(key);
Bird.getDataKeys();                     // → list of all stored keys

Full files: saveFile / readFile

Every script gets its own private folder at plugins/BirdApi/files/<scriptname>/ for logs, exports, small JSON databases, and so on.

stats.js
var raw = Bird.readFile("playerstats.json");
var stats = raw ? JSON.parse(raw) : {};

stats[sender.getName()] = (stats[sender.getName()] || 0) + 1;
Bird.saveFile("playerstats.json", JSON.stringify(stats, null, 2));
Security restrictions that can't be bypassed from JS Sandboxed: you can only read/write inside your own folder — no "../" or absolute paths
File extension blacklist: exe bat cmd dll so autorun ps1 ps2 psm1 php sh bash vbs vbe wsf wsh jse jar msi scr are always blocked, since these can execute code on their own
09

Cross-script Events

Bird.on(name, cb) / Bird.emit(name, data)

Let one script fire a custom-named event, and have other loaded scripts subscribe and listen for it — very useful when splitting a large project into multiple files that don't need to know about each other directly.

economy.js + logging.js
// ---- economy.js ----
Bird.emit("coins-added", { player: player.getName(), amount: 50 });

// ---- logging.js (a completely separate file) ----
Bird.on("coins-added", function (data) {
    Bird.log(data.player + " received " + data.amount + " coins");
});
Cleaned up automatically on reloadListeners bound with Bird.on are removed automatically when that script is unloaded/reloaded — no need to write your own cleanup code
10

Calling External APIs

Bird.fetch(...) and Discord webhooks

Requests automatically run off the main thread, but the callback is always invoked back on the main thread — you can safely touch players/blocks inside the callback without managing threads yourself.

checkip.js
Bird.fetch("https://api.example.com/status", function (res) {
    if (res.ok) {
        Bird.log("Status: " + res.status + " body: " + res.body);
    } else {
        Bird.warn("Request failed: " + res.status);
    }
});

// POST with a JSON body
Bird.fetch("https://api.example.com/events", "POST",
    JSON.stringify({ type: "join" }),
    function (res) { Bird.log(res.body); });

Discord webhook

discord.js
Bird.sendDiscordWebhook(webhookUrl, "A player just found a diamond! 💎");

// rich embed: title, description, hex color
Bird.sendDiscordEmbed(webhookUrl, "Player joined",
    playerName + " joined the server", "#57F287");
11

Error Handling

One broken script doesn't bring down the whole server

  • The BirdLibraryApi plugin itself will never be disabled — every code path that loads a script is wrapped in try-catch. A broken file is simply skipped, while other successfully loaded files keep working normally.
  • An error that occurs during load (a syntax error) is reported straight to chat immediately when you run /birdlib reload — no need to dig through the console log.
  • An error that occurs while a handler runs (e.g. an NPE in your own code) is logged to console tagged with the file/event that failed, without affecting other handlers or crashing the server.
Real example from broken-example.js (missing comma before the callback)
broken-example.js
Bird.log("Loading broken-example.js...")

Bird.onCommand("broken" function (sender, label, args) {
    sender.sendMessage("This line will never be reached");
});

Notice the missing , between "broken" and function — this file ships with the project (BirdApi-examples/broken-example.js) specifically to test the error-reporting system.

12

Python & Lua Scripting

The same Bird API, two more languages to write it in

Everything documented on this page works identically from .py and .lua files — same Bird, Bukkit, server, and plugin globals, same events/commands/scheduler/storage. Drop a .py or .lua file into plugins/BirdApi/ right alongside your .js files and it just works — the engine is picked automatically per file, based on its extension. Only the calling syntax changes.

Python — powered by GraalPy

.py scripts run on GraalPy, GraalVM's Python 3 runtime. Import any Java class with import java then java.type("...") — the equivalent of Java.type(...) in JavaScript — and call methods on Java objects with plain Python dot-syntax.

example.py
import java

Material = java.type("org.bukkit.Material")

Bird.log("Loading example.py...")

def on_join(event):
    player = event.getPlayer()
    event.setJoinMessage("&a[+] " + player.getName() + " joined")
    player.sendMessage("&bWelcome! (powered by BirdLibraryApi)")

Bird.onEvent("org.bukkit.event.player.PlayerJoinEvent", on_join)

def on_block_break(event):
    if event.getBlock().getType() == Material.BEDROCK:
        event.setCancelled(True)

# Priority can be set too, same as JS (LOWEST, LOW, NORMAL, HIGH, HIGHEST, MONITOR)
Bird.onEvent("org.bukkit.event.block.BlockBreakEvent", "HIGH", on_block_break)

def hello_command(sender, label, args):
    sender.sendMessage("&eHello from Python! You sent " + str(len(args)) + " argument(s)")

Bird.onCommand("hello", hello_command)

A few practical differences from the JavaScript side:

  • There's no Java.type(...) in Python — import java then java.type("org.bukkit.Material") is the equivalent, and you can also write from java.util import ArrayList-style imports for anything under the java package.
  • Event/command/tab-complete callbacks and Bird.runTask* just take a plain Python function or lambda — same as passing a JS function.
  • Python has no ===; use ==. Use .equals(...) when you specifically need Java's notion of equality on host objects (e.g. comparing two Player instances).
  • Each .py file gets its own GraalPy context, isolated from every other script the same way .js/.lua files are.
Heads upGraalPy is the heaviest of the three engines to load (it ships the full Python standard library), so expect .py scripts to load/reload noticeably slower than .js/.lua ones — especially the first script loaded after a server (re)start.

Lua — powered by LuaJ

.lua scripts run on LuaJ. Since Lua doesn't have JS-style dot-call-with-implicit-this, use Lua's colon syntax to call methods on the injected globals.

example.lua
Bird:log("Loading example.lua...")

local Material = luajava.bindClass("org.bukkit.Material")

Bird:onEvent("org.bukkit.event.player.PlayerJoinEvent", function(event)
    local player = event:getPlayer()
    player:sendMessage("&bWelcome to the server!")
end)

Bird:onCommand("hello", function(sender, label, args)
    sender:sendMessage("&eHello from Lua! You sent " .. #args .. " argument(s)")
end)

A few practical differences from the JavaScript side:

  • There's no Java.type(...) in Lua, but LuaJ ships the luajava library automatically — luajava.bindClass("org.bukkit.Material") is the equivalent, giving back a class reference for static access (Material:valueOf("BEDROCK")) or comparisons.
  • Java arrays passed back into Lua (like args in onCommand) stay backed by the real Java array, so they're indexed starting at 0, not Lua's usual 1-based tables. Use #args for the length.
  • Lua has no ===; use ==. String concatenation is .. instead of +.
  • LuaJ is a pure-Java interpreter with no native/JNI dependency — simpler to ship, and much lighter to load than GraalPy, but without a JIT it's slower than GraalJS/GraalPy for CPU-heavy scripts. Fine for typical event/command handlers.

Reloading by file name

All three extensions work with /birdlib reload, including the shorthand form that omits the extension — it picks whichever of the three actually exists on disk (preferring .lua, then .py, then falling back to .js):

terminal
/birdlib reload             → reload every script (.js, .lua and .py)
/birdlib reload example     → reload example.lua if it exists, else example.py, else example.js
/birdlib reload example.py  → reload example.py specifically
12

Full Example

All features combined in one file, from the BirdApi-examples/ folder

This script is a real example shipped with the project, covering everything from basic events to schedulers, cooldowns, storage, and cross-script custom events. Try copying it into plugins/BirdApi/example.js and running /birdlib reload right away.

example.js
Bird.log("Loading example.js...");

// Greet the player on join + permanently count how many times they've joined
Bird.onEvent("org.bukkit.event.player.PlayerJoinEvent", function (event) {
    var player = event.getPlayer();
    event.setJoinMessage("§a[+] " + player.getName() + " joined the server");
    player.sendMessage("§bWelcome to the server! (powered by BirdLibraryApi)");

    var key = "joins_" + player.getUniqueId();
    var count = parseInt(Bird.getData(key, "0")) + 1;
    Bird.setData(key, String(count));
    if (count === 1) {
        Bird.broadcast("§d" + player.getName() + " is joining for the first time, welcome them!");
    }
});

// Prevent breaking Bedrock (example of accessing the Bukkit API directly)
Bird.onEvent("org.bukkit.event.block.BlockBreakEvent", function (event) {
    var Material = Java.type("org.bukkit.Material");
    if (event.getBlock().getType() === Material.BEDROCK) {
        event.setCancelled(true);
        event.getPlayer().sendMessage("§cYou can't break Bedrock!");
    }
});

// /kit - gives items with a cooldown + fires a custom event for other scripts to listen for
Bird.onCommand("kit", function (sender, label, args) {
    if (Bird.hasCooldown("kit", sender)) {
        var left = Bird.getCooldownRemaining("kit", sender);
        Bird.tell(sender, "&cYou can claim another kit in " + Math.ceil(left) + "s");
        return;
    }
    Bird.giveItem(sender, "IRON_SWORD", 1, "&b&lStarter Sword", ["&7Given by /kit"]);
    Bird.giveItem(sender, "BREAD", 8);
    Bird.playSound(sender, "ENTITY_PLAYER_LEVELUP");
    Bird.spawnParticle(sender, "HAPPY_VILLAGER", 15);
    Bird.setCooldown("kit", sender, 300);
    Bird.tell(sender, "&aHere's your starter kit!");
    Bird.emit("kit-claimed", { player: sender.getName() });
});

Bird.on("kit-claimed", function (data) {
    Bird.log(data.player + " claimed the starter kit");
});

// /countdown  - scheduler example
Bird.onCommand("countdown", function (sender, label, args) {
    var seconds = args.length > 0 ? parseInt(args[0]) : 5;
    var remaining = [seconds];
    var taskId = Bird.runTaskTimer(function () {
        if (remaining[0] <= 0) {
            Bird.broadcastTitle("§a§lGO!", "");
            Bird.cancelTask(taskId);
            return;
        }
        Bird.broadcastActionBar("§e" + remaining[0] + "...");
        remaining[0] = remaining[0] - 1;
    }, 0, 20);
});

Bird.log("example.js loaded — /kit and /countdown are ready");
13

Full API Reference

A condensed lookup table for when you're scripting — no need to scroll through the whole page

CategoryMethod
EventonEvent(class, [priority,] cb)
CommandonCommand(name, [perm,] cb, [tabCb])
SchedulerrunTask / runTaskLater / runTaskTimer / runTaskAsync / runTaskTimerAsync / cancelTask
Sleepsleep(sec) / waitTicks(n) — usable only inside runTaskAsync
Chattell / broadcast / broadcastChat
Action barsendActionBar / broadcastActionBar
TitlesendTitle / broadcastTitle
ItemsgiveItem / hasItem / removeItem / getItemInHand / clearInventory
Sound/ParticlesplaySound / broadcastSound / spawnParticle / strikeLightning
Teleportteleport(player, x, y, z, [world])
BlocksgetBlock / getBlockType / isBlockType / setBlockType / getTargetBlock / getBlockPlayerIsOn
CooldownsetCooldown / hasCooldown / getCooldownRemaining / clearCooldown
PlayersgetPlayer / getPlayerExact / getOnlinePlayers / getPlayerNames / getNearbyPlayers
Player statusgetHealth/setHealth · getFood/setFood · addPotionEffect/removePotionEffect/hasPotionEffect · giveExp · getPing · getPlayerUUID
WorldgetWorldNames · getWorldTime/setWorldTime · setWeather/isStorming · isNight
Storage (key/value)setData / getData / removeData / getDataKeys / saveData
Storage (files)saveFile / readFile / fileExists / deleteFile / listFiles
Custom eventon(name, cb) / emit(name, data)
Internetfetch(url, [method,] [body,] [headers,] cb) / sendDiscordWebhook / sendDiscordEmbed
Internal commandsrunCommand(cmd) / runCommandAs(player, cmd)
Miscrandom / colorize / stripColor / formatTime / log / warn