# Creating authoring commands *Authoring commands* are the subset of commands that **create or mutate project content** — assets, scenes, GameObjects, components — on behalf of an agent. They build on the base command API in [Creating commands](creating-commands.md); this page covers the four concerns that are specific to content authoring: - **The authoring root** — the sandbox that bare paths resolve against and writes are confined to. - **`ObjectRef`** — how a command *receives* a reference to an existing object. - **`AuthoringResult`** — how a command *returns* the identity of an object it created or touched. - **Undo/redo** — grouping a command's scene mutations into a single, revertible step. Together these let one command's output feed the next command's input, so an agent can chain calls (`create_gameobject` → `add_component` → `set_component_properties` → `create_prefab`) without ever handling a raw Unity object itself. If you want to support a content type the package doesn't cover yet (materials, audio, lighting, terrain, …), this is the pattern to follow. Read [Creating commands](creating-commands.md) first — this page assumes you know how `[CliCommand]`, `[CliArg]`, `MainThreadRequired`, and the response envelope work. ## Authoring vs. management commands Not every state-changing command is an *authoring* command. This page — and the [checklist](#checklist-for-a-new-authoring-command) at the end — applies to commands that **create or mutate project content** (assets, scenes, GameObjects, components) and hand the agent back an object identity. **Management commands** change *configuration* or *drive tooling* rather than author content: project settings (`Editor/Commands/ProjectSettings/`), builds (`Build/`), and packages (`PackageManager/`). They are a **different category** and deliberately do **not** follow the full authoring contract. They share only the cross-cutting safety conventions (the `confirm`/`dry_run` gate and, where relevant, `ObjectResolver`/`ProjectPaths`) — see [Safety & mutations](safety-and-mutations.md). | Concern | Authoring command | Management command | |---|---|---| | Object input | `ObjectRef` → `ObjectResolver.TryResolve` | same, when it references an object (e.g. a scene in the build list) | | Path input | `ProjectPaths.Resolve` (confined to the authoring root) | as needed; may be unconfined by design (e.g. a build output path) | | Undo | scene/object mutations wrapped in `AuthoringUndoScope` + registered `Undo` APIs | **none** — settings / AssetDatabase / UPM writes don't participate in Undo, so no scope; note `Not undoable via Ctrl+Z.` in the description instead | | Destructive gate | `confirm`/`dry_run` where destructive | `confirm`/`dry_run` where destructive (same convention) | | Return value | `AuthoringResult` via `ObjectResolver.Describe` | a domain model (e.g. `ProjectSettingsResponse`, `PackageMutationResponse`) or a plain result object | | Failure | **throw** (`ArgumentException` / `InvalidOperationException`) | throw, **or** return a structured `{ success = false, code, error }` object — consistent within the area | If you're adding a config/build/package command, follow the management column and the shared safety conventions; the checklist below is for content-authoring commands. ## The building blocks | Type | Namespace | Role | |------|-----------|------| | `ProjectPaths` | `Unity.Pipeline.Editor.Authoring` | Resolve/confine agent-supplied paths to the authoring root. | | `ObjectRef` | `Unity.Pipeline.Models` | Input handle to an existing object (asset or scene object). | | `AuthoringResult` | `Unity.Pipeline.Models` | Output identity of an object your command created or acted on. | | `ObjectResolver` | `Unity.Pipeline.Editor.Authoring` | `TryResolve(ObjectRef …)` (handle → object) and `Describe(object)` (object → `AuthoringResult`). | | `AuthoringUndoScope` | `Unity.Pipeline.Editor.Authoring` | Collapses all `Undo`-registered mutations in its lifetime into one editor undo step. | Authoring commands are Editor-only. Put them under `Editor/Commands//` in the `Unity.Pipeline.Editor.Commands.` namespace (see `Editor/Commands/Authoring/AuthoringConfigCommands.cs`). ## The authoring root (`set_authoring_root`) Agents pass **bare, relative paths** (`"Materials/Stone"`), not full project paths. `ProjectPaths` resolves those against a configurable **authoring root** and *confines* every write to it. The root defaults to `Assets` (full project access); an agent can narrow it to a sub-folder to sandbox itself: ``` get_authoring_root → { "root": "Assets" } set_authoring_root --root Assets/AgentWork ``` These are just thin commands over `ProjectPaths.AuthoringRoot` (`Editor/Commands/Authoring/AuthoringConfigCommands.cs`): ```csharp [CliCommand("set_authoring_root", "Set the base folder (under Assets/) that bare authoring paths resolve against and are confined to. Use 'Assets' for full project access.")] public static object SetAuthoringRoot( [CliArg("root", "Project-relative folder under Assets/, e.g. Assets/AgentWork. Use 'Assets' to allow the whole project.", Required = true)] string root) { // Throws ArgumentException for invalid roots (outside Assets/ or containing ".."); the // server surfaces that message to the caller. ProjectPaths.AuthoringRoot = root; return new { root = ProjectPaths.AuthoringRoot }; } ``` **Every path parameter your command accepts must go through `ProjectPaths.Resolve`.** This is the sandbox boundary — it rejects `..` traversal and anything that escapes the root, and it lets callers omit the `Assets/` prefix. Do not build asset paths by string concatenation. ```csharp var normalized = ProjectPaths.Resolve(path, out var error); if (normalized == null) throw new ArgumentException(error); // e.g. "Path '../secrets' must not contain '..'." // normalized is now a project-relative path guaranteed to live under the authoring root. ``` Resolution rules (`Editor/Authoring/ProjectPaths.cs`): - Bare paths (`"Materials/Stone"`) are taken relative to the root → `Assets/AgentWork/Materials/Stone`. - Explicit `Assets/…` / `Packages/…` paths are used as-is (but still confined to the root). - Absolute paths must live under the project root and are converted to project-relative. - `..` anywhere, or a result outside the root, returns `null` + an `error` string. ## Receiving objects: `ObjectRef` When a command needs to act on an object that *already* exists, take an `ObjectRef` parameter. The agent supplies **one** of several forms; `ObjectResolver.TryResolve` tries them in order — `globalId`, `path`, `guid` (+ optional `fileId`), `instanceId`, `hierarchyPath` — and hands you the live object: ```csharp public static Renderer ResolveRenderer(ObjectRef target) { if (!ObjectResolver.TryResolve(target, out var obj, out var error)) throw new ArgumentException(error); var go = obj as GameObject ?? (obj as Component)?.gameObject; var renderer = go != null ? go.GetComponent() : null; if (renderer == null) throw new ArgumentException($"Object '{target}' has no Renderer."); return renderer; } ``` Resolve **outside** any undo scope / before mutating, so a bad handle fails before your command changes anything (see `create_gameobject`, which resolves its `parent` before entering the scope). When the handle is a plain string (the usual agent input), a value with a file extension and no leading `/` — e.g. `"Materials/Floor.mat"` — is taken as an asset path and normalized under the authoring root, so the `Assets/` prefix is optional (mirroring path-taking commands). A leading `/` or an extension-less value stays a `hierarchyPath`; a dotted scene name like `"Cube.001"` still resolves because the `path` branch falls back to a hierarchy lookup. ## Returning objects: `AuthoringResult` Any command that creates or modifies an object should return its identity so the agent can reference it in a follow-up call. Don't build this by hand — call `ObjectResolver.Describe(obj)`, which fills in the right fields for the object kind: - **Assets** get `assetPath`, `guid`, `fileId`, `type` (+ `globalId`). - **Scene / loaded objects** get `instanceId`, `hierarchyPath`, `type` (+ `globalId`). ```csharp var result = ObjectResolver.Describe(asset) ?? new AuthoringResult { Type = nameof(Material) }; result.AssetPath = assetPath; // ensure the path is set even if Describe returned a fresh result return result; ``` `AuthoringResult` is **identity only** — no success flag, no message. Success/failure and timing live in the outer `CommandExecutionResponse` (the server adds them). To report a failure, **throw** (`ArgumentException` for bad input, `InvalidOperationException` for an operation that failed); the server converts the exception into a failure envelope. For a batch, return a model that holds an `AuthoringResult[]` (see `CreateGameObjectsResult`). ## Undo/redo Undo is Unity's native `UnityEditor.Undo`, grouped per command by `AuthoringUndoScope` so a single call reverts as a single Ctrl+Z step. Register each mutation with the matching `Undo` API **inside** the scope: ```csharp using (new AuthoringUndoScope("Set Material")) { Undo.RecordObject(renderer, "Set Material"); // record BEFORE mutating renderer.sharedMaterial = material; EditorSceneManager.MarkSceneDirty(renderer.gameObject.scene); } ``` Which `Undo` call to use: | Mutation | Call | |----------|------| | New scene object | `Undo.RegisterCreatedObjectUndo(obj, name)` | | Change fields on an existing object | `Undo.RecordObject(obj, name)` / `RegisterCompleteObjectUndo` (before the change) | | Add a component | `Undo.AddComponent(go, type)` | | Reparent | `Undo.SetTransformParent(child, parent, name)` | | Serialized properties | `SerializedObject` + `so.ApplyModifiedProperties()` (registers undo itself) | > **Important caveat.** `AssetDatabase` operations are **not** part of Unity's undo system. Creating a > folder or asset, importing, `AssetDatabase.CreateAsset`, `SaveAsPrefabAsset`, and file writes are > **not undone** by Ctrl+Z. `AuthoringUndoScope` only covers scene/object mutations. If your command > writes to disk, say so in its description and don't rely on undo to clean up — validate up front and > fail before writing. ## Worked example — a new content type Say the package has no material (rendering) commands and you want to add them. Two commands cover the whole pattern: one that **creates an asset** (path handling + `AuthoringResult` out, no undo because it's an `AssetDatabase` op) and one that **mutates a scene object** (`ObjectRef` in + undo). ```csharp using System.IO; using Unity.Pipeline.Commands; using Unity.Pipeline.Editor.Authoring; using Unity.Pipeline.Models; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; namespace Unity.Pipeline.Editor.Commands.Rendering { /// Authoring commands for materials (rendering). public static class MaterialCommands { [CliCommand("create_material", "Create a Material asset (default shader 'Standard') under the authoring root. " + "NOTE: asset creation is not undoable via Ctrl+Z.")] public static AuthoringResult CreateMaterial( [CliArg("path", "Asset path relative to the authoring root; the Assets/ prefix and the .mat extension are optional. e.g. Materials/Stone", Required = true)] string path, [CliArg("shader", "Shader name to assign. Defaults to 'Standard'.")] string shader = "Standard") { // 1. Resolve + confine the path (the sandbox boundary). var normalized = ProjectPaths.Resolve(path, out var error); if (normalized == null) throw new ArgumentException(error); if (!normalized.EndsWith(".mat", System.StringComparison.OrdinalIgnoreCase)) normalized += ".mat"; var found = Shader.Find(shader); if (found == null) throw new ArgumentException($"Shader '{shader}' was not found."); // 2. Do the work (AssetDatabase op — no undo scope; it wouldn't be undoable anyway). EnsureParentFolder(normalized); var material = new Material(found); AssetDatabase.CreateAsset(material, normalized); AssetDatabase.SaveAssets(); // 3. Return the created object's identity so the agent can reference it next. var result = ObjectResolver.Describe(material) ?? new AuthoringResult { Type = nameof(Material) }; result.AssetPath = normalized; return result; } [CliCommand("set_material", "Assign a material asset to a Renderer on a scene GameObject.")] public static AuthoringResult SetMaterial( [CliArg("target", "Handle of the GameObject (or Renderer) to modify.", Required = true)] ObjectRef target, [CliArg("material", "Handle of the material asset to assign (path/guid/globalId).", Required = true)] ObjectRef material) { // Resolve both handles up front, before mutating, so a bad handle changes nothing. var renderer = ResolveRenderer(target); if (!ObjectResolver.TryResolve(material, out var matObj, out var matError)) throw new ArgumentException(matError); if (matObj is not Material mat) throw new ArgumentException($"'{material}' is not a Material."); using (new AuthoringUndoScope("Set Material")) { Undo.RecordObject(renderer, "Set Material"); // record BEFORE the change renderer.sharedMaterial = mat; EditorSceneManager.MarkSceneDirty(renderer.gameObject.scene); } return ObjectResolver.Describe(renderer); } private static Renderer ResolveRenderer(ObjectRef target) { if (!ObjectResolver.TryResolve(target, out var obj, out var error)) throw new ArgumentException(error); var go = obj as GameObject ?? (obj as Component)?.gameObject; var renderer = go != null ? go.GetComponent() : null; if (renderer == null) throw new ArgumentException($"Object '{target}' has no Renderer."); return renderer; } // Mirrors the shared helper used by the built-in asset commands. private static void EnsureParentFolder(string assetPath) { var parent = Path.GetDirectoryName(assetPath)?.Replace('\\', '/'); if (string.IsNullOrEmpty(parent) || AssetDatabase.IsValidFolder(parent)) return; // Create intermediate folders (see create_folder / CreateFolderRecursive). Directory.CreateDirectory(ProjectPaths.ProjectRoot + "/" + parent); AssetDatabase.Refresh(); } } } ``` An agent chains them by feeding the first result into the second: ``` create_material --path Materials/Stone --shader Standard → { "assetPath": "Assets/AgentWork/Materials/Stone.mat", "guid": "…", "type": "Material" } set_material --target '{"hierarchyPath":"/Ground"}' --material '{"path":"Assets/AgentWork/Materials/Stone.mat"}' → { "instanceId": …, "hierarchyPath": "/Ground", "type": "MeshRenderer" } ``` ## Checklist for a new authoring command For *content-authoring* commands. Config/build/package commands follow the lighter [management-command conventions](#authoring-vs-management-commands) instead. - [ ] Editor-only, under `Editor/Commands//`, `static` (any accessibility — `public`/`internal`/`private`), tagged `[CliCommand]`. - [ ] Every path parameter resolved through `ProjectPaths.Resolve` (never concatenated). - [ ] Existing-object inputs taken as `ObjectRef`, resolved via `ObjectResolver.TryResolve`; resolve **before** mutating. - [ ] Scene/object mutations wrapped in an `AuthoringUndoScope` and registered with the matching `Undo` API. - [ ] `AssetDatabase` writes noted as non-undoable in the description; validate before writing. - [ ] Returns `AuthoringResult` (via `ObjectResolver.Describe`) or a model containing `AuthoringResult[]`. - [ ] Reports failures by throwing; never build the response envelope yourself. ## Build & verify New commands register automatically after the next recompile (see [Creating commands → Discovery](creating-commands.md#discovery)). Drive the live editor to verify: ``` command recompile command recompile_status # poll until done command create_material --path Materials/Stone ``` ## See also - [Creating commands](creating-commands.md) — the base command API this page builds on. - [Asset & file commands](commands/assets-and-files.md) — reference for the built-in asset commands. - [GameObject & component commands](commands/gameobjects-and-components.md) — the scene-mutation commands.