# Batch command The `batch` command runs several existing pipeline commands in a single request, with cross-operation result references, atomic Undo, and dry-run validation. It exists to collapse the round-trips of a "create → add components → wire fields" job (otherwise 10–90 sequential calls) into one request, and to add what `eval` structurally lacks: **transactionality and rollback**, so a failure mid-way never leaves partial state on a user-owned scene. **Scope guard:** `batch` is transactional object CRUD with references. It intentionally has **no** `invoke`/loop/conditional op types — arbitrary logic belongs in code-on-disk (`run_script`/`eval`), not in JSON. This keeps the command surface auditable and allowlistable. ### `batch` Run multiple registered commands in one transactional request. | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `operations` | yes | – | Ordered list of operations, each `{ id?, command, params }`. Max 200. | | `transactional` | no | `true` | Group every op into one Editor Undo step and revert all applied ops if any op fails. Forced `false` when `on_error=continue`. Rejects commands whose mutations Undo cannot revert (see [Transactional restrictions](#transactional-restrictions)). | | `on_error` | no | `abort` | `abort`: stop at the first failing op. `continue`: run every op, collecting per-op errors (forces `transactional=false`). | | `dry_run` | no | `false` | Validate command names, parameters, reference topology, and excluded/non-batchable commands without mutating anything. | | `result_fields` | no | – | Optional per-op result projection: a map of op id-or-index → array of result field paths to keep (context economy). | | `time_budget_ms` | no | `50000` | Cooperative time budget for the whole batch, in milliseconds (max 3600000). Checked before each op after the first; when exhausted, the remaining ops are skipped and a transactional batch rolls back. | **Returns:** `BatchResult` — `{ results: [{ id?, command, success, result? | error?, skipped?, resultTruncated?, revertible? }], applied, reverted, transactional, onError, error?, undoGroup?, dryRun?, valid? }`. The batch-level `error` is set only for aborts not attributable to a single op ("batch time budget exceeded", "batch canceled"). **Notes:** Runs on the main thread. Each sub-operation goes through the same dispatch path as `/api/exec`, so its result is shaped identically to running that command standalone. #### Operation shape Each entry of `operations` is: ```json { "id": "createHead", "command": "add_component", "params": { "target": "$root.instanceId", "type": "Rigidbody" } } ``` - `id` (optional) — a name for referencing this op's result from a later op. Must be unique within the batch and match `^[A-Za-z_][A-Za-z0-9_-]*$` (purely numeric ids would be ambiguous with index selectors, and `.` collides with the reference path separator). - `command` — the registered command to run (any command except the non-batchable ones below). - `params` — the command's parameters, same shape as `/api/exec` `parameters`. Operations are **always** `command` + `params`; the `argv`/`commandLine` raw forms are exec-only and are not accepted inside a batch operation. #### Cross-operation references Any **string** value in a later op's `params` may reference an earlier op's result: ``` "$." ``` - `` names an earlier op by its `id` or its 0-based index (an `id` match takes precedence). - `` is a Newtonsoft SelectToken path into that op's result, e.g. `instanceId`, `components[0].instanceId`. - `"$$"` escapes a literal `$` (so `"$$5"` is the literal string `"$5"`). - References are **backward-only** and resolved as **whole values** (the entire string must be the reference). The substituted JSON token keeps its type, so a numeric `instanceId` deserializes into an `ObjectRef` parameter exactly as a literal would. References may appear anywhere in the parameter tree (including inside nested objects/arrays). - Referencing an op that **failed** or was **skipped** errors explicitly (this can happen under `on_error=continue`); referencing a succeeded op that **returned no result** errors with "returned no result". A reference never silently resolves to `null`. - References always resolve against the **full** result of the earlier op, even when the reported copy was truncated or projected via `result_fields`. - **Result size bounds** (both measured in serialized UTF-8 bytes, i.e. wire size): a single op's reported result above **16 KiB** is replaced by a truncation marker (`resultTruncated: true` + a preview), and the **aggregate** of all reported results in one batch is bounded to **256 KiB** — past that, later results are elided the same way (the ops still execute; only the echoed copy is dropped). Use `result_fields` to project results down when a batch reads large objects. - A malformed `result_fields` path never fails its op (the op has already executed); the bad path is reported inside the projection as `invalidResultFields`. Example — create a GameObject, add a Rigidbody to it, then set the Rigidbody's mass: ```json { "operations": [ { "command": "create_gameobject", "params": { "name": "Ball" } }, { "command": "add_component", "params": { "target": "$0.instanceId", "type": "Rigidbody" } }, { "command": "set_serialized_field", "params": { "target": "$1.instanceId", "field": "m_Mass", "value": 2.5 } } ] } ``` #### Atomicity The whole batch runs inside one `AuthoringUndoScope`, so every op (each of which registers its own Undo operations) collapses into a **single** Editor Undo step. When `transactional` (the default) and the batch aborts — an op fails under `on_error=abort`, the time budget runs out, or the job is canceled — every applied op is reverted with `Undo.RevertAllDownToGroup` (including partial mutations registered by the failing op itself), the scene returns to its pre-batch state, and nothing is left on the Undo stack. `reverted: true` and the failing op's `error` are reported. #### Transactional restrictions Rollback is only real for **Undo-tracked** mutations (scene objects, components, serialized fields). Commands that mutate state **outside** the Undo system — asset and file writes, scene saves, Build Settings, project settings, baked data — cannot be rolled back, so a transactional batch **rejects** them with a structured `not_batchable_transactional` error. Run them with `transactional: false`, or call them standalone. Their per-op result reports `revertible: false` so the limitation is visible. The non-revertible set (v1 — a curated list, pending per-command `Revertible` metadata): - **Assets & files:** `create_asset`, `import_asset`, `move_asset`, `copy_asset`, `rename_asset`, `delete_asset`, `create_folder`, `set_import_settings`, `write_text_file`, `create_script` - **Prefab assets:** `create_prefab`, `create_prefab_variant`, `apply_prefab_overrides`, `save_prefab_contents` (scene-side prefab ops — `instantiate_prefab`, `unpack_prefab`, `revert_prefab_overrides` — are Undo-tracked and stay fully batchable) - **Scene persistence & build list:** `save_scene`, `save_all`, `add_scene_to_build`, `remove_scene_from_build`, `set_build_settings` - **Settings:** `set_audio_settings`, `set_graphics_settings`, `set_input_settings`, `set_lighting_settings`, `set_navmesh_settings`, `set_physics_settings`, `set_player_settings`, `set_quality_settings`, `set_tags_layers`, `set_time_settings`, `set_authoring_root` - **Animation / Timeline assets:** `create_animation_clip`, `set_animation_curve`, `remove_animation_curve`, `create_animator_controller`, `add_animator_layer`, `add_animator_parameter`, `add_animator_state`, `add_animator_transition`, `create_timeline`, `add_timeline_track`, `add_timeline_clip` - **Materials:** `set_material_properties` (persists the `.mat` asset to disk) - **Baking:** `bake_lighting`, `clear_baked_lighting`, `bake_navmesh`, `bake_navmesh_surfaces`, `clear_navmesh`, `bake_occlusion_culling`, `clear_occlusion_culling` `open_scene` and `create_scene` are **always** rejected inside a batch (even with `transactional: false`): they clear the Editor Undo stack, which would silently destroy the batch's ability to revert anything that ran before them. #### Timing, budget and cancellation The batch executes in a **single main-thread turn** — the Editor UI is frozen for its duration, bounded by `time_budget_ms` (frame-spanning execution is future work). The elapsed time is checked cooperatively before each op after the first; when the budget is exhausted, the remaining ops are reported `skipped: true`, the batch-level `error` is set to "batch time budget exceeded", and a transactional batch rolls back. For long batches, submit the request as a detached job (`"job": true` on `/api/exec`) and raise `time_budget_ms` — jobs also make the batch cancelable: cooperative cancellation (`POST /api/job/cancel`) is checked between ops and aborts the batch the same way ("batch canceled"). Per-op progress is reported (`op i/n: `) and visible via `GET /api/progress`. #### Dry run `dry_run: true` validates without executing anything: it catches unknown commands, excluded/non-batchable (`not_batchable`) commands, transactional rejections (`not_batchable_transactional`), unknown/missing parameters, and unresolvable reference topology (unknown or forward references). It mutates nothing and reports `valid` plus a per-op `success`/`error`. #### Excluded commands The following are rejected with a structured `not_batchable` error: - **Out of scope:** `build`, `switch_build_target`, `package_*`, `editor_play`/`editor_stop`/`editor_pause`, `recompile` - **Arbitrary code:** `eval`, `eval_file`, `run_script`, and nested `batch` - **Modal-dialog hazard:** `menu` (menu items can open modal dialogs that wedge an unattended batch) - **Cannot make progress:** synchronous `wait_for` — the batch holds the main thread for its whole turn, so the editor state a condition watches cannot change; the wait would just burn its timeout inside the batch. Run an async `wait_for` before/after the batch instead (`wait_status`/`wait_cancel` are quick reads and stay batchable) - **Undo-stack wipe:** `open_scene`, `create_scene` - **Async commands** (`run_tests`, `list_tests`, and any command returning `Task`): they are completed by `EditorApplication.update` callbacks, which cannot fire while the batch holds the main thread — dispatching one would freeze the Editor permanently - **Runtime-only commands** (e.g. `set_timescale`): they belong to the Player server surface and cannot execute on the editor server