Write Once, Click Never: Automating Repetitive Unity Tasks with Editor Scripts
Write Once, Click Never: Automating Repetitive Unity Tasks with Editor Scripts
by Owen Briggs
09.13.2026

If you’ve spent any real time in Unity, you’ve probably caught yourself doing the same manual steps over and over — resetting transform values on a dozen objects before a build, renaming a batch of prefabs one by one, or running a five-step scene setup that you’ve memorized but never written down. Unity Editor scripting in C# lets you turn those workflows into a single menu click. This article walks you through the patterns you need: MenuItem, EditorWindow, Selection, AssetDatabase, and async-aware edit-mode patterns, all grounded in concrete, copy-pasteable examples.

A Unity Editor script is a C# script placed inside an Editor folder in your project that has access to the UnityEditor namespace. It compiles only in the Unity Editor, gets stripped from player builds automatically, and lets you extend the Unity interface with custom menus, windows, and batch operations. Editor scripts are tools for you and your team, not for players.

What You’ll Learn

  • Build a MenuItem utility that resets selected Transforms in under 15 lines
  • Batch-process GameObjects in the current selection using Selection and Undo.RecordObject
  • Create a dockable EditorWindow with real UI controls for more complex tools
  • Run async code in edit mode without coroutines using EditorApplication.update and C# Tasks
  • Avoid the domain reload, dirty-state, and undo pitfalls that break editor tools in practice

All examples in this article were tested against Unity 2022.3 LTS. The APIs shown are stable across recent LTS releases, but always check the Unity Scripting API reference if you’re on an older version.

What Unity Editor Scripting Actually Is

Editor scripts live in a folder named Editor anywhere in your Assets directory. Unity’s compilation pipeline treats that folder specially: scripts inside it can reference the UnityEditor namespace, and they’re excluded from player builds entirely. You don’t need a preprocessor guard to keep them out of builds — the folder handles that. But if you reference editor-only APIs from a script outside an Editor folder, you do need a #if UNITY_EDITOR guard, or your build will fail.

The UnityEditor namespace gives you APIs that have no runtime equivalent. AssetDatabase lets you query, load, and save project assets. EditorUtility provides progress bars, dirty-marking, and dialog boxes. Selection tells you what the user has highlighted in the Hierarchy or Project window. Undo registers operations so Ctrl+Z works correctly after your script runs. None of these exist at runtime, which is why the folder separation matters.

The key mental shift is that editor scripts are tools for your workflow, not game logic. Think of them the way you’d think of a build script or a code generator. They run inside Unity’s process, they can touch your scene and assets, and they can save you hours of clicking every week.

Adding Menu Items to Automate Tasks

How the [MenuItem] Attribute Works

The [MenuItem] attribute registers a static method as a Unity menu entry. You can add items under Tools, Assets, GameObject, or any custom top-level menu path. The method must be static and return void. That’s the whole setup.

Here’s a working example that resets every selected Transform to identity values — position zero, rotation zero, scale one. Drop this into any script inside an Editor folder and you’ll see the menu entry appear immediately.

using UnityEditor;
using UnityEngine;

public class TransformResetTool
{
    // Register this method as a menu item under Tools > Reset Selected Transforms
    [MenuItem("Tools/Reset Selected Transforms")]
    private static void ResetSelectedTransforms()
    {
        // Grab every GameObject the user has selected in the Hierarchy
        GameObject[] selected = Selection.gameObjects;

        // Iterate and reset each transform
        foreach (GameObject go in selected)
        {
            // Register the transform change with Unity's undo system before modifying it
            Undo.RecordObject(go.transform, "Reset Transform");
            go.transform.localPosition = Vector3.zero;
            go.transform.localRotation = Quaternion.identity;
            go.transform.localScale = Vector3.one;
        }
    }

    // Validation method: the menu item is grayed out if nothing is selected
    [MenuItem("Tools/Reset Selected Transforms", true)]
    private static bool ValidateResetSelectedTransforms()
    {
        return Selection.gameObjects.Length > 0;
    }
}

The second [MenuItem] overload with true as the second argument registers a validation method. Unity calls it before drawing the menu and grays out the item if it returns false. This is a small touch that makes your tools feel intentional rather than half-finished.

