Skip to content

Plugins Quickstart

Load a prebuilt plugin and call it from a query in under five minutes — no plugin authoring required.

This page uses the bundled ai.example.geo plugin, which registers a single scalar function: ai.example.geo.haversine, the great-circle distance (in kilometres) between two latitude/longitude points. You will read the prebuilt .wasm artifact, hand its bytes to load_wasm_component, inspect what was registered, and then call the function from Cypher.

If you want to understand how plugins fit together first, start with Concepts. To build your own, see Authoring.


Prerequisites

  • The default uni-db wheel already bundles wasmtime. No extra runtime to install — the Component Model loader (load_wasm_component) is built in. In the Rust workspace this is the wasm-plugins feature (Extism, via load_wasm_extism, is the separate extism-plugins feature). Both are compiled into the published Python wheel.

  • The prebuilt .wasm fixture must exist on disk. The example plugin is not checked in as a binary; it is compiled on demand. Build it once from the repo root:

    rustup target add wasm32-wasip2          # one-time
    ./scripts/build-wasm-fixtures.sh
    

    This produces the Component Model artifact at:

    examples/example-wasm-geo/target/wasm32-wasip2/release/example_wasm_geo.wasm
    

    The wasm32-wasip2 target emits a Component Model binary directly — no wasm-tools post-processing step is needed. For the full build story (and how to compile your own plugin), see WASM Components and Authoring.

Note

If the fixture is missing, the e2e tests skip rather than fail. Make sure the path above exists before running the snippets below.


Load it

Read the component bytes and pass them to load_wasm_component. The grants argument is a list of capability name strings; the geo plugin only registers a scalar function, so ["ScalarFn"] is sufficient. (Omitting grants defaults to scalar / aggregate / procedure.)

import uni_db

db = uni_db.Uni.open("./geo.db")

wasm_path = (
    "examples/example-wasm-geo/target/wasm32-wasip2/"
    "release/example_wasm_geo.wasm"
)
with open(wasm_path, "rb") as f:
    outcome = db.load_wasm_component(f.read(), grants=["ScalarFn"])

print(outcome["plugin_id"], outcome["version"])
print(outcome["scalars_registered"])
import uni_db

db = await uni_db.AsyncUni.temporary()

wasm_path = (
    "examples/example-wasm-geo/target/wasm32-wasip2/"
    "release/example_wasm_geo.wasm"
)
with open(wasm_path, "rb") as f:
    # Awaited: wasmtime instantiation runs off the event loop.
    outcome = await db.load_wasm_component(f.read(), grants=["ScalarFn"])

print(outcome["plugin_id"], outcome["version"])
print(outcome["scalars_registered"])
use uni_db::Uni;
use uni_plugin::{Capability, CapabilitySet};
use uni_plugin_wasm::WasmLoader;

let db = Uni::open("./geo.db")?;

let bytes = std::fs::read(
    "examples/example-wasm-geo/target/wasm32-wasip2/\
     release/example_wasm_geo.wasm",
)?;

// The Rust path takes an explicit loader and capability set; the
// Python methods wrap exactly this call.
let loader = WasmLoader::new();
let caps = CapabilitySet::from_iter_of([Capability::ScalarFn]);
let host_grants = vec!["ScalarFn".to_owned()];

let outcome = db.load_wasm_component(&loader, &bytes, &host_grants, &caps)?;

println!("{} {}", outcome.plugin_id, outcome.version);
println!("{:?}", outcome.scalars_registered);

The function is registered the moment the call returns — there is no separate "activate" step. Loading is instance-scoped: the scalar is available to every session and query on this Uni handle.


Inspect the LoadOutcome

Both Python methods return a dict describing exactly what the loader did. For the geo plugin you'll see:

{
    "plugin_id": "ai.example.geo",
    "version": "0.1.0",
    "scalars_registered": ["ai.example.geo.haversine"],
    "aggregates_registered": [],
    "procedures_registered": [],
    "effective_capabilities": [],
    "denied_capabilities": [],
}
Key Meaning
plugin_id The plugin's self-declared identity (from its manifest).
version The plugin's declared semantic version.
scalars_registered Fully-qualified names of scalar functions now callable.
aggregates_registered Aggregate function QNames registered.
procedures_registered Procedure QNames registered.
effective_capabilities Granted ∩ declared — what the plugin actually got.
denied_capabilities Declared-but-not-granted capabilities (diagnostics).

effective_capabilities is the intersection of what you granted and what the plugin's manifest declares. The geo plugin declares no capabilities, so its effective set is empty even though you passed grants=["ScalarFn"] — that grant is a surface grant that authorizes registering the scalar, distinct from the host-service capabilities a manifest can declare. If you withhold a capability a plugin does declare, it shows up under denied_capabilities instead — a quick way to spot a misconfigured grant set. In Rust, the same fields are typed members of uni_plugin_wasm::loader::LoadOutcome.

See Reference for the complete field table and the full list of capability names. The grant/deny model is covered in Trust & Capabilities.


Call it from Cypher

A registered scalar is callable by its fully-qualified name anywhere a Cypher expression is allowed:

# Paris (48.8566, 2.3522) -> London (51.5074, -0.1278)
rows = db.session().query(
    "RETURN ai.example.geo.haversine(48.8566, 2.3522, 51.5074, -0.1278) "
    "AS km"
)
print(rows)  # km ~= 343.557
let rows = db.session().query(
    "RETURN ai.example.geo.haversine(48.8566, 2.3522, 51.5074, -0.1278) \
     AS km",
)?;
// km ~= 343.557

The four f64 arguments are lat1, lon1, lat2, lon2; the result is the distance in kilometres (~343.557 km for Paris→London). Like any DataFusion scalar, it is vectorised — pass column expressions over a MATCH and it evaluates row-by-row across the whole batch.

The name ai.example.geo.haversine is a QName (qualified name). At plan time the query engine resolves it against the local plugin registry, so a registered plugin scalar slots in alongside built-in functions with no special syntax. See Concepts for how QName resolution and the registry fit into the wider plugin model.


Next steps

  • Concepts — loaders, registry, QName resolution, and the plugin lifecycle.
  • Loaders — the available loaders (WASM Component Model, Extism, Rhai) and when to use each.
  • Authoring — write and build your own plugin, including the wasm32-wasip2 toolchain setup.
  • Trust & Capabilities — the grant model, the full capability list, and host trust policy.