Script commands
Commands for creating C# script files, attaching MonoBehaviours to GameObjects, reading/writing serialized fields, and running compiled project entry points in memory. Writing a .cs file does not make its type available — Unity must import and compile it (a domain reload) first. The flow is: script create → script recompile → poll script recompile_status (until completed/up_to_date) → script attach. For bulk construction that does not need a new persistent project type, skip the recompile entirely: script run compiles a file in memory and runs it with no asset import and no domain reload.
Two commands run C# without a domain reload:
script eval |
script run |
|
|---|---|---|
| Runs | A C# script, inline or from a .cs file: statements, using lines, declarations, await |
A named static method of a .cs file |
| Arguments | None | args, coerced to the method's parameters |
| Where | Editor and desktop development Player | Editor only |
| Use for | Queries, quick edits, waits | Builders you keep in a file and re-run; extra defines; in-place method replacement |
script create
Create a new C# script (default base class MonoBehaviour) from a template under the authoring root. NOTE: the type does not exist until a recompile completes — to attach it, call recompile, poll script recompile_status, then script attach.
| Parameter | Required | Default | Description |
|---|---|---|---|
name |
yes | – | Class/file name without extension, e.g. PlayerController. Must be a valid C# identifier. |
path |
no | – |
Folder (relative to the authoring root; the Assets/ prefix is optional) to write the .cs into. Defaults to the authoring root. |
namespace |
no | – |
Optional namespace to wrap the class in. Omit for the global namespace. |
base-class |
no | MonoBehaviour |
Base class to derive from. Defaults to MonoBehaviour. |
overwrite |
no | false |
Overwrite the file if it already exists. Defaults to false (an existing file is an error). |
Returns: AuthoringResult of the new MonoScript asset (with assetPath).
Fails: InvalidUsage for an empty or invalid class name, or a path outside the authoring root; NotFound when the destination folder doesn't exist; Failed when the file exists and overwrite is false.
Notes: Does not trigger a recompile; the created type is not usable until a recompile completes (poll script recompile_status).
script attach
Add a MonoBehaviour to a GameObject by its (compiled) type name OR by its script asset path. Provide exactly one of 'type' or 'script'. If the type isn't compiled yet, returns a recoverable error: recompile, poll script recompile_status, then retry.
| Parameter | Required | Default | Description |
|---|---|---|---|
target |
yes | – | Reference to the GameObject to add the component to (globalId/path/guid/instanceId/hierarchyPath). |
type |
no | – |
Component type name to add, e.g. PlayerController or Game.Player.PlayerController. Must already be compiled. Mutually exclusive with 'script'. |
script |
no | – |
Script asset path, e.g. 'Assets/Pool/Scripts/CueShooter.cs'. The backing class is resolved via MonoScript.GetClass(), so the class name may differ from the filename. Mutually exclusive with 'type'. |
Returns: AuthoringResult of the new component.
Fails: InvalidUsage when both or neither of type and script are given, or target isn't a GameObject; NotFound when target doesn't resolve or no MonoScript is at script; Failed when the type isn't compiled yet (the error says to recompile, poll script recompile_status, then retry), isn't a concrete MonoBehaviour, or Unity refuses the component.
Notes: Undo-able. Provide exactly one of type or script.
component set_field
Set a serialized field on a component/asset. Supports primitives, enums, Vector/Color/Rect/Bounds, object references (value = an ObjectRef: asset by guid/fileId/path or scene object by instanceId/hierarchyPath), [Serializable] structs/classes (value = an object of their fields), whole arrays/lists, and array elements via 'name.Array.data[i]' (or 'name.Array.size' to resize).
| Parameter | Required | Default | Description |
|---|---|---|---|
target |
yes | – | Reference to the component or asset to modify (globalId/path/guid/instanceId/hierarchyPath). May be a GameObject when 'component' is given. |
field |
yes | – | SerializedProperty path, e.g. 'speed', 'settings.speed', or 'waypoints.Array.data[0]'. |
value |
yes | – | JSON value to assign. Vectors, colors and rects as {"x":1,"y":2}, [1,2] or "1,2". A struct/class as an object of the fields to change. Enums by name ([Flags]: "A|B") or underlying value. For object references pass an ObjectRef object (or null to clear). |
component |
no | – |
Component type name on the target GameObject (e.g. 'Rigidbody'). Use when 'target' is a GameObject; omit when 'target' is already a component handle. |
Returns: AuthoringResult of the modified component or asset.
Fails: NotFound when target, the component type, a matching component, or field doesn't exist; InvalidUsage when target is a GameObject and component is omitted, has several components of that type (the error lists their instanceIds), or is an asset given a component. Nothing changes when it fails.
Notes: Undo-able. A target path of the form Assets/UI/Screen.prefab:Canvas/Header addresses a GameObject inside a prefab asset; see Receiving objects.
component get_fields
Read serialized fields of a component/asset. Returns each top-level field's name, type and value (object references are returned as re-usable handles). Pass 'field' to read a single SerializedProperty path. Pass detail='value' to return just the value(s) — a token-efficient read for scalars.
| Parameter | Required | Default | Description |
|---|---|---|---|
target |
yes | – | Reference to the component or asset to read (globalId/path/guid/instanceId/hierarchyPath). May be a GameObject when 'component' is given. |
field |
no | – |
Optional single SerializedProperty path to read (e.g. 'speed' or 'items.Array.data[0]'). Omit to read all top-level fields. |
component |
no | – |
Component type name on the target GameObject (e.g. 'Rigidbody'). Use when 'target' is a GameObject; omit when 'target' is already a component handle. |
detail |
no | full |
full for a descriptor per field {name,path,propertyType,isArray,arrayLength,value}; value for just the value(s): the raw value for a single 'field', or a map for all fields. |
Returns: depends on field and detail:
detail=full (default) |
detail=value |
|
|---|---|---|
With field |
One descriptor { name, path, propertyType, isArray, arrayLength, value } |
The raw value |
Without field |
One descriptor record per top-level field | One { name: value } map |
Fails: as component set_field, and NotFound when field doesn't exist.
script eval
Compile and run C# as a script: statements, using directives, top-level methods and types, and
await. The value of return x;, or of a trailing expression, is the result.
| Parameter | Required | Default | Description |
|---|---|---|---|
code |
no | – |
C# code to evaluate. Give either code or file. May be given first, without --code. |
file |
no | – |
Path to a .cs file to evaluate instead of code. In the Editor, relative paths resolve against the project root (the parent of Assets/); in a player, against the working directory. |
timeout |
no | 30 |
Timeout in seconds for the whole run, awaits included. |
dry-run |
no | false |
Compile only: report diagnostics. Nothing is loaded or run. |
auto-import |
no | false |
When a type or name is not found, add a using for the one namespace that declares it and compile again. Each using it added is a warning on stderr. A name declared in several namespaces fails and lists them. |
internals |
no | false |
Editor only. Compile so Unity's internal APIs are callable. Each run borrows an assembly name that Unity's modules grant internal access; once all of them are in use, it fails until the next domain reload. |
A script can use these without a using line: System, System.Collections.Generic,
System.Linq, System.Threading.Tasks, UnityEngine, UnityEditor (Editor only), and
Unity.Pipeline.Scripting. Object means UnityEngine.Object unless the script declares its own
Object alias.
Wait (in Unity.Pipeline.Scripting) awaits something that finishes later without blocking the
main thread: Wait.Until(condition), Wait.Until(getValue, condition), Wait.For(asyncOperation),
and Wait.ForImport(assetPath) in the Editor. A wait that runs out of time throws
WaitTimeoutException; cancelling the job stops it. A wait cannot span a domain reload.
Returns: the script's value, as JSON: 4, "text", an object. A script with no value returns
null, and a dry run writes nothing: its exit code says whether the script compiled. Compiler warnings are warnings on stderr, as line L, col C: warning ID: message; lines are 1-based and count the script's own lines.
Errors: a failed eval writes no result and exits non-zero. Its error on stderr starts with the
category, then says what failed:
| Category | When | Exit code |
|---|---|---|
INVALID_COMMAND_ARGS |
No code, both code and file, a missing, empty or non-.cs file, or internals in a player |
InvalidUsage |
COMPILATION_ERROR |
The script does not compile. The first error line gives the count, then each compile error is its own error line, as line L, col C: error ID: message |
Failed |
RUNTIME_EXCEPTION |
The script threw, including a WaitTimeoutException; the error has the exception and its stack trace, with script lines |
Failed |
UNSUPPORTED_PLATFORM |
A player that is not a desktop development build | Failed |
EVAL_INTERNAL_ERROR |
The compiled script could not be loaded or started | Failed |
A run that outlasts timeout times out (HTTP 504, TIMEOUT); the script keeps running detached.
Notes: MainThreadRequired = true. Available on both editor and runtime. Run it as a job
(--detach) for long waits. Each run loads one small assembly that Unity cannot unload; a dry run
loads none.
Examples
Statements, a using alias, and top-level declarations with a trailing expression as the result:
unity command script eval 'return Application.unityVersion;' # the Editor's version
unity command script eval 'return 2 + 2;' # 4
unity command script eval 'using Random = UnityEngine.Random; return Random.Range(1, 7);' # 1 to 6
unity command script eval 'class P { public int V; } static int Twice(int x) => x * 2; Twice(new P { V = 21 }.V)' # 42
await keeps the Editor running, so Unity objects stay usable across frames:
unity command script eval 'var started = Time.realtimeSinceStartup; await Task.Delay(300); return Time.realtimeSinceStartup - started >= 0.3f;' # true
unity command script eval 'var go = new GameObject("Probe"); await Task.Yield(); var alive = go != null; Object.DestroyImmediate(go); return alive;' # true
Wait for an import, or give up with a reason:
unity command script eval 'await Wait.Until(() => !EditorApplication.isUpdating); return AssetDatabase.FindAssets("t:Prefab").Length;'
unity command script eval 'var path = "Assets/E2EEvalDemo.txt"; System.IO.File.WriteAllText(path, "hello"); AssetDatabase.ImportAsset(path); await Wait.ForImport(path); var text = AssetDatabase.LoadAssetAtPath<TextAsset>(path).text; AssetDatabase.DeleteAsset(path); return text;' # "hello"
unity command script eval 'await Wait.Until(() => false, timeoutMs: 200, because: "the build never finished");'
# RUNTIME_EXCEPTION: WaitTimeoutException: Wait.Until timed out after 200 ms: the build never finished
Run a long wait as a job, and cancel it:
unity command script eval 'await Wait.Until(() => GameObject.Find("E2ENeverSpawned") != null, timeoutMs: 600000); return "spawned";' --detach
unity job status <jobId> # running
unity job cancel <jobId>
unity job status <jobId> # canceled
Run a file. Keep it outside Assets/ (here AgentScripts/) so writing it triggers no import.
AgentScripts/CountByTag.cs:
using System.Text;
using UnityEngine.SceneManagement;
class Counted { public string Tag; public int Count; }
static Counted[] CountByTag(Scene scene) =>
scene.GetRootGameObjects()
.SelectMany(root => root.GetComponentsInChildren<Transform>(true))
.GroupBy(t => t.tag)
.Select(g => new Counted { Tag = g.Key, Count = g.Count() })
.OrderByDescending(c => c.Count)
.ToArray();
var report = new StringBuilder();
foreach (var c in CountByTag(SceneManager.GetActiveScene()))
report.AppendLine($"{c.Tag}: {c.Count}");
report.ToString()
unity command script eval --file AgentScripts/CountByTag.cs # "MainCamera: 1\nUntagged: 1\n"
unity command script eval --file AgentScripts/CountByTag.cs --dry-run # compiles and reports diagnostics; nothing runs
Add missing using directives, or see which namespaces to choose from:
unity command script eval 'return new StringBuilder("auto").Append("-import").ToString();' --auto-import
# "auto-import"; stderr: warning: auto-import added: using System.Text;
unity command script eval 'return EditorSceneManager.GetActiveScene().name;' --auto-import
# stderr: warning: auto-import added: using UnityEditor.SceneManagement;
unity command script eval 'return new Image();' --auto-import
# COMPILATION_ERROR: ... auto-import found it in several namespaces: ..., UnityEngine.UIElements.Image. Add a using directive for the one you mean.
Call internal Editor APIs:
unity command script eval 'return UnityEditor.LogEntries.GetCount();' --internals # the console's entry count
unity command script eval 'return UnityEditor.LogEntries.GetCount();'
# COMPILATION_ERROR: line 1, col 19: error CS0122: 'LogEntries' is inaccessible due to its protection level
Compile errors list each error with its line and column:
unity command script eval 'return Foo(;'
# COMPILATION_ERROR:
# line 1, col 11: error CS1026: ) expected
# line 1, col 7: error CS0103: The name 'Foo' does not exist in the current context
script run
Compile a single project .cs file in memory (no domain reload, no code carried through the
protocol) and execute a named static entry point. This is the builder-pattern path for bulk
construction — see the note below. Editor command; still arbitrary code execution, so it is grouped
under the same scripts/eval tag as script eval and is disabled wherever the eval family is disabled.
In short: keep a small tool as a .cs file, and call one of its methods with arguments. Editing
the file and running it again costs an in-memory compile, not a domain reload.
The file is ordinary C# — types and methods, no top-level statements — unlike a
script eval --file script, which runs top to bottom and takes no arguments. Nothing in the file
runs unless entry names it.
| Parameter | Required | Default | Description |
|---|---|---|---|
file |
yes | – |
Path to a .cs file. Relative paths resolve against the project root (the parent of Assets/), not the process working directory. May live outside Assets/ (e.g. AgentScripts/) so writing it never triggers an asset import or domain reload. |
entry |
no | – |
Entry point as Namespace.Type.Method (or Type.Method, or a bare Method). Nested types may be written with dots (Outer.Inner.Method). Default: the single public static method if unambiguous, else a method named Main. Static, non-generic methods only in v1. Ephemeral mode only — rejected with mode=hotpatch. |
args |
no | – |
JSON array of arguments, coerced to the entry's parameter types: primitives, enum names, string[], and a string/object handle (ObjectRef) for UnityEngine.Object parameters. JSON null is only valid for reference-type and nullable parameters (never a silent default(T)). Ephemeral mode only — rejected with mode=hotpatch. |
mode |
no | ephemeral |
ephemeral: compile to an in-memory assembly, run the entry, discard. hotpatch: apply in-place [MethodReload] method replacements to already-loaded types (delegates to script.methodreload reload). |
references |
no | – |
Extra assembly name prefixes to reference. In the Editor all loaded assemblies are already referenced, so this is effectively a no-op there. Ephemeral mode only; not applied by hotpatch (the compile is script.methodreload reload's). |
defines |
no | – |
Extra scripting define symbols, appended to the project's active editor defines (EditorUserBuildSettings.activeScriptCompilationDefines) — so UNITY_EDITOR, version and platform symbols are already set and sources behave like project code; your defines extend that set rather than replace it. Ephemeral mode only; not applied by hotpatch. |
pdb |
no | false |
Emit a portable PDB mapped to the source file so breakpoints bind. Ephemeral runs always emit source-mapped symbols so exception stack traces carry file:line. Compiles unoptimized. |
timeout |
no | 60 |
Timeout in seconds for the whole run, like script eval. |
dry-run |
no | false |
Compile only: return diagnostics; the emitted assembly is not loaded into the domain and nothing executes (the response carries no assemblyName). Rejected with mode=hotpatch — a hotpatch "dry run" would still apply live method replacements. |
Returns: { result, diagnostics, compileMs, executeMs, assemblyName }: the entry point's return
value, the compile's Roslyn diagnostics (warnings, with line/column), and timings. The result carries
no echo of the source (path + diagnostics only). A dry run logs Compiled successfully at info level
and writes result: null and no assemblyName.
Fails: errors go to stderr.
InvalidUsage: an emptyfileor one without a.csextension, an unknownmode,dry-run,entryorargswithmode=hotpatch, orargsthat don't fit the entry point's parameters.NotFound: the file doesn't exist, orentrydoesn't resolve to exactly one static method (the error lists the candidates).Failed: compile errors (each Roslyn error with its line, column and id, also kept as the result'sdiagnostics; nothing executes), an unreadable or empty file, an entry point that throws (its exception withfile:linemapped to the source file), a timeout, or a hotpatch reload that fails. Notes:MainThreadRequired = true. Editor command.Executing runs always compile unoptimized (Debug codegen) so the emitted symbols keep full sequence points for
file:linemapping — expect the generated code to run somewhat slower than regular project assemblies.Async entry points (
async Task/Task<T>) are awaited to completion;Task<T>.Resultbecomes the command result and exceptions in the async work surface as runtime errors. The await is asynchronous — the Editor main thread keeps pumping, so awaits that resume on Unity's main-thread context (Task.Yield(),Task.Delay(), Awaitable, …) work naturally; noConfigureAwait(false)contortions are required. The wait is bounded by what remains oftimeout; on expiry the command exitsFailedwith a timeout error and the task keeps running detached (its effects may still land), like an abandonedscript eval.hotpatch specifics:
entry,argsanddry-runare rejected (InvalidUsage);referencesanddefinesare not applied. Path resolution followsscript.methodreload reload, which probes underAssets/for bare filenames (unlike ephemeral's project-root resolution), and only accepts files underAssets/or a loaded package: anything else fails with anOut Of Project Scopeerror. Timing:executeMscarries the whole reload duration (compile + apply, not split byscript.methodreload reload);compileMsis0. Advisory reload diagnostics are surfaced as severity-warningentries on success.Under Unity's Mono runtime the in-memory assembly emitted by an executing
ephemeralrun is not collectible/unloadable, so each run retains one small assembly (the same behavior asscript eval); prefer a single builder entry over many tiny runs.dry-runloads nothing.
Examples
Build a grid of cubes whose size is chosen per run. AgentScripts/Grid.cs:
using UnityEngine;
public static class Grid
{
public static int Build(int columns, int rows, float spacing)
{
var root = new GameObject("Grid");
for (var x = 0; x < columns; x++)
for (var z = 0; z < rows; z++)
{
var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube.name = $"Cell_{x}_{z}";
cube.transform.SetParent(root.transform);
cube.transform.position = new Vector3(x * spacing, 0, z * spacing);
}
return columns * rows;
}
}
unity command script run --file AgentScripts/Grid.cs --entry Grid.Build --args '[4, 3, 1.5]'
# { "result": 12, "compileMs": ..., "executeMs": ..., "diagnostics": [], "assemblyName": "PipelineRunScript_Grid_..." }
unity command script run --file AgentScripts/Grid.cs --args '[10, 10, 2]' # entry omitted: Build is the only public static method
unity command script run --file AgentScripts/Grid.cs --dry-run # compile only: diagnostics, nothing runs
A project audit called with different thresholds. AgentScripts/Audit.cs:
using System.Linq;
using UnityEditor;
public static class Audit
{
public static string[] LargeTextures(int maxSize)
{
return AssetDatabase.FindAssets("t:Texture2D")
.Select(AssetDatabase.GUIDToAssetPath)
.Where(path => AssetImporter.GetAtPath(path) is TextureImporter importer && importer.maxTextureSize > maxSize)
.ToArray();
}
}
unity command script run --file AgentScripts/Audit.cs --entry Audit.LargeTextures --args '[1024]' # paths of textures imported above 1024
unity command script run --file AgentScripts/Audit.cs --entry Audit.LargeTextures --args '[2048]'
The builder pattern (bulk construction)
For bulk construction — creating many objects, wiring private fields, generating content — put the
logic in a versioned project script and invoke it with script run. Write the file with
asset write_text (ideally under a non-Assets/ folder such as AgentScripts/ so the write triggers
no asset import), then call script run --file AgentScripts/Build.cs --entry Build.All. Iterating the
script and re-running it costs an in-memory compile (< ~2s), not a 15–20s domain reload.
The anti-patterns this replaces: carrying construction code as escaped C# strings through script eval
(verbose, unreviewable, shell-escaping hazards), and editing a builder under Assets/ in a loop
(each edit pays a full domain reload). Code goes in files on disk. script eval --file runs a file as
a script, with using directives, declarations and await; script run calls a named entry point
with arguments, extra defines, or in-place method replacement.
See Creating commands and Connectivity.