---
title: "effects"
description: "The effects namespace — the engine's Luau API reference for effects."
section: "API Reference"
slug: "api-effects"
canonical: "https://origozero.ai/docs/api-effects"
updated: "2026-09-05T23:13:46.356457226+00:00"
tags: ["api", "reference"]
---

# effects

The `effects` namespace — 31 functions.

## globals/effects/backends {#globals-effects-backends}

```lua
effects.backends() -> { string }
```

The backend kinds an effect can be built out of, in name order. The
runtime ships `emitter`, `geometry`, `material`, `decal` and `feature`.

**Returns** `{ string }` — Array of kind names.

```lua
print(table.concat(effects.backends(), ", "))
```

## globals/effects/describe {#globals-effects-describe}

```lua
effects.describe(identity: string) -> { [string]: any }
```

What an effect declares about itself: its family, a one-line summary,
every parameter with its type, default and documented range, and the cost
one unpooled play of it was measured to draw. The one call to make against
an unfamiliar effect before playing it.

**Parameters**

- `identity` `string` — The effect's canonical identity, or a short name.

**Returns** `{ [string]: any }` — `{ identity, family, summary, cost, params }`.

```lua
local d = effects.describe("explosion"); print(d.family, d.cost.gpuMs)
```

## globals/effects/drain {#globals-effects-drain}

```lua
effects.drain() -> { [string]: number }
```

Free every backend the pool is holding idle. The pool keeps what it has
leased for as long as the engine runs — that is what makes repeated firing
cost nothing after the first — and this is the one call that gives it back.
A backend a live play still holds is left to that play's own end.

**Returns** `{ [string]: number }` — `{ freed, kept }`.

```lua
print(effects.drain().freed)
```

## globals/effects/families {#globals-effects-families}

```lua
effects.families() -> { string }
```

Every family the effects in this world declare, sorted — the values
`list { family = … }` filters on. An effect declaring no family is not one
of them.

**Returns** `{ string }` — Array of family names.

```lua
for _, f in ipairs(effects.families()) do print(f, #effects.list({ family = f })) end
```

## globals/effects/list {#globals-effects-list}

```lua
effects.list(opts: table?) -> { string }
```

The canonical identity of every effect this world can play, sorted.
These are the exact strings `play` takes. Pass `{ family = "combat" }` to
get only the effects of one family — the catalogue filtered the way an
effect declares itself.

**Parameters**

- `opts` `table` _(optional)_ — `{ family? = string }`. A family is matched without regard to case.

**Returns** `{ string }` — Array of identities.

```lua
for _, id in ipairs(effects.list()) do print(id) end
for _, id in ipairs(effects.list({ family = "combat" })) do print(id) end
```

## globals/effects/observe {#globals-effects-observe}

```lua
effects.observe() -> { [string]: any }
```

What the runtime is holding and driving right now — every live play with
the reason it is silent when it is, plus what the pool has leased out and
what it is keeping idle, in instances and in GPU bytes. This is how a caller
and a test tell a working effect from a silent one, and how they tell a pool
warming to a wider burst from something leaking: the pool is sized by the
most effects it has had to cover at once, which `peakLive` and `peakLeased`
report beside the current totals.

**Returns** `{ [string]: any }` — The observation.

```lua
local o = effects.observe(); print(o.live, o.leased, o.pooled, o.bytes)
print(o.peakLive, o.peakLeased)   -- the widest burst the pool covers
```

## globals/effects/play {#globals-effects-play}

```lua
effects.play(identity: string, opts: table?) -> any
```

Play an effect once at a world position. The effect allocates what it
needs from the shared pool, draws itself, and gives everything back when it
ends — with no update loop on the caller's side.

**Parameters**

- `identity` `string` — The effect's canonical identity, or a short name that reaches
exactly one effect.
- `opts` `table` _(optional)_ — `{ position? = { x, y, z }, rotation? = quat, direction? = { x, y, z },
params? = { … }, duration? = number, held? = boolean }`. Anything `params`
omits takes the effect's declared default, and an effect that declares a
`duration` parameter reads its length from there rather than from `duration`
here.

**Returns** `any` — The play handle — `stop`, `cancel`, `retarget`, `setParam`, `isPlaying`, `isFinished`, `seek`, `stats`, `whySilent`.

```lua
local h = effects.play("@builtin::systems.effects.combat.explosion", {
position = { 0, 2, 0 }, params = { scale = 4, coreColor = { 1, 0.4, 0.1 } },
})
```

