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; 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 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 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.
| 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/<Area>/ in the
Unity.Pipeline.Editor.Commands.<Area> 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):
[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.
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, returnsnull+ anerrorstring.
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:
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<Renderer>() : 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).
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:
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.
AssetDatabaseoperations 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.AuthoringUndoScopeonly 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).
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
{
/// <summary>Authoring commands for materials (rendering).</summary>
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<Renderer>() : 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 instead.
- [ ] Editor-only, under
Editor/Commands/<Area>/,static(any accessibility —public/internal/private), tagged[CliCommand]. - [ ] Every path parameter resolved through
ProjectPaths.Resolve(never concatenated). - [ ] Existing-object inputs taken as
ObjectRef, resolved viaObjectResolver.TryResolve; resolve before mutating. - [ ] Scene/object mutations wrapped in an
AuthoringUndoScopeand registered with the matchingUndoAPI. - [ ]
AssetDatabasewrites noted as non-undoable in the description; validate before writing. - [ ] Returns
AuthoringResult(viaObjectResolver.Describe) or a model containingAuthoringResult[]. - [ ] Reports failures by throwing; never build the response envelope yourself.
Build & verify
New commands register automatically after the next recompile (see Creating commands → 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 — the base command API this page builds on.
- Asset & file commands — reference for the built-in asset commands.
- GameObject & component commands — the scene-mutation commands.