---
title: "scopes"
description: "Scopes: work you start together and that ends together, tied to what started it (an entity, a component, a scene, another scope)…"
section: "API Reference"
slug: "api-scopes"
canonical: "https://origozero.ai/docs/api-scopes"
updated: "2026-10-20T13:15:36.957986995+00:00"
tags: ["api", "reference"]
---

# scopes

Scopes: work you start together and that ends together, tied to what started it (an entity, a component, a scene, another scope), and the owning contexts every live resource is registered under. -- Load two things at once; neither outlives the load. task.spawn(function() scopes.run(function(s) s:spawn("terrain", function() task.wait(2) end) s:spawn("props", function() task.wait(1) end) end) print("both are done") end) -- What is live, and what will never be reached by a seam? for _, r in scopes.list() do if not r.endsAtASeam then print(r.kind, r.handle, r.scope, r.detail) end end

Start several pieces of work that must not outlive the thing that started
them, and wait for all of them:
  scopes.run(function(s)
      s:spawn("load-terrain", function() ... end)
      s:spawn("load-props", function() ... end)
  end)
  -- returns once both are finished or cancelled; the first error either
  -- raised is raised again here, after the other one was cancelled
Start work tied to an entity, without waiting for it:
  local s = scopes.open({ entity = npc })
  s:spawn("patrol", function() while true do ... task.wait(1) end end)
  s:spawn("bark", function() ... end)
  -- despawning npc cancels both; so does s:cancel()
Everything a task in a scope starts belongs to the scope too: a
`task.spawn` inside it, a job it registers, a scope it opens. A scope is
lived inside what the code opening it belongs to, so a scope opened in a
component's hook ends with the component (destroyed or hot-reloaded), one
opened by a scene's entrypoint ends with that load of the scene, and one
opened in another scope's work ends with that scope.
When a scope ends, every task still running in it is cancelled, and
unwinds: the cancellation is raised at the wait it is suspended in, so a
`pcall` or cleanup around that wait runs (`task.isCancellation(err)`
recognises it). A scope ends when:
  * its `scopes.run` is over;
  * work in it raises: the rest of its work is cancelled, and `join` (and
    so `scopes.run`) raises that first error again where it is called.
    `s.onError = function(name, err) end` observes every error as it is
    raised. The VM-wide `task.onError` is not told while a `scopes.run`
    or a `join` waits for the scope or `onError` observes it; an error in
    a scope nothing waits for or observes is reported as any task's
    error is;
  * `s:cancel()`, `scopes.release(s.tag)`, or cancelling the task waiting
    in `scopes.run`;
  * what it is lived inside ends: the entity it is tied to despawns, the
    component is destroyed or hot-reloaded, the scene load ends, the
    enclosing scope ends.
`s:why()` says which ended it first, and is nil while the scope is open.
A `scopes.run` whose scope something else ended returns once the work
it started is cancelled, and `s:why()` names what ended it.
The registry: a resource reached through a handle (a task, a substrate
job, a camera feed, an input-map activation, an input subscription)
outlives the call that made it. Each one records the context it was
registered from, and the engine ends what a context left behind once
that context's run is over. The contexts, by scope tag:
  * `scope:<n>`: a task scope, as above.
  * `entity:<id>`: an entity. Ends when it is despawned.
  * `scene:<guid>#<n>`: one load of a scene layer. Ends when the load
    ends: the layer is reloaded or unloaded, the scene is swapped, the
    mode flips.
  * `chunk:<identity>`: a module. Ends when the module runs again (a VFS
    write to its source) or is dropped from the require cache
    (`vfs.reload`). The next run registers its own.
  * `component:<instanceId>`: one component instance. Ends when that
    instance is destroyed: its entity despawned, its scene cleared, the
    component removed. A hot reload of the component ends its tasks and
    the resources Luau code holds for it (listed with kind `luau`), and
    the new life's `awake` takes its own.
  * `exec:<chunk>`: a chunk submitted for a single run, such as an
    `execute` call. It is never re-entered, so nothing ends it on its own.