Step-by-Step: Creating Your First Editor Script

  1. In your Unity project’s Assets folder, create a subfolder named Editor if one doesn’t exist.
  2. Right-click inside Editor and choose Create > C# Script. Name it TransformResetTool.
  3. Replace the default MonoBehaviour content with the snippet above.
  4. Return to Unity and wait for compilation. The Tools menu will now contain your new entry.
  5. Select one or more GameObjects in the Hierarchy, then click Tools > Reset Selected Transforms.

The Undo.RecordObject call on line 14 is the one you absolutely cannot skip. Without it, your users (and you, the next morning) have no way to undo the operation. Always call it before you modify any object, not after.

Working with Selection and AssetDatabase in Batch Operations

Reading the Current Selection

Selection.gameObjects returns every GameObject selected in the Hierarchy. Selection.objects is broader and includes assets selected in the Project window. For batch scene operations, you’ll usually want gameObjects. For asset-level operations like batch renaming prefabs, you’ll want objects filtered by type.

Here’s a batch rename tool that prefixes every selected GameObject’s name with a string you define at the top of the method. This is the kind of thing that takes 30 seconds to write and saves you five minutes every time you restructure a scene hierarchy.

using UnityEditor;
using UnityEngine;

public class BatchRenameTool
{
    // Prefix all selected GameObjects with a shared label
    [MenuItem("Tools/Prefix Selected GameObjects")]
    private static void PrefixSelectedObjects()
    {
        string prefix = "ENV_";
        GameObject[] selected = Selection.gameObjects;

        // Group the rename into a single undo operation
        Undo.SetCurrentGroupName("Batch Rename");
        int undoGroup = Undo.GetCurrentGroup();

        foreach (GameObject go in selected)
        {
            Undo.RecordObject(go, "Rename " + go.name);
            go.name = prefix + go.name;
        }

        // Collapse all individual undo records into one Ctrl+Z step
        Undo.CollapseUndoOperations(undoGroup);
    }

    [MenuItem("Tools/Prefix Selected GameObjects", true)]
    private static bool ValidatePrefixSelected()
    {
        return Selection.gameObjects.Length > 0;
    }
}

The Undo.CollapseUndoOperations call groups all the individual renames into a single undo step. Without it, the user would need to hit Ctrl+Z once per object. That gets annoying fast on a selection of 40 objects.

Querying Assets with AssetDatabase

AssetDatabase.FindAssets lets you search the project by type, label, or name pattern without opening the Project window. The filter string "t:Prefab" returns all prefab GUIDs in the project. You can then load each one with AssetDatabase.LoadAssetAtPath. After modifying an asset, call EditorUtility.SetDirty(asset) and then AssetDatabase.SaveAssets() to write changes to disk. Skipping either of those calls means your changes exist only in memory until Unity reloads, and they may not persist at all.

Building a Custom Editor Window

Extending EditorWindow

A MenuItem is great for one-shot operations, but sometimes you need a persistent panel with configuration fields and a Run button. That’s where EditorWindow comes in. You extend it, implement OnGUI, and open it via a static factory method.

using UnityEditor;
using UnityEngine;

public class SceneSetupWindow : EditorWindow
{
    // These fields are serialized so they survive domain reloads
    [SerializeField] private string spawnTag = "Untagged";
    [SerializeField] private int spawnCount = 5;

    // Open the window from a menu item
    [MenuItem("Tools/Scene Setup Window")]
    public static void ShowWindow()
    {
        GetWindow<SceneSetupWindow>("Scene Setup");
    }

    private void OnGUI()
    {
        GUILayout.Label("Scene Setup Tool", EditorStyles.boldLabel);

        // Draw a text field for the tag to assign
        spawnTag = EditorGUILayout.TagField("Assign Tag", spawnTag);

        // Draw an int field for how many objects to create
        spawnCount = EditorGUILayout.IntField("Spawn Count", spawnCount);

        if (GUILayout.Button("Run Setup"))
        {
            RunSetup();
        }
    }

    private void RunSetup()
    {
        Undo.SetCurrentGroupName("Scene Setup");
        int group = Undo.GetCurrentGroup();

        for (int i = 0; i < spawnCount; i++)
        {
            // Create a new GameObject and register it with undo
            GameObject go = new GameObject("SetupObject_" + i);
            Undo.RegisterCreatedObjectUndo(go, "Create Setup Object");
            go.tag = spawnTag;
        }

        Undo.CollapseUndoOperations(group);
    }
}

Surviving Domain Reloads