## globals/effects/playOn {#globals-effects-playon}

```lua
effects.playOn(identity: string, target: any?, opts: table?) -> any
```

Play an effect on an entity: it starts where the entity stands and ends
if the entity leaves the world. Move it with the entity by calling
`handle:retarget(theEntity)` as it goes.

**Parameters**

- `identity` `string` — The effect's canonical identity, or a short name.
- `target` `any` _(optional)_ — An entity proxy or entity id.
- `opts` `table` _(optional)_ — The same options `play` takes; `position` is read from the entity.

**Returns** `any` — The play handle.

```lua
local h = effects.playOn("explosion", drum, { params = { scale = 3 } })
```

## globals/effects/registerBackend {#globals-effects-registerbackend}

```lua
effects.registerBackend(kind: string, backend: table)
```

Register a new way of drawing under a kind name, so an effect family
that needs one the runtime does not ship adds it rather than widening the
runtime. Every effect reaches it through `ctx.lease(kind, spec)`.

**Parameters**

- `kind` `string` — The kind name a spec asks for.
- `backend` `table` — The backend — `key`, `acquire`, `seat`, `start`, `stop`, `quiet`,
`place`, `bytes`, `active`, `silence` and `free`.

```lua
effects.registerBackend("ribbonTrail", myBackend)
```

## globals/effects/silenceReasons {#globals-effects-silencereasons}

```lua
effects.silenceReasons() -> { { reason: string, means: string } }
```

The closed set of reasons a play can be producing nothing, in the order
a reading resolves them — nearest cause first — each with what it means.
Every `reason` an observation reports is one of these.

**Returns** `{ { reason: string, means: string } }` — Array of `{ reason, means }`.

```lua
for _, r in ipairs(effects.silenceReasons()) do print(r.reason, r.means) end
```

## modules/effects/README {#modules-effects-readme}

```lua
require("@builtin/modules/api/engine/effects") -- effects (also available as global 'effects')
```

Fire a finished visual effect from gameplay code in one line. `play` puts an effect at a position and `playOn` sticks it to an entity; both return a handle that stops it early, moves it, or re-tunes a parameter while it runs. The effect owns its own lifetime — nothing here needs the caller to tick it — and repeated firing re-uses what the last one left rather than allocating again.

Usage: local effects = require("@builtin/modules/api/engine/effects")
Also available as global: effects

## modules/effects/backends {#modules-effects-backends}

```lua
backends(): { string }
```

The backend kinds an effect can be built out of, in name order. The
runtime ships `emitter`, `geometry`, `material`, `decal` and `feature`.

```lua
print(table.concat(effects.backends(), ", "))
```

## modules/effects/describe {#modules-effects-describe}

```lua
describe(identity: string): { [string]: any }
```

What an effect declares about itself: its family, a one-line summary,
every parameter with its type, default and documented range, and the cost
one unpooled play of it was measured to draw. The one call to make against
an unfamiliar effect before playing it.

**Parameters**

- `identity` `string` — The effect's canonical identity, or a short name.

```lua
local d = effects.describe("explosion"); print(d.family, d.cost.gpuMs)
```

## modules/effects/drain {#modules-effects-drain}

```lua
drain(): { [string]: number }
```

Free every backend the pool is holding idle. The pool keeps what it has
leased for as long as the engine runs — that is what makes repeated firing
cost nothing after the first — and this is the one call that gives it back.
A backend a live play still holds is left to that play's own end.

```lua
print(effects.drain().freed)
```

## modules/effects/families {#modules-effects-families}

```lua
families(): { string }
```

Every family the effects in this world declare, sorted — the values
`list { family = … }` filters on. An effect declaring no family is not one
of them.

```lua
for _, f in ipairs(effects.families()) do print(f, #effects.list({ family = f })) end
```

## modules/effects/list {#modules-effects-list}

```lua
list(opts: table?): { string }
```

The canonical identity of every effect this world can play, sorted.
These are the exact strings `play` takes. Pass `{ family = "combat" }` to
get only the effects of one family — the catalogue filtered the way an
effect declares itself.

**Parameters**

- `opts` `table?` _(optional)_ — `{ family? = string }`. A family is matched without regard to case.

```lua
for _, id in ipairs(effects.list()) do print(id) end
for _, id in ipairs(effects.list({ family = "combat" })) do print(id) end
```

## modules/effects/observe {#modules-effects-observe}

```lua
observe(): { [string]: any }
```

