Events, signals and tasks

Most scripts do two things: wait for something to happen, and schedule things to happen later. This page covers both: signals (events you attach functions to) and the task library (timers and threads).

How handlers run

Every event handler runs on its own thread. This applies to Touched, PlayerTouched, Clicked, MouseClick, GUI clicks, RunService.Frame, RunService.Draw, input, remotes, PlayerAdded, CharacterAdded, Died, tween Completed, Changed, ChildAdded and the rest. That means:

  • task.wait() and signal:Wait() work inside any handler.
  • A handler that errors does not stop the other handlers of the same event.
  • A handler that waits does not block the next firing of the event. If that matters, guard with a flag or wrap the function in task.debounce.
  • Handlers of one signal run in the order they were connected.

Signals

A signal is an event object such as part.Touched or Players.PlayerAdded. You listen with :On.

MemberReturnsDescription
signal:On(fn)ConnectionCalls fn every time the signal fires.
signal:Once(fn)ConnectionLike On, but stops listening after the first firing.
signal:Wait()The signal's argumentsYields until the signal fires. The thread is parked, not polled.
connection:Off()Stops calling the handler.
connection.Activebooleantrue until Off.
local conn = part.Touched:On(function(hit)
    print("touched by", hit.Name)
end)

task.wait(10)
conn:Off()         -- stop listening after ten seconds

A signal accepts at most 500 handlers. Call Off on the ones you no longer need.

Events every object has

These exist on parts and other scene objects, the workspace, models and GUI elements.

EventFiresArguments
inst.ChangedAfter a script assigns one of the object's properties.The property name. On value objects: the new Value.
inst:GetPropertyChangedSignal("Name")After a script assigns that one property.None.
inst.ChildAddedWhen a child is created, cloned, reparented in, or moved in.The child.
inst.ChildRemovedWhen a child is destroyed or moved out.The child.
inst.DescendantAddedWhen anything is added anywhere below the object.The new object.
inst.DestroyingJust before inst:Destroy() removes it.None.

ChildRemoved and Destroying run while the object still exists, so a handler can read its name and properties before it goes.

Changed only sees assignments made by scripts. Changes the engine makes itself, such as physics moving an unanchored part, do not fire it.

local lamp = workspace.Lamp
lamp:GetPropertyChangedSignal("Transparency"):On(function()
    print("lamp is now", lamp.Transparency)
end)
workspace.ChildAdded:On(function(child) print("new object:", child.Name) end)
lamp.Destroying:On(function() print("lamp going away") end)

Touch and click events on parts

EventArgumentsDescription
part.TouchedhitFires on the server when something touches the part: a player's character part, or another part. A player fires it once on stepping onto the part.
part.TouchEndedhitFires once when the thing leaves.
part.PlayerTouchedplayer, character, hitStudz addition. Fires only for players, with the player and character already resolved.
part.PlayerTouchEndedplayer, character, hitThe matching end event.
part.ClickedplayerStudz addition. The part's ClickDetector.MouseClick. The detector is created the first time you use Clicked.

Prefer PlayerTouched over Touched when you only care about players. It saves you the hit.Parent:FindChild("Humanoid") and Players:FromCharacter dance:

-- The long way
part.Touched:On(function(hit)
    local humanoid = hit.Parent:FindChild("Humanoid")
    if not humanoid then return end
    local player = Players:FromCharacter(hit.Parent)
    -- ...
end)

-- Studz
part.PlayerTouched:On(function(player, character, hit)
    -- ...
end)

The task library

FunctionDescription
task.wait([seconds])Yields the current script for seconds (default: one step). Returns the time actually waited.
task.spawn(fn, ...)Runs fn now on a new thread. If it yields with task.wait, it is rescheduled.
task.after(seconds, fn, ...)Runs fn on a new thread after seconds. (task.delay is the deprecated Roblox name.)
task.defer(fn, ...)Runs fn on the next step.
task.every(seconds, fn, ...)Calls fn again and again. Returns a handle with handle:Cancel(). An error inside fn is reported and the timer keeps running.
task.debounce(fn, seconds)Returns a wrapper that ignores calls arriving less than seconds after the last accepted one.
task.cancel(thread)Cancels a thread returned by spawn, after or defer.
-- Repeat every 2 seconds, stop after 10 seconds
local timer = task.every(2, function() print("tick") end)
task.after(10, function() timer:Cancel() end)

-- Ignore button spam
local onClick = task.debounce(function(player)
    player:Tell("Thanks!")
end, 1)
button.Clicked:On(onClick)

Frame events

RunService fires every frame. Get it with game:Service("RunService") or use the global.

MemberDescription
RunService.Frame:On(function(dt) end)Every simulation step. dt is seconds since the last one.
RunService.Draw:On(function(dt) end)Every rendered frame (client).
RunService:IsServer() / :IsClient()Opposites. IsClient is true on a player's machine.
RunService:IsStudio() / :IsRunning()Whether the script runs inside Polycarbonate, and whether the game is running.

Anything in a frame event must finish within the 0.25 second time limit, so keep the work small and use task.every for slower periodic work.

Other event objects

BindableEvent signals between scripts on the same side (server to server, or client to client). Use :Fire(...) to send and .Event:On(fn) to receive. For server and client communication use remotes: see Client and server.