Skip to content

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.

EventOne-liner
BeforeRelease()Leave/shutdown hook before profile session release; does not fire on SaveData().
BeforeSave()Hook before save; fires on SaveData(), player leave, and server shutdown.
ProfileLoaded()Player data ready after join.
ProfileUnloaded()Session ended after release; read-only cleanup.
MigrationResultLegacy 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.

NameTypeDescription
listener(player: Player, data: PlayerData) -> ()Callback invoked before session release.

Returns: Connection — disconnect with :Disconnect() to remove the listener.

When it fires

TriggerFires?
Player leaves (Players.PlayerRemoving)Yes
Server shutdown (game:BindToClose)Yes
PlayerState.SaveData(player) (manual save hook)No
ProfileStore periodic autosaveNo
Session stolen externally (OnSessionEnd from another server)No (use ProfileUnloaded)
Player left before profile finished loadingNo (no loaded profile)

When it does NOT fire

Leave / shutdown order

On player leave or BindToClose, events run in this order:

  1. BeforeRelease — leave-only final mutations
  2. BeforeSave — also runs on leave (backward compatible)
  3. Batch flush / leaderboard updates (internal)
  4. EndSession() — profile released and saved to DataStore
  5. ProfileUnloaded — session ended (profile no longer active)

On leave, BeforeRelease always runs before BeforeSave.

Profile state during the callback

Example
lua
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 SaveData(). On leave, runs before BeforeSave(). Use PlayerState functions for data changes (recommended) as they provide validation and leaderstats sync. Do not rely on Players.PlayerRemoving + GetPath for final saves — the profile may already be released by then.

Comparison: lifecycle events

EventTypical useFires on SaveData()Fires on leaveProfile writable
BeforeReleaseLeave-only final writes (world → data)NoYes (before BeforeSave)Yes
BeforeSaveHooks before save / manual save side effectsYesYes (after BeforeRelease)Yes
ProfileLoadedPlayer data ready after joinNoNoYes
ProfileUnloadedSession ended; cleanup / analytics readNoYes (after release)No (session inactive)

Common mistakes

  • Using BeforeSave for leave-only logic — also runs when you call SaveData() during gameplay (purchases, jobId, etc.).
  • Using Players.PlayerRemoving + GetPath — may run after PlayerState has called EndSession() and cleared the profile → GetPath returns nil.
  • Using ProfileUnloaded for last-minute writes — session is already inactive; too late for reliable SetPath.

Leave-only world → data sync:

lua
PlayerState.BeforeRelease:Connect(function(player, data)
    -- write final state here
end)

Logic that should run on manual saves and leave:

lua
PlayerState.BeforeSave:Connect(function(player, data)
    -- e.g. refresh jobId on SaveData() and on leave
end)

Read-only analytics after session ends:

lua
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.

NameTypeDescription
listener(player: Player, data: PlayerData) -> ()Callback invoked before save.

Returns: Connection — disconnect with :Disconnect() to remove the listener.

Fires on: SaveData(), player leave, and server shutdown.

Does not fire on: ProfileStore's internal periodic autosave (unless you call SaveData() yourself).

Example
lua
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 SaveData() during gameplay.

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
lua
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.Init() completes successfully. PlayerState functions work normally - use them for data changes (recommended) for validation and proper sync.


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
lua
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
lua
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.

PlayerState - High-Performance Roblox Data Management