Reaper Examples
This page demonstrates every public API in the Reaper framework through scenario-based examples with inline commentary.
Reaper Module APIs
Reaper.Inject
Injects optional dependencies into Reaper before using advanced features.
Scenario A — Injecting both Assignment and Relay at game startup
local Reaper = require(game.ReplicatedStorage.Reaper)
local Assignment = require(game.ReplicatedStorage.Assignment)
local Relay = require(game.ReplicatedStorage.Relay)
-- Inject Assignment first so Scope:Repeat() becomes available
Reaper.Inject("Assignment", Assignment)
-- Inject Relay to activate the three global lifecycle signals
Reaper.Inject("Relay", Relay)
-- From this point on, Reaper.Signals.OnTracked/OnCleaned/OnRemoved are live
Scenario B — Running Reaper standalone without any dependencies
local Reaper = require(game.ReplicatedStorage.Reaper)
-- No Inject calls needed — Reaper tracks and cleans items immediately
-- Scope:Spawn(), :Defer(), :Delay(), :Connect() all work via native task.*
-- Scope:Repeat() is unavailable without Assignment
-- Reaper.Signals are no-op stubs that warn in Studio if connected to
local scope = Reaper.Scope("PlayerScope")
scope:Connect(workspace.ChildAdded, function(child)
print("Added:", child.Name)
end)
Reaper.Track
Registers a raw item into Reaper's memory system and returns a handle to manage its lifecycle.
Scenario A — Tracking an Instance; death detected via the engine event, not polling
local character = player.Character
-- Instances are always monitored via an AncestryChanged hook — not by polling.
-- The Frequency argument to :Configure() is ignored for Instances and forced to 0.
-- Reaper calls :Destroy() automatically when the character leaves the game world.
local track = Reaper.Track(character)
:Configure("Character_" .. player.UserId, 0)
Scenario B — Tracking a custom OOP object with a non-standard teardown method
local weaponController = WeaponController.new(player)
-- "Dispose" is called instead of the default :Destroy() on teardown
local track = Reaper.Track(weaponController, "Table", "Dispose")
:Configure("Weapon_" .. player.UserId, 0)
-- Frequency 0: no background polling — cleaned manually or via cascade
Scenario C — Re-tracking an already-registered item returns the existing handle
local part = Instance.new("Part", workspace)
local trackA = Reaper.Track(part)
local trackB = Reaper.Track(part) -- item already registered
print(trackA == trackB) -- true — same handle, no duplicate entry created
Scenario D — Protected tracking for an Object Pool
local pooledPart = objectPool:Acquire()
-- IsProtected = true: when this part leaves the workspace, Reaper releases it
-- from the registry without destroying it, so the pool can safely reclaim it
local track = Reaper.Track(pooledPart, nil, nil, true)
:Configure("PooledPart_" .. pooledPart:GetAttribute("PoolID"), 0)
Scenario E — Frequency-based polling for a Connection
-- Connections and Threads placed in the background batch tier are eligible
-- to be promoted to the frequency-polled tier via a non-zero Configure value.
-- Here, Reaper checks whether this connection is still active every 3 seconds.
local conn = someRemoteEvent.OnServerEvent:Connect(onRemoteEvent)
local track = Reaper.Track(conn)
:Configure("RemoteConn_" .. player.UserId, 3)
-- If the connection has been disconnected externally, Reaper detects it
-- within 3 seconds and removes it from the registry automatically
Reaper.Scope
Creates a named, managed container that accumulates items and tears them all down together when the Scope is cleaned.
Scenario A — Scoping a player's active connection set
local combatScope = Reaper.Scope("Combat_" .. player.UserId)
-- All connections added here are disconnected when combatScope is cleaned
combatScope:Connect(player.Character.Humanoid.Died, function()
print(player.Name, "died in combat")
end)
combatScope:Connect(player.Character.Humanoid.HealthChanged, function(health)
updateHealthUI(player, health)
end)
-- When combat ends:
Reaper.Clean("Combat_" .. player.UserId)
-- Both connections are disconnected in one call
Scenario B — Nested Scope for a sub-state within a larger state
local tradingScope = Reaper.Scope("Trading_" .. player.UserId)
-- Create a sub-scope for the item-selection phase
local itemSelectScope = Reaper.Scope("ItemSelect_" .. player.UserId)
itemSelectScope:Connect(tradeUI.ItemSelected, onItemSelected)
-- Bind the sub-scope to the parent; when trading ends, item selection also ends
itemSelectScope:BindToTrack(Reaper.Get("Trading_" .. player.UserId))
Reaper.Clean
Immediately destroys one or more tracked items and cascades teardown to everything they own.
Scenario A — Cleaning a single Scope by its ID
-- Somewhere at match start:
local matchScope = Reaper.Scope("Match_Round1")
matchScope:Connect(game.Players.PlayerRemoving, onPlayerLeft)
matchScope:Spawn(function() runMatchTimer() end)
-- At match end:
Reaper.Clean("Match_Round1")
-- The PlayerRemoving connection is disconnected
-- The matchTimer thread is cancelled
-- matchScope is removed from the registry
Scenario B — Cleaning multiple objects in a single call
-- Clean three scopes created for a player when they leave
Reaper.Clean(
"Combat_" .. player.UserId,
"UI_" .. player.UserId,
"Respawn_" .. player.UserId
)
-- All three are torn down sequentially in the same call
Scenario C — Cleaning via a raw TrackObject reference
local track = Reaper.Track(someInstance):Configure("SomeInstance", 5)
-- Later, clean via the handle directly (no ID lookup needed)
track:Destroy() -- identical to Reaper.Clean(track)
Reaper.Remove
Releases items from the registry without destroying them. Only the primary item is preserved; any chained children or bound Scopes are still destroyed.
Scenario A — Reclaiming a pooled instance before re-use
-- Release the root item from Reaper so neither system attempts to destroy it.
-- Any children chained to this track would be destroyed in the same call.
-- Chain nothing to a pooled item's track if you need children preserved too.
local evicted = Reaper.Remove("PooledPart_42")
-- evicted[1] is the raw Part instance, fully intact
objectPool:Release(evicted[1])
Scenario B — Releasing multiple items and inspecting what was returned
local removed = Reaper.Remove(
"TempEffect_A",
"TempEffect_B",
"TempEffect_C"
)
for _, item in removed do
print("Released:", tostring(item)) -- items are still alive; just de-registered
end
Reaper.Get
Returns the live tracking handle for an item or Scope registered under a given identifier.
Scenario A — Fetching a Scope from another module to chain additional items
-- In UIManager.lua:
local uiScope = Reaper.Scope("UI_" .. player.UserId)
uiScope:Connect(screenGui.Activated, onUIActivated)
-- In CombatManager.lua (separate script, no shared reference):
local uiScope = Reaper.Get("UI_" .. player.UserId)
if uiScope then
-- Chain a temporary combat HUD element to the existing UI scope
uiScope:Chain(combatHUDFrame)
-- When UIManager cleans "UI_playerX", the combatHUDFrame is also destroyed
end
Scenario B — Checking existence before operating on a Scope
local function tryCleanCombat(userId: number)
local existing = Reaper.Get("Combat_" .. userId)
if existing then
Reaper.Clean("Combat_" .. userId)
else
warn("No active combat scope for user:", userId)
end
end
Reaper.Is and Reaper.IsCleanable
Guards for validating live Reaper handles before operating on them.
Scenario A — Reaper.Is: guarding a function that accepts TrackObjects, ScopeObjects, or raw items
local function safeChain(target: any, child: any)
if Reaper.Is(target) then
-- target is a live TrackObject or ScopeObject; safe to call :Chain()
target:Chain(child)
else
-- target is a raw item or already-cleaned handle; register it first
Reaper.Track(target):Chain(child)
end
end
Scenario B — Reaper.IsCleanable: checking a Scope before handing it to a system
local function bindScopeToPlayer(scope: any, track: TrackObject)
if not Reaper.IsCleanable(scope) then
warn("Attempted to bind an invalid or destroyed scope — skipped.")
return
end
-- Safe to call BindToTrack only after the guard passes
(scope :: ScopeObject):BindToTrack(track)
end
Scenario C — Distinguishing a TrackObject from a ScopeObject
local handle = Reaper.Get("SomeID")
if Reaper.IsCleanable(handle) then
-- handle is specifically a ScopeObject
print("Got a scope:", handle.AssignID)
elseif Reaper.Is(handle) then
-- handle is a TrackObject (not a Scope)
print("Got a track:", handle.AssignID)
else
print("Not found or already cleaned")
end
TrackObject Methods
:Configure
Assigns an identifier and polling frequency to a TrackObject. For Instances and manual-tier items, the Frequency argument is always forced to 0; non-zero values are only meaningful for Connections and Threads.
Scenario A — Configuring a Connection with a dead-state check interval
-- Connections start in the background batch tier (30-second sweep).
-- Configure promotes this one to the frequency-polled tier at 5-second intervals.
local conn = someEvent:Connect(onEvent)
local connTrack = Reaper.Track(conn)
connTrack:Configure("EventConn_" .. player.UserId, 5)
-- Reaper checks every 5 seconds whether this connection is still active
Scenario B — Configuring a long-lived object with a relaxed interval
local conn = longLivedEvent:Connect(onLongLivedEvent)
local mapTrack = Reaper.Track(conn)
-- This connection rarely goes dead; check every 60 seconds to minimise overhead
mapTrack:Configure("LongConn_" .. player.UserId, 60)
Scenario C — Configure with Frequency 0 for manual-only management
local dataTrack = Reaper.Track(playerDataTable, "Table")
-- Tables have no native dead-state; opt out of polling entirely
dataTrack:Configure("PlayerData_" .. player.UserId, 0)
-- Must be cleaned explicitly: Reaper.Clean("PlayerData_playerX")
Scenario D — Configuring an Instance track (Frequency is always 0)
local character = player.Character
-- Frequency 0 is the only valid value for Instance tracks.
-- AncestryChanged detection is already registered at Track() time;
-- passing any other value here is silently treated as 0.
local charTrack = Reaper.Track(character)
:Configure("Char_" .. player.UserId, 0)
:Chain
Binds raw child items to a handle for cascade cleanup.
Scenario A — Chaining a connection and a thread to an Instance track
local characterTrack = Reaper.Track(character):Configure("Char_" .. userId, 0)
local healthConn = character.Humanoid.HealthChanged:Connect(onHealthChanged)
local animThread = task.spawn(runAnimationLoop, character)
-- Both are destroyed when the character Instance leaves the game world
characterTrack:Chain(healthConn)
characterTrack:Chain(animThread)
Scenario B — Chaining with a custom teardown method
local customObj = MyFramework.new()
-- MyFramework uses :Dispose() instead of :Destroy()
vehicleTrack:Chain(customObj, "Dispose")
-- On cascade, Reaper calls customObj:Dispose() instead of the default
Scenario C — Fluent chaining across multiple items
Reaper.Track(part):Configure("TrackedPart", 0)
:Chain(part.Touched:Connect(onTouched))
:Chain(part.ChildAdded:Connect(onChildAdded))
:Chain(task.delay(10, destroyEffect, part))
-- All three children are torn down when the part leaves the game world
:HandleScope
Subordinates an entire Scope to this handle.
Scenario A — Binding a behaviour Scope to a character Track
local charTrack = Reaper.Track(character):Configure("Char_" .. userId, 0)
local combatScope = Reaper.Scope("Combat_" .. userId)
combatScope:Connect(character.Humanoid.Died, onDied)
combatScope:Spawn(combatLoop)
-- When the character is destroyed, the combat scope is fully torn down too
charTrack:HandleScope(combatScope)
Scenario B — Binding a Scope by string ID (cross-script pattern)
-- CharacterManager creates the Track:
local charTrack = Reaper.Track(character):Configure("Char_" .. userId, 0)
-- CombatManager creates the Scope independently:
local combatScope = Reaper.Scope("Combat_" .. userId)
-- CharacterManager binds by ID without needing the live ScopeObject reference:
charTrack:HandleScope("Combat_" .. userId)
ScopeObject Methods
:BindToTrack
Submits a Scope to be owned by a physical Track.
Scenario A — Standard bind from the Scope's perspective
local playerTrack = Reaper.Track(player):Configure("Player_" .. userId, 0)
local uiScope = Reaper.Scope("UI_" .. userId)
uiScope:Connect(screenGui.Activated, onActivated)
-- Bind this Scope to the player Track; cleaned when the player Track is cleaned
uiScope:BindToTrack(playerTrack)
Scenario B — Binding after fully populating the Scope
local inventoryScope = Reaper.Scope("Inventory_" .. userId)
for _, slot in inventoryFrame:GetChildren() do
inventoryScope:Connect(slot.MouseButton1Click, function()
onSlotClicked(slot)
end)
end
-- Bind last, after all connections are established
local playerTrack = Reaper.Get("Player_" .. userId)
if playerTrack then
inventoryScope:BindToTrack(playerTrack)
end
:Unchain
Removes a child from a Scope without destroying it.
Scenario A — Releasing a connection to transfer it to a new owner
local scopeA = Reaper.Scope("ScopeA")
local conn = workspace.ChildAdded:Connect(onChildAdded)
scopeA:Chain(conn)
-- Transfer ownership: release from A and give to B
local scopeB = Reaper.Scope("ScopeB")
scopeA:Unchain(conn) -- conn is still alive, just no longer owned by scopeA
scopeB:Chain(conn) -- scopeB is now the owner
Scenario B — Removing a conditional child before cleanup
local sessionScope = Reaper.Scope("Session_" .. userId)
local tempEffect = createParticleEffect()
sessionScope:Chain(tempEffect)
-- The effect was already manually destroyed; unchain to prevent double-teardown
tempEffect:Destroy()
sessionScope:Unchain(tempEffect)
:CleanChild
Immediately destroys a specific child without waiting for the Scope to be cleaned.
Scenario A — Destroying a temporary effect early
local roundScope = Reaper.Scope("Round_" .. roundId)
local countdown = createCountdownUI()
roundScope:Chain(countdown)
-- Round started; countdown UI is no longer needed
roundScope:CleanChild(countdown)
-- countdown is destroyed and removed from roundScope's child list
-- roundScope itself is still alive and tracking other children
Scenario B — Conditionally cleaning one of several chained connections
local adminScope = Reaper.Scope("Admin_" .. userId)
local kickConn = kickButton.Activated:Connect(onKickClicked)
local banConn = banButton.Activated:Connect(onBanClicked)
adminScope:Chain(kickConn)
adminScope:Chain(banConn)
-- Player lost admin; disable ban ability but keep kick
adminScope:CleanChild(banConn) -- banConn is disconnected and removed
-- kickConn is still alive and owned by adminScope
:Connect
Connects a signal and automatically adds the connection to this Scope's child list.
Scenario A — Replacing manual connection management
-- Before Reaper: manual tracking required
local conn = character.Humanoid.Died:Connect(onDied)
-- (must remember to disconnect conn later...)
-- With Reaper Scope: automatic lifecycle
local charScope = Reaper.Scope("CharScope_" .. userId)
charScope:Connect(character.Humanoid.Died, onDied)
-- Disconnected automatically when charScope is cleaned
Scenario B — Connecting a custom signal table
local mySignal = Relay.Create("CustomEvent") -- a Relay signal object
local eventScope = Reaper.Scope("EventScope")
eventScope:Connect(mySignal, function(data)
processData(data)
end)
-- Works with any table that has a :Connect(callback) method
:Spawn
Spawns a coroutine and adds it to this Scope's child list. The coroutine is registered before it begins executing, so cleanup is safe regardless of timing.
Scenario A — Running a polling loop tied to a player session
local sessionScope = Reaper.Scope("Session_" .. userId)
sessionScope:Spawn(function()
while true do
task.wait(1)
syncPlayerData(player)
end
end)
-- When sessionScope is cleaned, the thread is cancelled
-- No dangling loop continues after the session ends
Scenario B — Spawning with arguments
local animScope = Reaper.Scope("Anim_" .. userId)
animScope:Spawn(function(character, speed)
playRunAnimation(character, speed)
end, player.Character, 16)
-- Arguments are forwarded to the coroutine on first resume
:Defer
Defers a callback and adds the scheduled task to this Scope's child list.
Scenario A — Deferring a UI update to the end of the current frame
local uiScope = Reaper.Scope("UI_" .. userId)
uiScope:Defer(function()
-- Runs after the current resume cycle completes
refreshInventoryUI(player)
end)
-- If uiScope is cleaned before this frame ends, the deferred task is cancelled
Scenario B — Deferring initialisation that depends on other systems
local initScope = Reaper.Scope("Init")
initScope:Defer(function()
-- All synchronous setup has run; safe to initialise dependent systems now
notifySystemsReady()
end)
:Delay
Schedules a delayed task and adds it to this Scope's child list.
Scenario A — Auto-expiring a timed buff
local buffScope = Reaper.Scope("SpeedBuff_" .. userId)
applySpeedBuff(player, 1.5)
-- Remove buff after 10 seconds; also cancel if buffScope is cleaned early
buffScope:Delay(10, function()
removeSpeedBuff(player)
Reaper.Clean("SpeedBuff_" .. userId)
end)
Scenario B — Delayed despawn of a temporary effect
local fxScope = Reaper.Scope("HitFX_" .. hitId)
fxScope:Chain(effectPart)
-- Despawn after 2 seconds regardless of game state
fxScope:Delay(2, function()
Reaper.Clean("HitFX_" .. hitId)
end)
:Repeat
Creates a repeating scheduled task and adds it to this Scope's child list.
Strict Dependency: Assignment Library
:Repeat() requires the Assignment library to be injected. Without it, this method is a no-op.
Scenario A — Periodic leaderboard sync during a match
local matchScope = Reaper.Scope("Match_" .. matchId)
-- Sync leaderboard every 5 seconds for the duration of the match
matchScope:Repeat(-1, 5, function(iteration, handle)
syncLeaderboard()
end)
-- Automatically cancelled when matchScope is cleaned at match end
Scenario B — Running a fixed-count animation burst
local animScope = Reaper.Scope("BurstAnim_" .. userId)
-- Flash the character 5 times with a 0.2s interval
animScope:Repeat(5, 0.2, function(i, handle)
character.Humanoid.RootPart.Transparency = (i % 2 == 0) and 0 or 0.5
end)
Global Lifecycle Signals
Reaper.Signals.OnTracked, OnCleaned, OnRemoved
Strict Dependency: Relay Library
These signals are no-op stubs until Relay is injected. Connect only after calling Reaper.Inject("Relay", Relay).
Scenario A — Logging all tracked items for debugging
Reaper.Signals.OnTracked:Connect(function(item, classification, assignId)
print(string.format("[Reaper] Tracked: %s | Class: %s | ID: %s",
tostring(item), classification, assignId or "none"))
end)
Scenario B — Observing cleanups for analytics
-- OnCleaned fires after the teardown action has run.
-- The item reference is available but may no longer be in a valid state.
local cleanCount = 0
Reaper.Signals.OnCleaned:Connect(function(item, classification)
cleanCount += 1
if classification == "Instance" then
-- Read only metadata that does not require the Instance to be alive
print(string.format("[Reaper] Instance destroyed (total cleans: %d)", cleanCount))
end
end)
Scenario C — Reacting to evictions from an Object Pool
Reaper.Signals.OnRemoved:Connect(function(item, classification)
if classification == "Instance" then
-- An Instance was released (protected); return it to the pool
-- It is still fully intact when OnRemoved fires
objectPool:Release(item :: Instance)
end
end)
Scenario D — Using :Once() for a one-shot observation
-- Wait for the very first item to be cleaned, then stop listening
Reaper.Signals.OnCleaned:Once(function(item, classification)
print("First item cleaned:", classification)
-- Connection is automatically disconnected after this fires once
end)
Scenario E — Using :Wait() to yield until an eviction occurs