Developer

PNode Plugin API

Add custom nodes to PNode from your own Bukkit/Paper plugin using the public API jar. The API jar is free to use and requires no source access.

Note: PNode and its source code are not open source. The compiled API jar and plugin jar are freely available for use.

Download API Jar

Select a version and download the API jar to add as a compileOnly dependency.

⬇ Download

1. Add the API Jar

Download pnode-api-<version>.jar and place it in your project's libs/ folder. Add it as a compileOnly dependency — do not shade or shadow it, PNode provides the classes at runtime.

Gradle (Kotlin DSL)

dependencies {
    compileOnly(files("libs/pnode-api-<version>.jar"))
}

Maven

<dependency>
    <groupId>dev.pnode</groupId>
    <artifactId>pnode-api</artifactId>
    <version>1.2.0</version>
    <scope>system</scope>
    <systemPath>${project.basedir}/libs/pnode-api-<version>.jar</systemPath>
</dependency>

Declare PNode as a dependency in your plugin.yml:

# soft-depend if PNode is optional, depend if required
softdepend: [PNode]

2. Obtain the API Instance

Retrieve PNodeAPI from Bukkit's ServicesManager in your onEnable:

import dev.pnode.api.PNodeAPI;
import org.bukkit.plugin.RegisteredServiceProvider;

@Override
public void onEnable() {
    RegisteredServiceProvider<PNodeAPI> rsp =
        getServer().getServicesManager().getRegistration(PNodeAPI.class);

    if (rsp == null) {
        getLogger().warning("PNode not found — custom nodes will not be registered.");
        return;
    }

    PNodeAPI pnode = rsp.getProvider();
    pnode.registerNode(this, new MyCustomNode());
}

You do not need to unregister nodes in onDisable(). PNode automatically removes all nodes from a plugin when that plugin shuts down.

3. Implement a Custom Node

Implement dev.pnode.node.NodeHandler. Define the node's appearance and ports once in a static NodeDefinition, then handle execution in execute().

import dev.pnode.node.*;

public class GreetPlayerNode implements NodeHandler {

    // Define once — type ID must be unique; prefix with your plugin name
    private static final NodeDefinition DEF = new NodeDefinition(
            "myplugin/greet-player",   // type ID
            "Greet Player",            // display name in editor palette
            "My Plugin",               // sidebar category
            "#9b59b6",                 // header colour (hex)
            "Sends a greeting to a player."
    )
    .addInput(NodePort.execIn())
    .addInput(NodePort.input("player", PortType.PLAYER.id, "Player to greet"))
    .addOutput(NodePort.execOut());

    @Override
    public NodeDefinition getDefinition() { return DEF; }

    @Override
    public void execute(NodeInstance node, ExecutionContext ctx, NodeExecutorCallback cb) {
        Object playerObj = cb.resolveInput(node, 1); // slot 1 = "player" input
        if (playerObj instanceof org.bukkit.entity.Player player) {
            player.sendMessage("Hello, " + player.getName() + "!");
        }
        cb.followExec(node); // advance execution to the next connected exec node
    }
}
Type ID convention: use yourplugin/node-name (lowercase, hyphens ok). This avoids collisions with PNode's built-in nodes and other plugins.

4. Port Types

Use PortType constants when defining input/output ports:

ConstantJava TypeNotes
PortType.EXECExecution flow wire (use NodePort.execIn() / execOut())
PortType.STRINGString
PortType.NUMBERDouble
PortType.BOOLEANBoolean
PortType.PLAYERorg.bukkit.entity.Player
PortType.SENDERorg.bukkit.command.CommandSender
PortType.ENTITYorg.bukkit.entity.Entity
PortType.WORLDorg.bukkit.World
PortType.LOCATIONorg.bukkit.Location
PortType.VECTORorg.bukkit.util.Vector
PortType.EVENTorg.bukkit.event.Event
PortType.LISTjava.util.List
PortType.MATERIALorg.bukkit.Material
PortType.ANYanyUse sparingly

5. Node Properties

Properties appear in the editor's right-hand panel when a node is selected. Define them on NodeDefinition and read them inside execute().

// In NodeDefinition builder:
.addProperty("message", "string",  "Hello!",  "Message text")
.addProperty("count",   "number",  1,          "How many times")
.addProperty("loud",    "boolean", false,      "Use ALL CAPS")
// In execute():
String message = node.prop("message", "Hello!");
double count   = node.propDouble("count", 1.0);
boolean loud   = node.propBool("loud", false);

Supported property types: string, number, boolean, textarea, select.

6. Exec Nodes vs Data Nodes

Exec nodes

Have at least one exec input and one exec output. Call cb.followExec(node) at the end to advance the flow. Can also emit data via cb.emitOutput(slot, value).

Data nodes

Have only data ports (no exec ports). Call cb.emitOutput(slot, value) to publish a computed value. Do not call cb.followExec — data nodes are pulled lazily.

7. Trigger Nodes (onEnable / onDisable)

Override onEnable and onDisable if your node needs to register Bukkit listeners or schedule repeating tasks. PNode calls these when the graph containing the node is enabled or reloaded.

@Override
public void onEnable(NodeInstance node, NodeGraph graph) {
    // register a listener, start a task, etc.
}

@Override
public void onDisable(NodeInstance node, NodeGraph graph) {
    // cancel tasks, unregister listeners, free resources
}