Domain reloads happen every time Unity recompiles scripts. Any static field or in-memory state that isn't serialized gets wiped. For EditorWindow subclasses, marking fields with [SerializeField] tells Unity to preserve them across reloads. For static state outside a window, use SessionState to store simple values like strings and ints that you want to persist for the current editor session without writing to disk.

This is one of the trickier parts of editor scripting. You write a tool, it works great, you save a script, Unity recompiles, and suddenly your window has forgotten everything the user typed. Adding [SerializeField] to your window's fields fixes that in most cases.

Running Async Code in Edit Mode

Coroutines don't tick in edit mode. Update() doesn't run. Time is frozen. So if you need to do something async in an editor script — read a file, call a local API, process a large batch without freezing the UI — you need a different approach.

Using EditorApplication.update

EditorApplication.update is a delegate that Unity calls every editor frame, even outside play mode. You can subscribe a method to it, do a chunk of work per frame, and unsubscribe when done. It's the closest thing to a coroutine that edit mode offers.

using UnityEditor;
using System.Collections.Generic;

public class BatchProcessor
{
    private static Queue<string> _assetQueue;
    private static int _total;

    [MenuItem("Tools/Process Assets Async")]
    private static void StartProcessing()
    {
        // Populate a queue of asset GUIDs to process
        string[] guids = AssetDatabase.FindAssets("t:Texture2D");
        _assetQueue = new Queue<string>(guids);
        _total = guids.Length;

        // Subscribe to the editor update loop
        EditorApplication.update += ProcessNextAsset;
    }

    private static void ProcessNextAsset()
    {
        if (_assetQueue == null || _assetQueue.Count == 0)
        {
            // Clean up when done
            EditorUtility.ClearProgressBar();
            EditorApplication.update -= ProcessNextAsset;
            return;
        }

        string guid = _assetQueue.Dequeue();
        string path = AssetDatabase.GUIDToAssetPath(guid);

        // Show progress without blocking the editor
        float progress = 1f - ((float)_assetQueue.Count / _total);
        EditorUtility.DisplayProgressBar("Processing Assets", path, progress);

        // Do your per-asset work here
    }
}

C# Tasks and async/await in Edit Mode

C# Tasks with async/await do work in edit mode, and they're the right tool when you're calling something genuinely asynchronous like file I/O. The catch is that exceptions inside an unawaited Task are swallowed silently. Always await your async editor methods or attach a .ContinueWith that logs failures. Losing an exception in an editor tool is annoying; losing it silently is worse because you won't know the operation failed.

using UnityEditor;
using System.Threading.Tasks;
using System.IO;

public class AsyncEditorExample
{
    [MenuItem("Tools/Read Config Async")]
    private static async void ReadConfigAsync()
    {
        // Read a file without blocking the editor UI thread
        string path = "Assets/Config/setup.json";
        string content = await File.ReadAllTextAsync(path);
        UnityEngine.Debug.Log("Config loaded: " + content.Length + " chars");
    }
}

The method returns async void rather than async Task because MenuItem methods must return void. That means you can't await it from the outside, so make sure you handle exceptions inside the method body with a try/catch block.

Gotchas That Will Break Your Editor Tools

A few pitfalls come up constantly when building editor tools, and knowing them in advance saves a lot of head-scratching.

  • Forgetting EditorUtility.SetDirty: If you modify a ScriptableObject or a component on a prefab asset, Unity won't know to save it unless you call EditorUtility.SetDirty(target) followed by AssetDatabase.SaveAssets(). Changes that aren't marked dirty may appear to work but won't survive a Unity restart.
  • Skipping Undo.RecordObject: Any modification to a scene object without a prior Undo.RecordObject call is permanent from the user's perspective. This is the single most common way to make a team member distrust your tool.
  • Editor APIs in runtime code: If you reference UnityEditor classes from a script outside an Editor folder, your build breaks. Wrap those references in #if UNITY_EDITOR ... #endif guards. This gets tricky when you have a MonoBehaviour that uses editor-only helpers for debug visualization. Guard those blocks carefully.
  • Domain reload wiping static state: Static fields on non-window classes reset on every recompile. If your tool stores state in a static field, use [InitializeOnLoad] with a static constructor to reinitialize it, or move the state to SessionState.

A Practical End-to-End Example: Scene Audit Tool

This tool scans the open scene for objects with missing script components and lists them in an EditorWindow. Clicking a row in the list pings the object in the Hierarchy. It ties together EditorWindow, EditorGUILayout, and Selection in a tool you'd actually keep open during a project review.

