---
title: "plugin"
description: "Compiled code in a world, used the way a module is."
section: "Types"
slug: "types-plugin"
canonical: "https://origozero.ai/docs/types-plugin"
updated: "2026-09-30T00:45:53.941076233+00:00"
tags: ["asset-type", "reference"]
---

# plugin

A plugin is a WebAssembly component in a `<name>.plugin/` asset. Code that
uses one `require`s it and calls it like any module: the calls answer values,
and a refusal raises. Nothing about the call says compiled code ran, the way
calling into a native library from Python does not.

```luau
local counter = require("counter")

local total = await(counter.add(2))     -- a number
print(await(counter.total()))           -- 2
```

It is for work Luau cannot do fast enough, or cannot do at all: a simulation
step, a decoder, a solver, a model.

## Getting one into a world

Write the component into the world as a `.wasm` file (upload it). It becomes
a plugin asset of the same name, with the component moved inside, and it
loads. The asset starts with:

```
counter.plugin/
├── plugin.wasm        the component
├── plugin.yaml        what it offers, and where it loads
├── surface.module/    the module a `require` of the asset lands on
├── README.md          this plugin's own description
└── .metadata
```

Nothing is callable until `plugin.yaml` lists it. Edit it to name the methods
the component offers, and the plugin reloads with them.

## plugin.yaml

```yaml
profiles:        # where it loads; required
  - editor
  - runtime
storage: none    # none | private; none when left out
events: private  # private | exposed; private when left out
memory: 512MiB   # the most memory it may hold; 4GiB when left out
exposed:         # the methods a caller may run; none when left out
  - name: add
  - name: total
```

- **profiles** says where the plugin loads. A `runtime` plugin is part of the
  game: components and scene entrypoints can call it, and a player loads it.
  An `editor` plugin is for authoring, and a call to it from a component or an
  entrypoint is refused, because the shipped game would not have it.
- **storage** `private` gives the plugin a store of its own, in this world,
  that survives a restart. With `none` what it stores lasts until it is
  reloaded.
- **events** `exposed` lets code in the world listen to the events the plugin
  raises. With `private` nothing in the world hears them.
- **memory** is the most memory the plugin may hold, written with `KiB`,
  `MiB` or `GiB`, up to `4GiB`, which is also what a plugin that says nothing
  gets. A plugin that would grow past it stops, and the log says it went past
  its `memory:`.
- **exposed** is the plugin's whole callable surface. A method not listed
  cannot be called, whatever the component contains, and a listing that names
  a method the component does not have is refused when the plugin loads.

## Using it

The asset's `surface.module` is its Luau face, and `require("<name>")`, with
the asset's name, lands on it. Its code is `surface.module/init.luau`, and
each of its functions calls the plugin through the asset it belongs to:

```luau
local self = asset.containing(__FILE__)
local me = self.modules.shared.forAsset(self)

local M = {}

function M.add(amount: number)
    return me.call("add", amount)
end

return M
```

- Arguments are numbers, strings, booleans, nil, buffers, handles, and tables
  of them. A whole number arrives as an integer. A string has to be text; send
  bytes as a `buffer`, which arrives whole, as bytes.
- A file or a folder goes as a handle, which the plugin reads or writes by
  itself, without a copy through the call:

  ```luau
  local weights = vfs.open(self.path .. "/weights.bin")            -- read a file
  local model = vfs.openDir(self.path .. "/model")                  -- read a folder
  local out = vfs.sink("/source/out/sphere.zvol", { maxBytes = 64 * 1024 * 1024 })
  local frames = vfs.dirSink("/source/frames", { maxFiles = 240, maxBytes = 200 * 1024 * 1024 })
  me.call("render", model, frames)
  ```

  A handle lets the plugin do what this script could do at that path, and
  nothing more: a plugin has no filesystem of its own. A sink's file appears
  when the plugin closes the sink or lets go of it, and only the world's own
  files take a sink. The `plugin-development` guide has the rules.
- Entities go as a binding: `ecs.bindEntities(ids):forPlugin()`. The plugin
  reads and writes their fields as packed floats and never learns an id; a
  write it makes answers to the same locks, owners and builds this script's
  own `entity.batchWrite` does.
- A GPU buffer goes as `buf:forPlugin()` (a `"gpu"` buffer from
  `substrate.createBuffer`), and a compute shader as `shaderRef:forPlugin()`.
  The plugin writes and reads the buffer's floats and dispatches the shader
  over buffers the same script handed it. It never learns a buffer's name or
  a shader's key, so it reaches no buffer it was not given.
- The answer is the value the plugin returned: a table as a table, a number as
  a number, bytes as a `buffer`.
- A refusal raises where it is awaited: a method not exposed, a plugin that is
  not loaded, or the plugin's own error.
- `me.on(function(eventType, payload) ... end)` listens to the plugin's
  events, waiting for it to load first. The payload is a value, as an answer
  is. It needs `events: exposed`.
- A refusal is an error like any other, and `pcall` catches it.
- `plugins.isLoaded("<name>")` answers whether it is running, and never
  raises.

## More than one part

A plugin can carry more than its surface: modules the surface builds on, and
an editor panel. They sit beside `surface.module` inside the asset:

```
counter.plugin/
├── plugin.wasm
├── plugin.yaml
├── surface.module/    what `require("counter")` lands on
├── history.module/    a module the surface uses
└── panel.editorPanel/ a dock tab
```

The parts reach each other with the dot form, which means the same thing
wherever the asset is installed:

- From a module, `require(".history")` is the module beside it.
- From the panel, `require(".")` is the plugin (its surface), and
  `require(".history")` the module beside the panel.

The surface IS the plugin: it has no name of its own, so no part requires
`.surface`. Don't write a path (`./history.module`) or a name that says where
the asset is installed (`require("counter.history")`): moved into a library,
the asset stops finding its own parts.

## While it runs

- **A new build replaces it in place.** Upload the component again under the
  same name and the running plugin is replaced, with no restart. Uploading the
  same bytes again changes nothing.
- **Editing plugin.yaml reloads it.**
- **Removing the asset stops it.**
- **A new build or an edit stops a call in progress.** A call the plugin is
  in the middle of answers that the plugin was stopped, however long it would
  have run, and the new build starts.
- **A crash stops it once.** If the component traps, the plugin stops, the log
  says so in one line, and every later call raises that it stopped and why,
  until it loads again: a new build, or an edit to `plugin.yaml`.
- **`plugins.status()`** lists each plugin: whether it is loaded, why not, and
  its component: the SHA-256, the size, and what built it.

The same holds in the desktop engine and in a browser.

## Building one

The `plugin-development` guide covers writing a component: the interface in
`zero-plugin.wit` beside this file, a Rust project that builds one, how calls
are encoded, what a plugin may use, and the development loop.
