Authoring a node plugin
Build your own nodes in Python, a shader, or compiled code — and have them show up in the palette next to the built-ins.
A node plugin is just a folder in ~/.dna/plugins/ with a small manifest and one source file. Once it loads, your node appears in node search, wires up like any other node, and can be saved inside a .dna project. This page walks through the pieces: the kind of node, the entry point, the manifest, the capabilities you ask for, and how to bundle it for other people.
Pick a kind of node
There are three ways to author a node, depending on what you're making.
| Kind | You write | Best for | Live reload |
|---|---|---|---|
| Python | a .py file | prototyping, data crunching, glue logic | yes — edit and it refreshes |
| Shader | a .wgsl file | GPU image effects, looks, filters | yes |
| Native | compiled code (Rust or C) | heavy number-crunching that has to be fast | no — restart to swap |
Reach for Python first. It's the fastest to iterate on and reloads the moment you save. Move to a Native node only when you've measured that you need the speed.
The simplest Python node
A Python node is a small class with an execute method. You declare its name, where it lives in the palette, and what it does.
import nodetool
@nodetool.node(id="my_plugin.invert", display_name="Invert Values", category="Math")
class InvertNode:
input: float = 0.0
output: float = 0.0
def execute(self, input: float) -> dict:
return {"output": -input}
The fields with defaults become the node's inputs and outputs; execute returns a dictionary keyed by output name. Numbers, vectors, colours, and number arrays convert automatically in both directions, so you can pass them straight through.
You can also ship a panel — a little custom control surface — by returning a list of widgets (sliders, buttons, and so on) from a layout() method.
A shader node
A shader node is pure GPU — no per-frame code on your side. You write a fragment (or compute) shader and declare its controls in comments, which DNA turns into real parameter knobs on the node.
// @param amount: float = 0.5 [0.0, 1.0]
// @param color: vec4 = vec4(1.0, 0.0, 0.0, 1.0)
@fragment
fn main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let uv = pos.xy / uniforms.resolution;
let col = textureSample(content, samp, uv);
return mix(col, color, amount);
}
You get up to four named image inputs and a few built-in values for free: uniforms.resolution, uniforms.time (seconds), and uniforms.frame. See Authoring a shader plugin for the full shader story.
The manifest
Every plugin folder needs a plugin.toml describing what it is. You declare exactly one kind of node in it.
[plugin]
name = "My Plugin"
version = "1.0.0"
api_version = "1.0.0"
# Exactly ONE of these sections:
[plugin.python]
entry = "main.py"
# [plugin.native]
# library = "libfast_noise" # the file extension is added for you
# [plugin.shader]
# [[shader]]
# id = "custom.chromatic_aberration"
# file = "chromatic.wgsl"
# language = "WGSL"
# shader_type = "Fragment"
# inputs = ["content"] # up to 4 image inputs
The api_version is the version of DNA your plugin was built against. DNA checks it on load and refuses to run a plugin built for an incompatible version, so a mismatch fails loudly instead of misbehaving.
The Rust authoring kit currently handles the node's identity and execution for you, but you still declare inputs, outputs, and parameters yourself — a friendlier way to declare those is planned.
Capabilities and trust
By default a plugin can compute and nothing else. If your node needs to reach outside its sandbox, you ask for it in the manifest:
[capabilities]
gpu = false # use the GPU directly
state = false # remember values between cooks
filesystem = false # read or write files
network = false # talk over the network
plugin_host = false # load other plugins
Only request what you actually use. When someone installs your plugin they see exactly which capabilities you asked for and decide whether to grant them. In a locked-down project an ungranted capability is simply denied at cook time — your node keeps running, but that one door stays shut.
If your node crashes, DNA isolates the damage: it flags your plugin, skips the rest of that cook for your nodes, and keeps the built-in nodes and other plugins running. Reloading the plugin clears the flag. This means a buggy plugin won't take the whole project down — but do watch the event log while you develop.
See Capabilities & trust for how grants work, and Trust & Permissions for the project-wide trust levels.
Where plugins live, and how reload works
Each plugin is its own folder under ~/.dna/plugins/:
~/.dna/plugins/
├── my-python-plugin/ plugin.toml + main.py
├── my-shader-plugin/ plugin.toml + chromatic.wgsl
└── my-native-plugin/ plugin.toml + libfast_noise.dylib
Drop a folder in, and the Plugin Manager window picks it up, lists it with its status, and gives you enable, disable, and reload toggles plus a shortcut to the folder. Python and shader plugins are watched — save the file and the change takes effect. Native plugins load once and stay put, so swapping one means restarting DNA.
Packaging for other people
When you want to hand your plugin to someone else, package it as a single bundle file (a zip carrying the manifest and the built code, with an optional signature). The installer inspects it, shows the capabilities it requests and whether it's signed, and on accept drops it into ~/.dna/plugins/ for them.
Sign your bundle if you can. A signed bundle from a publisher the user already trusts installs with a plain, neutral prompt; an unsigned one installs with an "unverified publisher" warning. Either way the user stays in control. One bundle targets one platform — ship a separate build per platform (macOS, Windows, Linux).
See Installing plugins for the install flow and plugin.toml reference for every manifest field.