using UnityEditor;
using UnityEngine;
using System.Collections.Generic;

public class SceneAuditWindow : EditorWindow
{
    private List<GameObject> _missingScriptObjects = new List<GameObject>();
    private Vector2 _scroll;

    [MenuItem("Tools/Scene Audit")]
    public static void ShowWindow()
    {
        GetWindow<SceneAuditWindow>("Scene Audit");
    }

    private void OnGUI()
    {
        if (GUILayout.Button("Run Audit"))
        {
            RunAudit();
        }

        GUILayout.Label("Objects with missing scripts: " + _missingScriptObjects.Count,
            EditorStyles.helpBox);

        _scroll = EditorGUILayout.BeginScrollView(_scroll);

        foreach (GameObject go in _missingScriptObjects)
        {
            if (GUILayout.Button(go.name))
            {
                // Ping the object in the Hierarchy so the user can find it
                EditorGUIUtility.PingObject(go);
                Selection.activeGameObject = go;
            }
        }

        EditorGUILayout.EndScrollView();
    }

    private void RunAudit()
    {
        _missingScriptObjects.Clear();

        // Find all GameObjects in the open scene
        GameObject[] allObjects = FindObjectsOfType<GameObject>();

        foreach (GameObject go in allObjects)
        {
            Component[] components = go.GetComponents<Component>();
            foreach (Component c in components)
            {
                // A null component in this array means a missing script reference
                if (c == null)
                {
                    _missingScriptObjects.Add(go);
                    break;
                }
            }
        }

        Repaint();
    }
}

The clickable rows that call EditorGUIUtility.PingObject are what make this tool genuinely useful rather than just informative. You get the list, you click the offending object, Unity highlights it in the Hierarchy. That's the kind of small UX detail that separates a tool you'll actually use from one you'll run once and forget.

Unity Editor Automation Approaches Compared

Approach Use Case Complexity Best For Key API
MenuItem One-shot operations on selection Low Batch resets, renames, tag assignments [MenuItem], Selection
EditorWindow Configurable, persistent tools Medium Setup wizards, audit panels, batch processors EditorWindow, EditorGUILayout
EditorUtility helpers Long-running background tasks Medium Asset processing, file imports, progress feedback EditorUtility, EditorApplication.update

Where to Take Your Editor Tooling Next

The patterns in this article compose well. A MenuItem can open an EditorWindow, which can trigger an async batch process using EditorApplication.update, which can write results back to assets via AssetDatabase. Once you have these building blocks, you can build tools that are genuinely complex without the code getting hard to follow.

The natural next step from here is PropertyDrawer and custom inspector work, which lets you control how individual components look in the Inspector rather than building standalone windows. After that, ScriptableObject-based configuration is worth exploring as a way to store tool settings that survive domain reloads cleanly, without relying on SessionState or static fields.

Which repetitive Unity task are you going to automate first? Drop a comment and let us know. Batch renaming, component validation, scene setup, prefab auditing — whatever it is, the patterns here should get you most of the way there on the first try.

Frequently Asked Questions

Why is my Editor script not showing in the menu?

The most common cause is that the script isn't inside an Editor folder. Create a folder named exactly Editor anywhere under Assets, move your script there, and recompile. If the script is already in an Editor folder and the menu still doesn't appear, check for compile errors in the Console — a single error anywhere in the project can prevent all scripts from compiling.

Can I use async/await in Unity Editor scripts?

Yes. C# Tasks and async/await work in edit mode. The main limitation is that MenuItem methods must return void, so you'll use async void rather than async Task. Always wrap the body in a try/catch block because exceptions in async void methods won't surface automatically.

What happens to my EditorWindow data when scripts recompile?

By default, in-memory state is wiped on domain reload. Mark your window's fields with [SerializeField] to preserve them across reloads. For simple primitive values you want to persist for a session without serialization, use SessionState.GetString and SessionState.SetString.

Do I need #if UNITY_EDITOR guards in my Editor scripts?

Not for scripts inside an Editor folder — the folder itself handles exclusion from builds. You do need #if UNITY_EDITOR guards when you reference UnityEditor APIs from a MonoBehaviour or other runtime script that lives outside an Editor folder.

How do I make sure changes to assets are saved to disk?

Call EditorUtility.SetDirty(asset) after modifying an asset, then call AssetDatabase.SaveAssets() to write all dirty assets to disk. For scene objects rather than project assets, marking the scene dirty with UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty is the right approach instead.

Owen Briggs