Log inGet started

task

Updated 4 September 2026

The task namespace — 21 functions.

task/active

task.active() -> number

Get number of active (running + waiting + deferred) tasks.

Returns number — Count of active tasks

task/await

task.await(promise_id) -> any

Yield until a promise is resolved. Used with mcp_call and other promise-returning APIs.

Parameters

  • promise_id string — Promise identifier returned by a promise-returning API.

Returns any — The resolved value.

task/background

task.background() -> boolean

Declare that the calling task belongs in the background. A caller driving it inline — use_tool, bash, execute — hands back the task's watchable handle on its next poll instead of waiting out an inline budget the work will outlast. Call it as soon as the operation knows its own scale, before the long stretch begins; the decision can be per-call, so a small scope stays inline and a large one is handed back. Returns false outside any scheduler task, where there is no handle to hand back. Completion still arrives the usual way: poll task.status(handle), read the watch file, or wait for the operation's own notice.

Returns boolean — True when the calling task was marked; false outside any task

See also: task/current · task/status

task/batched

task.batched(count, fn, batch_size?, on_complete?) -> number

Run fn(i) for i = 1..count, yielding to the next frame every batch_size iterations (default 100). Spawns a coroutine and returns its task handle immediately. Use for bulk spawn loops or mass mutations that would otherwise block the VM long enough to trip the watchdog.

Parameters

  • count number — Total number of iterations.
  • fn function — Function called as fn(i) for each iteration.
  • batch_size number (optional) — Iterations per frame before yielding (default 100).
  • on_complete function (optional) — Optional callback invoked after the final iteration.

Returns number — Task handle (the spawned coroutine).

task/callerInfo

task.callerInfo(level?) -> string?

Return the formatted "<source>:<line>" of a Luau stack frame, or nil if the frame doesn't exist or has no source. level defaults to 1 (the immediate Lua caller); 2 skips one frame above, and so on. Used by task.loop to tag iteration-error warnings with the spawn site, and useful for any custom error-reporting wrapper since the stock debug.info is gated out of user scope. See gh#3416 / gh#3248.

Parameters

  • level number? — Stack level — 1 (default) = immediate Lua caller, 2 = caller's caller, etc.

Returns string? — "source:line" string, or nil when unavailable

task/cancel

task.cancel(handle)

Cancel a running or waiting task. The coroutine will not resume. No-op on finished tasks.

Parameters

  • handle number — Task handle from spawn/defer/delay

task/collect

task.collect(handle?) -> boolean

Free a finished task handle (or all finished handles if no argument). Returns true if any were collected. Finished handles are auto-collected after ~5 seconds.

Parameters

  • handle number? — Optional task handle to collect; omit to collect all

Returns boolean — Whether any handles were collected

task/current

task.current() -> number?

The handle of the task the calling code runs in, or nil outside any scheduler task. Pair it with task.status to tell whether the owner of a long-running operation is still being advanced: a cancelled task stops mid-flight without unwinding, so state it claimed (a lock, an in-flight flag, a progress record) outlives it unless someone can ask about the owner. Record task.current() when you take such a claim, and let the next caller reap it when task.status(owner) reports a terminal state. Scoped to the calling VM.

Returns number? — Handle of the calling task, or nil when not running inside one

task/defer

task.defer(fn, ...args) -> number

Schedule fn to run at end of current frame. Returns a task handle that can be used with task.status/task.cancel.

Parameters

  • fn function — Function to run at end of frame

Returns number — Task handle

task/delay

task.delay(seconds, fn, ...args) -> number

Schedule fn to run after seconds of real time. Returns a task handle.

Parameters

  • seconds number — Seconds before fn runs.
  • fn function — Function to run after the delay.

Returns number — Task handle for cancel/status.

task/inflight

task.inflight(label) -> number

Number of scheduler tasks with the given label that have been dispatched and not yet reached a terminal state (finished or cancelled). Component lifecycle hooks dispatch under their hook name — task.inflight("awake") == 0 means every awake() coroutine has settled. The join primitive for "loaded means loaded": scene-load completion polls it so 'done' includes the detached lifecycle work the spawn dispatched, not merely the entity rows.

Parameters

  • label string — Task label to count (e.g. "awake")

Returns number — Count of in-flight tasks carrying that label

task/loop

task.loop(fn, dt?) -> number

Spawn a self-healing per-frame loop. Each iteration runs fn(delta) where delta is the seconds elapsed since the previous iteration, so task.loop(function(delta) pos += speed * delta end) moves by real time; a no-argument function() end still works. An uncaught error inside fn is caught, surfaced to task.onError (if registered), logged via log.warn, and the loop continues on the next iteration. Use this for the common scene pattern of one long-lived coroutine that drives input/sim/UI per frame; a bad nil-deref on one entity no longer kills the entire loop. See gh#3248.

Parameters

  • fn function — Body to run every iteration, called as fn(delta) with the seconds elapsed since the previous iteration. Errors are caught — they do not kill the loop.
  • dt number (optional) — Seconds to wait between iterations (default 0 = next frame). This is the loop interval, distinct from the per-iteration delta passed to fn.

Returns number — Task handle (cancel via task.cancel to stop the loop).

