Quick answer: Your data asset changes are not persisting because the package is not marked dirty. Call Modify() on the asset before changing properties, then explicitly save the package with UPackage::SavePackage. Simply editing values in memory does not trigger a save.

You modify a UDataAsset at runtime or through an editor utility, everything looks correct in the editor, and then you restart — all your changes are gone. This is one of the most frustrating Unreal Engine issues because there’s no error message. The asset simply reverts to its previous state. Here’s why it happens and how to make your changes stick.

Understanding Unreal’s Dirty Package System

Unreal Engine uses a dirty-flag system for asset persistence. When you modify a property through the editor’s Details panel, the editor automatically marks the containing package as dirty. The asterisk (*) that appears next to the asset name in the tab title is the visual indicator. When you press Ctrl+S, only dirty packages are saved.

When you modify an asset programmatically — through C++ code, a Blueprint utility, or an editor script — you bypass the editor’s property change tracking. The asset is modified in memory, but the package is never flagged as dirty. Ctrl+S skips it entirely because Unreal thinks nothing changed. The fix is to call Modify() before making changes.

// WRONG: modifying without marking dirty
void UMyEditorUtility::UpdateDataAsset(UMyDataAsset* Asset)
{
    Asset->DamageMultiplier = 2.5f;  // Changed in memory only
    // No save — change will be lost on restart
}

// CORRECT: mark dirty, then modify, then save
void UMyEditorUtility::UpdateDataAsset(UMyDataAsset* Asset)
{
    Asset->Modify();  // Marks the outer package dirty
    Asset->DamageMultiplier = 2.5f;

    UPackage* Package = Asset->GetOutermost();
    FString PackageFileName = FPackageName::LongPackageNameToFilename(
        Package->GetName(), FPackageName::GetAssetPackageExtension());
    UPackage::SavePackage(Package, Asset, *PackageFileName,
        FSavePackageArgs());
}

Editor Transactions vs. Saving

A common misconception is that wrapping changes in an FScopedTransaction will cause them to persist. Transactions serve the undo/redo system, not the save system. They record property changes so the user can press Ctrl+Z to revert them, but they do not write anything to disk.

// This enables undo/redo but does NOT save to disk
{
    FScopedTransaction Transaction(LOCTEXT("UpdateAsset", "Update Data Asset"));
    Asset->Modify();  // Required for both undo AND dirty flag
    Asset->Health = 200;
}
// Package is now dirty and undoable, but still needs explicit save

The Modify() call is the critical step shared by both systems. It notifies Unreal that the object is about to change, which both registers the pre-change state for undo and marks the package dirty for saving. If you skip Modify(), you get neither undo support nor dirty-flag marking.

Runtime vs. Editor Modifications

Changes made to data assets during Play-in-Editor (PIE) mode are intentionally discarded when you stop playing. This is by design — Unreal does not want gameplay logic to permanently alter your game data. If you need runtime modifications to persist, you must either save them outside of PIE or use a different storage mechanism like save game files.

For editor utilities that run outside PIE, the standard approach is to call Modify() and then prompt the user to save, or save programmatically. If you’re building a batch processing tool that modifies many assets, collect all dirty packages and save them in one pass at the end.

// Batch-saving multiple modified data assets
TArray<UPackage*> PackagesToSave;

for (UMyDataAsset* Asset : AssetsToModify)
{
    Asset->Modify();
    Asset->RebalancedValue = CalculateNewValue(Asset);
    PackagesToSave.AddUnique(Asset->GetOutermost());
}

// Save all dirty packages at once
FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave,
    /*bCheckDirty=*/false,
    /*bPromptToSave=*/true);

Source Control Complications

If your project uses Perforce, saving will fail silently if the .uasset file is not checked out. Unreal will mark the package dirty in memory, but SavePackage will return false because the file is read-only on disk. Always check out files before modifying them, or use Unreal’s source control API to handle checkout automatically.

With Git, this is less of an issue because files are not read-only by default. However, if you have Git LFS configured and the file is locked by another team member, you will hit a similar problem when you try to push. The save itself will succeed locally, but collaboration issues can lead to confusing merge conflicts in binary .uasset files.

When in doubt, verify that the file on disk actually changed by checking its modification timestamp or running a diff in your source control tool. This tells you definitively whether SavePackage succeeded or failed silently.

Modify() first, SavePackage second, verify on disk third.