---
title: "Requiring modules"
description: "require is how one piece of Luau pulls in another. You hand it a string; it hands you back the table that module returned. The interesting part is the string — what you write decides how the engine…"
section: "Core"
slug: "core-requiring-modules"
canonical: "https://origozero.ai/docs/core-requiring-modules"
updated: "2026-08-05T12:30:02.055210954+00:00"
tags: ["documentation", "guide"]
---

# Requiring modules

- by **identity** — a name in the asset index, which the engine resolves to an
  asset and then to that asset's code file;
- by **path** — a literal location in the VFS hierarchy, walked like a directory
  tree.

You never choose between them with a flag. The engine reads the **shape** of the
string and decides. Get the shape right and resolution is unambiguous; get it
wrong and you get a loud "not found" rather than a surprise. This guide is about
reading that shape.

## The mechanism: shape → identity-or-path → asset → primary file

Every `require` runs the same pipeline:

1. **Classify by shape.** Does the string carry a type suffix (`.module`,
   `.tool`, …) and/or a `/`? Then it's a **path**. Is it a suffix-free token,
   plain or dotted (`json`, `modules.transform`)? Then it's an **identity**.
   A leading `.` / `..` / `./` / `../` marks it **relative** — anchored to the
   file doing the requiring — and the same suffix test then says whether that
   relative form is an identity (`.helper`) or a path (`./helper.module`).
2. **Resolve to an asset.** An identity is looked up in the **asset index** —
   the string is a registry key, never parsed into a folder. A path is walked
   **literally in the VFS** from wherever it's anchored. Either way you land on
   one asset.
3. **Resolve the asset's primary file.** Every asset type names a primary code
   file — a `.module`'s primary is its `init.luau`, and so on. `require` loads
   *that* file. This is why you can require a module by its *folder* and never
   name `init.luau` yourself.

The throughline: **identity and path are two front doors to the same back
room.** Both converge on an asset, then on that asset's primary file. The shape
only decides which front door you walk through.

The module runs **once**, and every later `require` of it hands back that same
loaded instance. So a table a module returns is shared state: a field on it that
a component sets is the field an `execute` call reads. That is what makes a
module's returned table the way one chunk observes another's state — the
scripting-and-tasks guide covers the state model.

Saving a module's source re-runs its body and folds what it returns into that
same instance, so every reference held to it stays live. The tables it exports
keep their identity the same way: where the previous source and the new one both
build a table at a key, that table stays and the new source's keys land on it.
So a handler another chunk registered into an exported bus is still registered
after the save, and an edit to a constant table the module exports is what that
table reads. A key the previous source declared and the new one drops goes away;
a key something set at runtime stays.

Hold onto one fact before the forms: **an identity is never a path.**
`@builtin::modules.transform` does not "mean" the folder
`/zero/source/libs/@builtin/modules/transform` — it means *the index entry named
`modules.transform` in the `@builtin` root*. Where that asset's bytes physically
live is incidental; the index knows, and you don't have to.

## The forms

### Cross-root identity — `@scope::name`

`@scope::` is a **root rekey**. It says "resolve the rest of this string in
library `scope`'s root, not in mine." The library's own location is found
through the index.

After the `::`, a **suffix-free** remainder is an **identity** within that root:

```lua
local Transform = require("@builtin::modules.transform")   -- → table
```

The dotted token `modules.transform` is the index key inside `@builtin`. This is
the everyday way to reach a built-in module from your own world: rekey to
`@builtin`, then name the identity.

### Cross-root path — `@scope::a/b.module`

Give the remainder a `/` and **full type suffixes** and it flips from identity
to **path** — a literal location walked inside that library's root:

```lua
local mc = require("@builtin::demos/minecraft/minecraft.module")   -- → table
```

This addresses the file tree directly: `demos/minecraft/minecraft.module` is a
real folder under `@builtin`, and naming the `.module` directory resolves to its
primary file. Reach for this when you want a specific file by where it sits
rather than by its indexed name.

### Bare token — current-root identity

