Recordings in Change History Service

Hi Plugin Developers,

Today we are introducing an evolution of the ChangeHistoryService API, which will make undo handling in Studio more robust and open up additional undo-related capabilities. Please review the new API and consider taking the time to migrate your plugins.

What’s Changing

Previously you would call ChangeHistoryService:SetWaypoint after making changes to the DataModel.

Now you

Basic usage looks like this:

local CHS = game:GetService("ChangeHistoryService")
local recording = CHS:TryBeginRecording("MyAction")

if recording then
  -- Take your action here
  CHS:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)

Why the new API?

Studio is transitioning to a richer model for undo handling, where changes to the DataModel are placed into explicitly specified buckets called “recordings” rather than groups of changes being implicitly specified via waypoints. The new API exposes this new undo model.

This will help Studio know what actions are currently being performed against the DataModel, and by which source. With this additional information, Studio can better coordinate between your plugins, other plugins, and built-in Studio behaviors.

Recordings mean Studio can differentiate between long-running actions like terrain generation and other actions you take in the meantime. They also let you express that you canceled an action without it affecting the DataModel or extended a prior action. Here are a few examples of things we couldn’t do with the old API which we will be able to do once plugins begin migrating to the new API:

  • Immediately warn developers when they fail to use recordings in their plugin.
  • Use the cancellation mechanics to not mark packages as modified after canceling previewed changes to them.
  • Avoid auto-saving in the middle of a plugin performing a long-running action such as dragging or generating terrain.


It will take time for both your plugins and our internal Studio code to migrate towards this new Recording API, and there will be a transitional period where both APIs are in use. Here’s our planned timeline for the migration:

  • Now

    • You can update your plugins to use the Recording API.
    • We will begin updating Studio to internally use the Recording API.
  • In the coming months

    • SetWaypoint will be deprecated assuming a smooth migration.
    • Calls to SetWaypoint will warn that you should migrate to the new API.
    • Studio systems such as the Package system will begin relying on the new API and possibly have degraded interactions with plugins that haven’t migrated.
  • Long term:

    • We want to internally replace the Waypoint system entirely with the Recording system. We’ll evaluate how to proceed on an ongoing basis depending on developer feedback and how the migration is progressing.

If you notice any issues adopting the new API. Please fill a bug in the bug reports section, or send a message to our @Bug-Support team. Since this new API and the existing Waypoints API exist in parallel for the time being, we want to make sure that your adoption goes smoothly.

Finally, shout-out to the many folks in the Creator Collaboration Team who made this work possible:

@Mr_Purrsalot, @Rusi_002, @yipiokay, @tnavarts, @moodymandymeow, @cruiser_forever


This topic was automatically opened after 10 minutes.

This update has a lot of potential! With this change, will studio command bar actions finally have automatic recordings?


What a good thing to hear! Will there be support for Undo/Redo when selecting Instances in Roblox Studio?


Why is it named TryBeginRecording instead of BeginRecording? Why does it have to try and probably fail instead of succeeding?


This is awesome! Excited to see plugins being able to properly undo things. I’ve had a few errors where some plugins deleted some of my instances and I couldn’t undo it. So I hope this will help a lot of plugin developers improve their creations :smiley:


This update will be useful for my upcoming plugins, Thank you roblox!


Naming convention. Try implies the method does not always work (because it doesn’t)

At OP: it would be nice to have a method to figure out if the recording is available, its more useful than prodding TryBeginRecording in the hope that its available IsRecordingInProgress


Not yet. We have considered this. The challenge is that you can create arbitrary callbacks from the command bar context. Examples like:

  function(a, b, Value, c) game.Workspace.Path.To.Instance.Property = Value; end)

Making special cases for the command bar is something we considered for the future. It’s just more complicated than wrapping the simple synchronous cases.


With SetWaypoint, if there are no changes to the DataModel, no “waypoint” is created. With these new methods, does FinishRecording create one even if no changes are made to the DataModel?


Why wouldn’t it work. Might be optional to use as a BETA feature but after that?


Only one thing can record at any time


Are there any internal restrictions here when it comes to yielding threads, does the recording have to complete in a single resumption, or can it wait before finishing


After thinking about this a bit more… what would happen if a plugin unloads while it’s in the middle of yielding for an action and the thread with FinishRecording is canceled? Wouldn’t it be possible for one plugin to ruin multiple other plugins if it doesn’t finish its recording with FinishRecording?


In the near future, you can think of this as the same consequence of not calling CHS:SetWaypoint to capture your change in flight into the undo stack. We did not change the underlying mechanism enough with this API addition to isolate DM changes to a given recording by context (though that is one of the points of doing this whole operation).


Now I get to open up some plugins and update them, fun.

I’m not entirely sure how this is going to benefit any of my plugins in any way, but hopefully it manages to benefit someone out there.


I loved how easy it was to adapt this to my plugin


Can someone make a Plugin, type game.Players:CreateLocalPlayer() in the console. Then somehow record the deletion of the Player that was created, and see if you can duplicate the Player somehow?


Based on that argument, wouldn’t every single function ever be a Try method? things are expected to work, everyone knows of the possibility of failure, you’ll learn how probable success is in a tool-by-tool basis, this naming convention implies in expecting common failures as part of the feature, like retrieving specific data based on an inputted path, if the path is incorrect it fails, but a function that just explicitly starts a save has no logical need for this, it’s not a “try” as in “it’s a guess if it’ll work”, it should be working, and if it fails, just do an error message and that’s it.

Does Publish or Import Assets also need an “Try” prefix? those functions fail a third of the time, nonetheless, they’re expected to work, not working is an occasional failure.


The HTTP methods error. This in itself is proof that you expect them to work. Meanwhile, TryBeginRecording returns nil when it doesn’t work, which means the engine resolves the conflict error for you. There’s an internal method, TryInstallPlugin, with the same convention here.

Obviously, there’s RequestAsync, but this too errors if there’s an issue with the request (not the response)