---
title: "procNode — custom procedural nodes as content"
description: "A .procNode is a procedural operation authored as content: a folder whose init.luau returns a def table. When the asset registers, its def joins the proc op registry keyed by the asset's stable GUID,…"
section: "Types"
slug: "types-procnode"
canonical: "https://origozero.ai/docs/types-procnode"
updated: "2026-08-22T07:41:56.166582875+00:00"
tags: ["asset-type", "reference"]
---

# procNode — custom procedural nodes as content

```
MyNode.procNode/
  init.luau     -- returns the def table
  README.md
  .metadata
```

## Two def kinds

### Op-style — a custom operation

`init.luau` returns an op def: typed input sockets, params, typed output
sockets, and an `eval` that produces the outputs.

```lua
return {
    kind = "op",
    description = "Scale a Geometry uniformly.",
    inputs = { geometry = { type = "Geometry" } },
    params = { factor = { type = "Float", default = 2.0 } },
    outputs = { geometry = { type = "Geometry", default = true } },
    -- eval(ctx, inputs, params) -> outputs table.
    -- ctx carries seed / nodeId / compositePath / hash / checkpoint.
    eval = function(ctx, inputs, params)
        return { geometry = inputs.geometry:scaled(params.factor) }
    end,
}
```

The def's `version` is derived from the source text on every registration, so
editing `init.luau` re-keys the node caches of every graph that reaches this
node — downstream cooks refresh automatically.

`init.luau` is compiled from its current source on each registration, so any
module it pulls in uses an absolute require (`require("@builtin::…")`).

### Subgraph-style — a `.procGraph` graph used as a node

`init.luau` returns a def bound to a `.procGraph` graph asset. The graph's declared
inputs become the node's params; its declared outputs become the node's
outputs. Evaluating the node evaluates the graph.

```lua
return {
    kind = "graph",
    -- A `.procGraph` asset ref (resolve it by guid, not by name).
    graph = asset.resolve("<graph-guid>", "procGraph"),
}
```

A `.procGraph` graph can also be used as a node **directly**, without a wrapping
procNode, by passing its asset ref to `Graph.node`:

```lua
local g = proc.graph("parent")
local childRef = asset.resolve("<graph-guid>", "procGraph")
g:node("sub", childRef):param("radius", 3)
```

Either way, referencing a graph as a node persists a typed GUID ref in the
parent's `graph.json`, so the referenced asset travels with the parent as a
dependency (it appears in `asset.deps`). A graph that reaches itself — directly
or transitively — errors loudly at eval rather than recursing without bound.

## Registration is push-model

The registry is populated by this type's lifecycle hooks: `onRegister`
registers instances present at load, `onChange` re-registers + hot-reloads on
edit, `onDelete` unregisters. A graph that references a procNode which is not
registered errors loudly at load/eval, naming the missing GUID — there is no
name-probing fallback.