A suffix-free token with no `@scope::` is an **identity in the requiring file's
own root**:

```lua
-- inside a module that itself lives in the @builtin root:
local Transform = require("modules.transform")   -- → @builtin::modules.transform
```

This is the same identity lookup as `@scope::`, with the root left implicit as
*your* root. The consequence catches people out: a module in your **world**
source that writes `require("modules.transform")` looks for `modules.transform`
*in the world root*, finds nothing, and fails —

```lua
-- inside a module under /zero/source (the world root):
require("modules.transform")
-- → error: module 'modules.transform' not found
```

— because the built-in lives in `@builtin`, not your world. The bare token is
**not** shorthand for the VFS path `/zero/source/modules/transform`; it is an
index lookup in your root. To cross into the engine's library, rekey:
`require("@builtin::modules.transform")`.

### Relative — anchored to the requiring file

Two relative spellings, both anchored to the file doing the `require`, and the
suffix test tells them apart.

**Dot form — relative identity.** Suffix-free, walks the *module hierarchy*
(siblings and ancestors by logical name):

```lua
-- inside helper's sibling module:
local helper = require(".helper")        -- the sibling identity → table
```

**Slash form — relative path.** Full names with type suffixes, walks the
*directory tree*:

```lua
local helper = require("./helper.module")     -- sibling folder → its primary
local up     = require("../target.module")    -- climb one directory, then resolve
```

`./` is "this file's directory," `../` climbs one level — exactly like a shell.
A `.module` directory resolves to its primary file; naming a specific file
(`../shared.tool/extra.luau`) loads that file as-is, which is how you reach a
submodule *inside* a composite asset.

### Why `./helper` is rejected

The suffix is the form-marker, and it is mandatory on the slash form:

```lua
require("./helper.module")   -- ✓ a path — resolves the sibling folder
require(".helper")           -- ✓ an identity — resolves the sibling by name
require("./helper")          -- ✗ error: module './helper' not found
```

`./helper` is a *path* (the `./` says so) that is **missing its type suffix**, so
it names no real VFS entry; and it isn't a dotted identity either (the `./`
disqualifies it). It is neither valid form, so it fails loudly rather than
guessing. Write `./helper.module` (path) or `.helper` (identity) — pick the door,
name it fully.

## One asset → one primary file

Whatever the form, resolution ends at an asset, and the asset type picks the code
file to load. That is what makes "require the folder" work, and it is also why
`require` is **for code**:

```lua
require("@builtin::demos/minecraft/minecraft.scene")
-- → error: module '...minecraft.scene' not found
```

A `.scene`'s primary file is its scene data (JSON), not Luau. `require` converges
on the scene asset, finds its primary isn't code, and stops with a clean "not
found." It does **not** hand the JSON to the Luau compiler. Requiring a
`.material`, or any data asset, fails the same clean way — `require` is the door
for modules, and data assets are loaded through their own APIs.

## Unresolved is loud, never silent

Every failure above is the *same* failure: a clear "module not found." This is
deliberate and load-bearing.

```lua
require("@builtin::no.such.module")   -- → error: module '@builtin::no.such.module' not found
require("no_such_bare_token")         -- → error: module 'no_such_bare_token' not found
```

An identity with no index entry, or a path with no VFS entry, fails immediately.
What it will **never** do is fall through — take an identity string, bend it into
a folder path, and compile whatever file happens to sit there. A miss is a miss,
reported as one, at the point you asked. When a `require` fails, read the form
against the rules above: wrong root (use `@scope::`), wrong door (suffix on a
slash path), or the asset genuinely isn't there.

## The model to carry

Read the shape, and the rest follows: **suffix or `/` ⇒ path** (walked literally
in the VFS), **suffix-free token ⇒ identity** (looked up in the index), **leading
dot ⇒ relative** to the requiring file (and the suffix again says identity vs
path). Both roads end at one asset and its primary code file. `@scope::` rekeys
which root an identity resolves in; a bare token uses your own root. And a `require`
that can't find its target says so, plainly, instead of resolving something you
didn't ask for.
