Events
Signals around profile load, save, session release, and legacy migration results.
Data Manipulation Guidelines
BeforeRelease, BeforeSave & ProfileLoaded: Use PlayerState functions for data changes (recommended) as they provide validation, leaderstats sync, and proper error handling. Direct data manipulation via the data parameter bypasses these safeguards.
ProfileUnloaded: PlayerState functions are disabled. Only use direct data access via the data parameter for read-only operations or external system synchronization.
| Event | One-liner |
|---|---|
| BeforeRelease() | Leave/shutdown hook before profile session release; does not fire on . |
| BeforeSave() | Hook before save; fires on , player leave, and server shutdown. |
| ProfileLoaded() | Player data ready after join. |
| ProfileUnloaded() | Session ended after release; read-only cleanup. |
| MigrationResult | Legacy migration completed for a player. |
BeforeRelease()
BeforeRelease:Connect(listener)
Fired immediately before a player's profile session is released (player leave or server shutdown). Use this for leave-only logic that must write final data into the profile before it is saved and unlocked.
| Name | Type | Description |
|---|---|---|
listener | (player: | Callback invoked before session release. |
Returns: Connection — disconnect with :Disconnect() to remove the listener.
When it fires
| Trigger | Fires? |
|---|---|
Player leaves (Players.PlayerRemoving) | Yes |
Server shutdown (game:BindToClose) | Yes |
PlayerState. (manual save hook) | No |
| ProfileStore periodic autosave | No |
Session stolen externally (OnSessionEnd from another server) | No (use ProfileUnloaded) |
| Player left before profile finished loading | No (no loaded profile) |
When it does NOT fire
- Manual saves via
— use BeforeSave() for that.SaveData() - Mid-session autosaves from ProfileStore — neither
BeforeReleasenorBeforeSavefire on those (onlyfiresSaveData()BeforeSave). - After the profile session has ended — use
ProfileUnloadedfor post-release cleanup (read-only recommended).
Leave / shutdown order
On player leave or BindToClose, events run in this order:
- BeforeRelease — leave-only final mutations
- BeforeSave — also runs on leave (backward compatible)
- Batch flush / leaderboard updates (internal)
EndSession()— profile released and saved to DataStore- ProfileUnloaded — session ended (profile no longer active)
On leave, BeforeRelease always runs before BeforeSave.
Profile state during the callback
- The profile is still active (
profile:IsActive()istrue). PlayerState.GetPath,SetPath,Set, etc. work normally.- Prefer mutating via
PlayerState.SetPath/Set(validation + leaderstats sync) rather than editingdatadirectly.
Example
local PlayerState = require(ReplicatedStorage.Libraries.PlayerState.PlayerStateServer)
PlayerState.BeforeRelease:Connect(function(player, data)
-- Leave-only: e.g. scan world for unplaced packages and persist to profile
local unplacedPackages = scanWorldPackagesForPlayer(player)
PlayerState.SetPath(player, "Inventory.UnplacedPackages", unplacedPackages)
end)Note
Fired only before session release (player leave or server shutdown), not on . On leave, runs before BeforeSave(). Use PlayerState functions for data changes (recommended) as they provide validation and leaderstats sync. Do not rely on SaveData()Players.PlayerRemoving + GetPath for final saves — the profile may already be released by then.
Comparison: lifecycle events
| Event | Typical use | Fires on | Fires on leave | Profile writable |
|---|---|---|---|---|
| BeforeRelease | Leave-only final writes (world → data) | No | Yes (before BeforeSave) | Yes |
| BeforeSave | Hooks before save / manual save side effects | Yes | Yes (after BeforeRelease) | Yes |
| ProfileLoaded | Player data ready after join | No | No | Yes |
| ProfileUnloaded | Session ended; cleanup / analytics read | No | Yes (after release) | No (session inactive) |
Common mistakes
- Using
BeforeSavefor leave-only logic — also runs when you callduring gameplay (purchases,SaveData()jobId, etc.). - Using
Players.PlayerRemoving+GetPath— may run after PlayerState has calledEndSession()and cleared the profile →GetPathreturnsnil. - Using
ProfileUnloadedfor last-minute writes — session is already inactive; too late for reliableSetPath.
Recommended patterns
Leave-only world → data sync:
PlayerState.BeforeRelease:Connect(function(player, data)
-- write final state here
end)Logic that should run on manual saves and leave:
PlayerState.BeforeSave:Connect(function(player, data)
-- e.g. refresh jobId on SaveData() and on leave
end)Read-only analytics after session ends:
PlayerState.ProfileUnloaded:Connect(function(player, data)
-- use `data` argument; do not use GetPath
end)BeforeSave()
BeforeSave:Connect(listener)
Fired before player data is saved, allowing for final data modifications.
| Name | Type | Description |
|---|---|---|
listener | (player: | Callback invoked before save. |
Returns: Connection — disconnect with :Disconnect() to remove the listener.
Fires on: , player leave, and server shutdown.SaveData()
Does not fire on: ProfileStore's internal periodic autosave (unless you call yourself).SaveData()
Example
PlayerState.BeforeSave:Connect(function(player, data)
-- Use PlayerState functions for data changes (recommended)
PlayerState.Set(player, "LastSaveTime", os.time())
PlayerState.SetPath(player, "Stats.TotalPlayTime", data.Stats.PlayTime or 0)
-- Direct data manipulation bypasses validation (use sparingly)
data.FinalCoins = data.Coins -- Direct access for read-only operations
print(`Saving data for {player.Name}`)
end)Note
On player leave and server shutdown, BeforeSave still fires, but BeforeRelease() runs first. Use BeforeRelease for logic that should only run when the session is about to end (not on ). SaveData()BeforeSave also fires when you call during gameplay.SaveData()
Use PlayerState functions for data changes (recommended) as they provide validation and leaderstats sync.
ProfileLoaded()
ProfileLoaded:Connect(listener)
Fired when a player's profile data has been loaded and is ready for use.
Parameters: listener: (player: Player, data: PlayerData) -> ()
Returns: Connection - Event connection for disconnecting
Example
PlayerState.ProfileLoaded:Connect(function(player, data)
-- Use PlayerState functions for data changes (recommended)
PlayerState.Set(player, "JoinTime", os.time())
PlayerState.SetPath(player, "Stats.Logins", (data.Stats.Logins or 0) + 1)
-- Read data for initialization logic
print(`{player.Name}'s data loaded with {data.Coins} coins`)
-- Setup player-specific features
setupPlayerUI(player)
end)Note
Fired after PlayerState. completes successfully. PlayerState functions work normally - use them for data changes (recommended) for validation and proper sync.Init()
ProfileUnloaded()
ProfileUnloaded:Connect(listener)
Fired when a player's profile session ends and data is being unloaded.
Parameters: listener: (player: Player, data: PlayerData) -> ()
Returns: Connection - Event connection for disconnecting
Example
PlayerState.ProfileUnloaded:Connect(function(player, data)
-- Cleanup player-specific systems
print(`{player.Name}'s profile unloaded`)
-- Direct data access only - PlayerState functions don't work here
local finalCoins = data.Coins
local playTime = data.Stats.PlayTime or 0
-- Save to external systems if needed
saveToAnalytics(player.UserId, finalCoins, playTime)
end)Note
Fired when profile session ends (after EndSession() on leave or shutdown). PlayerState functions do NOT work during this event - only direct data access via the data parameter is available for read-only operations. For leave-only writes before release, use BeforeRelease() instead.
MigrationResult
MigrationResult:Connect(listener)
Fired when legacy migration completes for a player. Use for logging, analytics, or debugging migration behavior.
Parameters: listener: (player: Player, result: MigrationResult, data: PlayerData) -> ()
Returns: Connection - Event connection for disconnecting
Result fields: applied, legacyFound, markerWritten, keyUsed, strategy, markerPath, continuous, reason
Example
PlayerState.MigrationResult:Connect(function(player, result, data)
print("Migration for", player.Name, result.applied, result.reason, result.strategy, result.keyUsed)
end)See Legacy Data Migration for full documentation.