What the runtime is holding and driving right now — every live play with
the reason it is silent when it is, plus what the pool has leased out and
what it is keeping idle, in instances and in GPU bytes. This is how a caller
and a test tell a working effect from a silent one, and how they tell a pool
warming to a wider burst from something leaking: the pool is sized by the
most effects it has had to cover at once, which `peakLive` and `peakLeased`
report beside the current totals.

```lua
local o = effects.observe(); print(o.live, o.leased, o.pooled, o.bytes)
print(o.peakLive, o.peakLeased)   -- the widest burst the pool covers
```

## modules/effects/play {#modules-effects-play}

```lua
play(identity: string, opts: table?): any
```

Play an effect once at a world position. The effect allocates what it
needs from the shared pool, draws itself, and gives everything back when it
ends — with no update loop on the caller's side.

**Parameters**

- `identity` `string` — The effect's canonical identity, or a short name that reaches
exactly one effect.
- `opts` `table?` _(optional)_ — `{ position? = { x, y, z }, rotation? = quat, direction? = { x, y, z },
params? = { … }, duration? = number, held? = boolean }`. Anything `params`
omits takes the effect's declared default, and an effect that declares a
`duration` parameter reads its length from there rather than from `duration`
here.

```lua
local h = effects.play("@builtin::systems.effects.combat.explosion", {
position = { 0, 2, 0 }, params = { scale = 4, coreColor = { 1, 0.4, 0.1 } },
})
```

## modules/effects/playOn {#modules-effects-playon}

```lua
playOn(identity: string, target: any, opts: table?): any
```

Play an effect on an entity: it starts where the entity stands and ends
if the entity leaves the world. Move it with the entity by calling
`handle:retarget(theEntity)` as it goes.

**Parameters**

- `identity` `string` — The effect's canonical identity, or a short name.
- `target` `any` _(optional)_ — An entity proxy or entity id.
- `opts` `table?` _(optional)_ — The same options `play` takes; `position` is read from the entity.

```lua
local h = effects.playOn("explosion", drum, { params = { scale = 3 } })
```

## modules/effects/registerBackend {#modules-effects-registerbackend}

```lua
registerBackend(kind: string, backend: table)
```

Register a new way of drawing under a kind name, so an effect family
that needs one the runtime does not ship adds it rather than widening the
runtime. Every effect reaches it through `ctx.lease(kind, spec)`.

**Parameters**

- `kind` `string` — The kind name a spec asks for.
- `backend` `table` — The backend — `key`, `acquire`, `seat`, `start`, `stop`, `quiet`,
`place`, `bytes`, `active`, `silence` and `free`.

```lua
effects.registerBackend("ribbonTrail", myBackend)
```

## modules/effects/silenceReasons {#modules-effects-silencereasons}

```lua
silenceReasons(): { { reason: string, means: string } }
```

The closed set of reasons a play can be producing nothing, in the order
a reading resolves them — nearest cause first — each with what it means.
Every `reason` an observation reports is one of these.

```lua
for _, r in ipairs(effects.silenceReasons()) do print(r.reason, r.means) end
```

## typed/builtin//modules/api/engine/effects/effects/backends {#typed-builtin-modules-api-engine-effects-effects-backends}

```lua
effects.backends() -> { string }
```

The backend kinds an effect can be built out of, in name order. The
runtime ships `emitter`, `geometry`, `material`, `decal` and `feature`.

**Returns** `{ string }` — Array of kind names.

```lua
print(table.concat(effects.backends(), ", "))
```

## typed/builtin//modules/api/engine/effects/effects/describe {#typed-builtin-modules-api-engine-effects-effects-describe}

```lua
effects.describe(identity: string) -> { [string]: any }
```

What an effect declares about itself: its family, a one-line summary,
every parameter with its type, default and documented range, and the cost
one unpooled play of it was measured to draw. The one call to make against
an unfamiliar effect before playing it.

**Parameters**

- `identity` `string` — The effect's canonical identity, or a short name.

**Returns** `{ [string]: any }` — `{ identity, family, summary, cost, params }`.

```lua
local d = effects.describe("explosion"); print(d.family, d.cost.gpuMs)
```

## typed/builtin//modules/api/engine/effects/effects/drain {#typed-builtin-modules-api-engine-effects-effects-drain}

```lua
effects.drain() -> { [string]: number }
```

Free every backend the pool is holding idle. The pool keeps what it has
leased for as long as the engine runs — that is what makes repeated firing
cost nothing after the first — and this is the one call that gives it back.
A backend a live play still holds is left to that play's own end.

