task
The task namespace — the engine's Luau API reference for task.
The task namespace — 16 functions.
task/active
task.active() -> number
Get number of active (running + waiting + deferred) tasks.
task/await
task.await(promise_id) -> any
Yield until a promise is resolved. Used with mcp_call and other promise-returning APIs.
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.
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.
task/cancel
task.cancel(handle)
Cancel a running or waiting task. The coroutine will not resume. No-op on finished tasks.
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.
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.
task/delay
task.delay(seconds, fn, ...args) -> number
Schedule fn to run after seconds of real time. Returns a task handle.
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.
task/loop
task.loop(fn, dt?) -> number
Spawn a self-healing per-frame loop. Wraps task.spawn(function() while true do pcall(fn); task.wait(dt or 0) end end) — 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.
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.
task/result
task.result(handle) -> any?
Get a finished task's return value, decoded from its lossless JSON capture. Returns nil if the task isn't finished (still running, errored, cancelled, or its result wasn't captured). Pairs with task.status to poll a promoted execute/bash by id without building a /runtime/tasks path.
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.
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.
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.
task/wait
task.wait(seconds?) -> number
Yield the current coroutine. Resumes after seconds of real time (default: next frame). Returns actual elapsed time.