A `luau` resource (an input-map activation, an input subscription) taken
by work in a task scope, such as a `future.spawn` body or a future's
handler, follows what that scope runs inside rather than the scope: the
holder keeps it after the work is over.
`list()` shows every live resource by the context it follows, and
`release(scope)` ends everything under one. That last is what reaches a
resource no variable names any more: a task an `execute` call started and
lost the handle of is listed under `exec:<chunk>`, and releasing that tag
cancels it.

```lua
require("@builtin::modules.scopes") -- scopes (also available as global 'scopes')
```

The `scopes` namespace: 15 functions.

## globals/scopes/current {#globals-scopes-current}

```lua
scopes.current() -> string?
```

The scope the calling code registers a resource under right now: the
module whose body is running, the task scope its work runs in, the
component instance whose lifecycle hook is on the stack, or the chunk of
this call. Nil when the caller registers under no context.

**Returns** `string?` — The scope tag, or nil.

```lua
print("resources I register follow", scopes.current())
```

## globals/scopes/list {#globals-scopes-list}

```lua
scopes.list() -> { LiveResource }
```

Every live resource that follows an owning context, across every
subsystem holding them. A resource registered with no context above it,
the engine's own, is not listed, because no scope reaches it.

**Returns** `{ LiveResource }` — Array of live resources.

```lua
for _, r in scopes.list() do print(r.scope, r.kind, r.detail) end
```

## globals/scopes/open {#globals-scopes-open}

```lua
scopes.open(options: ScopeOptions?) -> Scope
```

Open a task scope without waiting for it: start work in it with
`s:spawn`, end it with `s:cancel()`, or let what it is lived inside end it
(`{ entity = id }` ties it to an entity). Wait for its work with
`s:join()` from a task.

**Parameters**

- `options` `ScopeOptions` _(optional)_ — `{ entity = id }` ties the scope to an entity; `{ name = "..." }` names it.

**Returns** `Scope` — The scope.

```lua
local s = scopes.open({ entity = npc }); s:spawn("patrol", patrol)
```

## globals/scopes/release {#globals-scopes-release}

```lua
scopes.release(scope: string) -> { Released }
```

End every resource registered under `scope`, across every subsystem:
tasks are cancelled, jobs dropped. Reaches contexts no seam does, such as
the chunk of an `execute` call that started something and ended without
ending it. An `execute` call runs as a task listed under its own `exec:`
scope, so releasing that scope from the same call cancels the call too:
release it from a later call.

**Parameters**

- `scope` `string` — A scope tag, as the `scope` field of a `list()` row carries it.

**Returns** `{ Released }` — One row per subsystem that ended something, with how many it ended.

```lua
local ended = scopes.release("exec:__exec_12")
```

## globals/scopes/run {#globals-scopes-run}

```lua
scopes.run(fn: (Scope) -> ...any, options: ScopeOptions?) -> ...any
```

Run `fn(s)` in a new task scope and wait until every piece of work
started in it is finished or cancelled. Returns what `fn` returned. The
first error raised by `fn` or by any work in the scope cancels the rest,
and is raised again here, as it was raised, once their cleanup has run.
Waits, so it must be called from a task: in code that cannot wait (a
component hook called directly, a callback) it raises, naming that.

**Parameters**

- `fn` `(Scope) -> ...any` — The function to run; it receives the scope, and starts work with `s:spawn`.
- `options` `ScopeOptions` _(optional)_ — `{ entity = id }` ties the scope to an entity; `{ name = "..." }` names it.

**Returns** `...any` — What fn returned.

```lua
scopes.run(function(s) s:spawn("a", fa); s:spawn("b", fb) end)
```

## modules/scopes/cancel {#modules-scopes-cancel}

```lua
cancel(self: Scope, name: string?): number
```

Cancel the work named `name`, or end the whole scope when `name` is
omitted. Cancelled work unwinds: its cleanup runs at the wait it is
suspended in. Returns how many tasks this cancelled.

**Parameters**

- `self` `Scope`
- `name` `string?` _(optional)_ — A child's name; omit to end the scope.

```lua
s:cancel("patrol")
```

## modules/scopes/children {#modules-scopes-children}

```lua
children(self: Scope): { ScopeChild }
```

What this scope started with `spawn`, in the order started, each with
its current `task.status`: every child still running, and the 32 that
ended most recently.

**Parameters**

- `self` `Scope`

```lua
for _, c in s:children() do print(c.name, c.status) end
```

## modules/scopes/current {#modules-scopes-current}