**Returns** `{ [string]: number }` — `{ freed, kept }`.

```lua
print(effects.drain().freed)
```

## typed/builtin//modules/api/engine/effects/effects/families {#typed-builtin-modules-api-engine-effects-effects-families}

```lua
effects.families() -> { string }
```

Every family the effects in this world declare, sorted — the values
`list { family = … }` filters on. An effect declaring no family is not one
of them.

**Returns** `{ string }` — Array of family names.

```lua
for _, f in ipairs(effects.families()) do print(f, #effects.list({ family = f })) end
```

## typed/builtin//modules/api/engine/effects/effects/list {#typed-builtin-modules-api-engine-effects-effects-list}

```lua
effects.list(opts: table?) -> { string }
```

The canonical identity of every effect this world can play, sorted.
These are the exact strings `play` takes. Pass `{ family = "combat" }` to
get only the effects of one family — the catalogue filtered the way an
effect declares itself.

**Parameters**

- `opts` `table` _(optional)_ — `{ family? = string }`. A family is matched without regard to case.

**Returns** `{ string }` — Array of identities.

```lua
for _, id in ipairs(effects.list()) do print(id) end
for _, id in ipairs(effects.list({ family = "combat" })) do print(id) end
```

## typed/builtin//modules/api/engine/effects/effects/observe {#typed-builtin-modules-api-engine-effects-effects-observe}

```lua
effects.observe() -> { [string]: any }
```

What the runtime is holding and driving right now — every live play with
the reason it is silent when it is, plus what the pool has leased out and
what it is keeping idle, in instances and in GPU bytes. This is how a caller
and a test tell a working effect from a silent one, and how they tell a pool
warming to a wider burst from something leaking: the pool is sized by the
most effects it has had to cover at once, which `peakLive` and `peakLeased`
report beside the current totals.

**Returns** `{ [string]: any }` — The observation.

```lua
local o = effects.observe(); print(o.live, o.leased, o.pooled, o.bytes)
print(o.peakLive, o.peakLeased)   -- the widest burst the pool covers
```

## typed/builtin//modules/api/engine/effects/effects/play {#typed-builtin-modules-api-engine-effects-effects-play}

```lua
effects.play(identity: string, opts: table?) -> any
```

Play an effect once at a world position. The effect allocates what it
needs from the shared pool, draws itself, and gives everything back when it
ends — with no update loop on the caller's side.

## typed/builtin//modules/api/engine/effects/effects/playOn {#typed-builtin-modules-api-engine-effects-effects-playon}

```lua
effects.playOn(identity: string, target: any?, opts: table?) -> any
```

Play an effect on an entity: it starts where the entity stands and ends
if the entity leaves the world. Move it with the entity by calling
`handle:retarget(theEntity)` as it goes.

**Parameters**

- `identity` `string` — The effect's canonical identity, or a short name.
- `target` `any` _(optional)_ — An entity proxy or entity id.
- `opts` `table` _(optional)_ — The same options `play` takes; `position` is read from the entity.

**Returns** `any` — The play handle.

```lua
local h = effects.playOn("explosion", drum, { params = { scale = 3 } })
```

## typed/builtin//modules/api/engine/effects/effects/registerBackend {#typed-builtin-modules-api-engine-effects-effects-registerbackend}

```lua
effects.registerBackend(kind: string, backend: table)
```

Register a new way of drawing under a kind name, so an effect family
that needs one the runtime does not ship adds it rather than widening the
runtime. Every effect reaches it through `ctx.lease(kind, spec)`.

**Parameters**

- `kind` `string` — The kind name a spec asks for.
- `backend` `table` — The backend — `key`, `acquire`, `seat`, `start`, `stop`, `quiet`,
`place`, `bytes`, `active`, `silence` and `free`.

```lua
effects.registerBackend("ribbonTrail", myBackend)
```

## typed/builtin//modules/api/engine/effects/effects/silenceReasons {#typed-builtin-modules-api-engine-effects-effects-silencereasons}

```lua
effects.silenceReasons() -> { { reason: string, means: string } }
```

The closed set of reasons a play can be producing nothing, in the order
a reading resolves them — nearest cause first — each with what it means.
Every `reason` an observation reports is one of these.

**Returns** `{ { reason: string, means: string } }` — Array of `{ reason, means }`.

```lua
for _, r in ipairs(effects.silenceReasons()) do print(r.reason, r.means) end
```