task/onError

task.onError(handler: ((err: string, source: string?, handle: number) -> ())?)

Register a per-VM error handler invoked when a coroutine spawned via task.spawn / task.defer / task.delay errors. The handler receives (err_message, source_name, handle) and is itself called under pcall — its own errors are caught and logged, never re-raised. Passing nil clears the handler. Use this to surface 'silent freeze' coroutine deaths to scene-side code (e.g. show a SCRIPT ERROR overlay, restart the loop with backoff, escalate to telemetry). See gh#3248.

Parameters

  • handler function | nil — Function called as (err, source, handle) on coroutine error, or nil to clear

task/result

task.result(handle) -> any?

Get a FINISHED task's return value, decoded from its lossless JSON capture. Raises when the task is not finished — still running/waiting/deferred, failed, cancelled, or an unknown handle — naming the state, so a nil return means exactly one thing: the task finished and returned nil. Poll task.status(handle) until it reports 'finished' (or wait on the watch file a promoted call gives you), then read the result.

Parameters

  • handle number — Task handle from spawn/defer/delay

Returns any? — The finished task's return value; nil only when it returned nil. Raises otherwise.

task/spawn

task.spawn(fn, ...args) -> number

Spawn a new coroutine that runs fn with the given args. Returns a numeric task handle (not a thread) that can be passed to task.cancel/task.status.

Parameters

  • fn function — Function to run in a new coroutine

Returns number — Task handle (numeric ID) for cancel/status

task/spawnSystem

task.spawnSystem(fn, ...args) -> number

Like task.spawn, but the coroutine is forced system-owned: it is never gameplay-owned, so it keeps running while the editor pauses gameplay — even when started from a component's awake()/onEnable(). Use for system-level loops (input ticking, watchers, editor tooling) that must survive pause regardless of where they were launched. Nested spawns inherit non-ownership normally.

Parameters

  • fn function — Function to run in a new system-owned coroutine

Returns number — Task handle (numeric ID) for cancel/status

task/status

task.status(handle) -> string

Get task status: 'running' (executing), 'waiting' (task.wait), 'suspended' (await), 'deferred' (queued), 'finished' (completed), 'cancelled', or 'dead' (unknown handle / aged out of terminal history). A finished task that was auto-collected still reports its terminal status from the terminal-history cache (mirrors task.result), so missing the live finished flip doesn't lose the outcome.

Parameters

  • handle number — Task handle from spawn/defer/delay

Returns string — Task status: 'running' | 'waiting' | 'suspended' | 'deferred' | 'finished' | 'cancelled' | 'dead'

task/wait

task.wait(seconds?) -> number

Yield the current coroutine. With a duration, resumes after seconds of real time. With none — task.wait() — resumes on the NEXT frame: getFrame() reads exactly one higher on the other side of it, wherever in the frame the wait was armed. Either way it returns the elapsed seconds.

Parameters

  • seconds number (optional) — Seconds to wait. Omit (or 0) to wait exactly one frame.

Returns number — Actual elapsed seconds.

task/waitDrawnFrames

task.waitDrawnFrames(frames?) -> number

Yield the current coroutine until the renderer has DRAWN frames more frames (default: 1), and ask it to keep drawing for as long as the wait lasts. This is the wait to use before reading anything the renderer produced — a capture, an observation, a readback, a cost — because the engine runs a frame whether or not anything wants a picture of it, and a headless renderer declines the ones nothing is consuming. task.waitFrames(n) counts frames the ENGINE ran, which on an idle headless engine is many times the number it drew. renderer.drawnFrames() is the count this is deadlined on.

Parameters

  • frames number (optional) — Drawn frames to wait for. Default 1.

Returns number — Frames the renderer drew across the wait.

task/waitFrames

task.waitFrames(frames?) -> number

Yield the current coroutine until frames more frames have run (default: 1). Exactly that many: the deadline is the engine's own frame counter, the one getFrame() reads, so local a = getFrame() task.waitFrames(n) leaves getFrame() - a == n however long those frames took and wherever in the frame the wait was armed. Work that lands a fixed number of frames later is read by counting frames rather than seconds, and one yield covers the whole span, where the same wait spelled as repeated task.wait() calls costs a scheduler round-trip per frame. It counts frames the ENGINE ran; frames the renderer submitted is a separate count (renderer.framePacing().submittedFrames), lower whenever the renderer declines a frame nothing is consuming.

Parameters

  • frames number (optional) — Frames to wait. Default 1 (the next frame).

Returns number — Frames waited.

task/wake

task.wake(co: thread, ...: any) -> (boolean, string?)

Resume a suspended coroutine and keep the scheduler's ownership of what it yields next. Code that captures coroutine.running() and resumes it later — a signal, a component event — uses this so the resumed coroutine's task.wait still reaches the scheduler and its task keeps reporting status and result. A coroutine the scheduler is not holding parked is resumed directly, and one whose task was cancelled is refused. Returns true when the resume ran, or false and the reason it could not.

Parameters

  • co thread — The suspended coroutine to resume
  • ... any (optional) — Values the coroutine's yield returns

Returns boolean — Whether the coroutine was resumed

  • api
  • reference