```lua
current(): string?
```

The scope the calling code registers a resource under right now: the
module whose body is running, the task scope its work runs in, the
component instance whose lifecycle hook is on the stack, or the chunk of
this call. Nil when the caller registers under no context.

```lua
print("resources I register follow", scopes.current())
```

## modules/scopes/join {#modules-scopes-join}

```lua
join(self: Scope, name: string?)
```

Wait until the work named `name` (all of the scope's work when omitted)
is finished or cancelled, its cleanup included. Raises the first error
that work raised, as it was raised. Called from work in the scope (the
function `scopes.run` runs, say), it waits for the rest of the work and
not for the calling task. Waits, so call it from a task.

**Parameters**

- `self` `Scope`
- `name` `string?` _(optional)_ — A child's name; omit to wait for everything in the scope.

```lua
s:join()
```

## modules/scopes/list {#modules-scopes-list}

```lua
list(): { LiveResource }
```

Every live resource that follows an owning context, across every
subsystem holding them. A resource registered with no context above it,
the engine's own, is not listed, because no scope reaches it.

```lua
for _, r in scopes.list() do print(r.scope, r.kind, r.detail) end
```

## modules/scopes/open {#modules-scopes-open}

```lua
open(options: ScopeOptions?): Scope
```

Open a task scope without waiting for it: start work in it with
`s:spawn`, end it with `s:cancel()`, or let what it is lived inside end it
(`{ entity = id }` ties it to an entity). Wait for its work with
`s:join()` from a task.

**Parameters**

- `options` `ScopeOptions?` _(optional)_ — `{ entity = id }` ties the scope to an entity; `{ name = "..." }` names it.

```lua
local s = scopes.open({ entity = npc }); s:spawn("patrol", patrol)
```

## modules/scopes/release {#modules-scopes-release}

```lua
release(scope: string): { Released }
```

End every resource registered under `scope`, across every subsystem:
tasks are cancelled, jobs dropped. Reaches contexts no seam does, such as
the chunk of an `execute` call that started something and ended without
ending it. An `execute` call runs as a task listed under its own `exec:`
scope, so releasing that scope from the same call cancels the call too:
release it from a later call.

**Parameters**

- `scope` `string` — A scope tag, as the `scope` field of a `list()` row carries it.

```lua
local ended = scopes.release("exec:__exec_12")
```

## modules/scopes/run {#modules-scopes-run}

```lua
run(fn: (Scope) -> ...any, options: ScopeOptions?): ...any
```

Run `fn(s)` in a new task scope and wait until every piece of work
started in it is finished or cancelled. Returns what `fn` returned. The
first error raised by `fn` or by any work in the scope cancels the rest,
and is raised again here, as it was raised, once their cleanup has run.
Waits, so it must be called from a task: in code that cannot wait (a
component hook called directly, a callback) it raises, naming that.

**Parameters**

- `fn` `(Scope) -> ...any` — The function to run; it receives the scope, and starts work with `s:spawn`.
- `options` `ScopeOptions?` _(optional)_ — `{ entity = id }` ties the scope to an entity; `{ name = "..." }` names it.

```lua
scopes.run(function(s) s:spawn("a", fa); s:spawn("b", fb) end)
```

## modules/scopes/spawn {#modules-scopes-spawn}

```lua
spawn(self: Scope, name: string, fn: (...any) -> ...any, ...: any): number
```

Start `fn(...)` as a task in this scope, named `name`, and return its
task handle. The task, and everything it starts, belongs to the scope: it
is cancelled (and unwinds) when the scope ends. Raises when the scope has
already ended, naming why.

**Parameters**

- `self` `Scope`
- `name` `string` — What the work is called, in `children()`, `join(name)`, errors and listings.
- `fn` `(...any) -> ...any` — The function to run.
- `args` `any` _(optional)_

```lua
s:spawn("props", function() task.wait(1) end)
```

## modules/scopes/why {#modules-scopes-why}

```lua
why(self: Scope): string?
```

Why the scope ended: its run finished, it was cancelled, a named
child raised, or what it was lived inside ended (`entity:<id> ended`,
`component:<id> ended`, ...). Nil while the scope is open.

**Parameters**

- `self` `Scope`

```lua
print(s:why())
```
