Editing Blueprints with edit plans
Experimental: Blueprint editing (the Blueprint Edit Automation plugin, edit plans and
blueprintreview edit) is experimental. The plan format and the ops may still change between releases, and an edit can fail or need finishing by hand in the editor. Keep the project under source control and review every change before you commit it.
The BlueprintEditAutomation plugin applies edit plans to Blueprints, user enums, user structs and data tables: JSON files that say what to change, op by op. A person, a script or an AI agent writes the plan, and the plugin makes the edits through the same engine calls the Blueprint editor uses.
It doesn't change anything by halves:
- every reference must resolve to exactly one node or pin;
- every edit is checked;
- nodes are laid out without overlaps;
- every edited Blueprint is compiled.
If any op or any compile fails, nothing is saved.
A plan speaks the language of blueprintreview dump: types are its PinType objects, values its Values, nodes
their NodeGuids and pins nodeGuid:pinId. So the loop is:
- read the asset with
blueprintreview dump; - write a plan in the same terms;
- check it with
blueprintreview edit validate; - apply it with
blueprintreview edit apply; - review the change with
blueprintreview diffon the before and after JSON.
- Install: PLUGIN.md. This plugin needs BlueprintExporter enabled too.
- Engines: UE 5.7 and 5.8.
- Supported assets: uncooked editor assets.
Contents
- Running a plan
- Plan format
- References
- Types and values
- Ops
- Layout
- The report
- The edit server
- Not covered
1. Running a plan
blueprintreview edit validate plan.json --project MyGame.uproject # offline: shape, and the GUIDs/pins/graphs it names
blueprintreview edit apply plan.json --project MyGame.uproject --dry-run
blueprintreview edit apply plan.json --project MyGame.uproject
apply validates first. It then uses the open editor's edit server when one runs (section 8), and a headless
commandlet otherwise (--live / --commandlet force one or the other).
- Commandlet: it saves, unless
--dry-run. Close the editor first. - Live: it leaves the packages dirty for you to review and save, and Ctrl+Z undoes the whole plan.
--savesaves instead.
The command exits with 1 unless the plugin's report, carrying this run's nonce, says it succeeded.
The plugin can also be driven directly:
| Host | Command |
|---|---|
| Commandlet | UnrealEditor-Cmd.exe MyGame.uproject -run=BlueprintEdit -Plan=<abs.json>[+<abs.json>...] -Nonce=<hex> [-DryRun] [-Report=<abs.json>] -unattended -nopause |
| Snapshot only | ... -run=BlueprintEdit -Snapshot=/Game/A[+/Game/B] -Nonce=<hex>: each asset's canonical JSON |
| Op table | ... -run=BlueprintEdit -ListOps=<abs.json> |
| Editor console | BlueprintEdit.Apply <abs.json> [-DryRun] [-Save] [-Nonce=<hex>] |
Several +-joined plans share one editor start-up, which is the slow part, and each gets its own report.
Don't trust the exit code. Editor commandlets have exited 0 without running. A run counts only when its report
exists, carries your nonce, and has a status of applied, dry_run_ok or snapshot_ok. started means the
process died part-way. blueprintreview edit apply checks all of this for you.
Preflight refuses an asset when:
- its file is read-only (check it out first;
--dry-runskips this check); - it has unsaved changes in the editor;
- it already fails to compile;
- its parent isn't
expect_parent; - it's listed with
createbut already exists.
Saving. Every existing file is copied to Saved/BlueprintEdit/backup/<plan>.<nonce>/ first. If a save fails
part-way, the files already saved are restored from those copies. New files still need adding to source control.
2. Plan format
{
"schema": "BlueprintEditPlan/2",
"meta": { "ticket": "GAME-123", "agent_run": "..." },
"check_referencers": false,
"assets": [
{ "asset": "/Game/Characters/BP_Hero", "expect_parent": "/Script/Engine.Character", "ops": [ ... ] },
{ "asset": "/Game/Data/E_Mood", "create": { "kind": "enum", "enumerators": ["Calm", "Angry"] } }
],
"asset_ops": [ { "op": "rename_asset", "asset": "/Game/Old/BP_Thing", "to": "/Game/New/BP_Thing" } ]
}
- Unknown keys are errors everywhere. The whole plan is validated before anything loads.
metais free-form, and is echoed into the report.- Order: assets apply and compile in array order. Something that uses what this plan adds to another asset must come after it.
check_referencers(commandlet only): after the apply, it loads every Blueprint that references an edited asset, refreshes its nodes and compiles it. If any of them fails, nothing is saved.asset_opsrename, duplicate or delete whole assets. They run after the edits are saved, and only in a saving run. A dry run only checks them.- Common op keys:
op(required);id: an alias,@id, for what the op creates;step: a cross-reference to your own notes;note.
Creating assets with an asset's create block. A created asset may take ops too.
kind |
Keys |
|---|---|
blueprint |
parent, type?: normal, function_library, macro_library or interface. Widget and Anim Blueprints are refused |
enum |
enumerators: display names |
struct |
fields: [{name, type, default_value?}] |
data_table |
row_struct, rows: [{ "Name": "Row1", ... }] (the DataTable editor's JSON import form) |
The full grammar is schema/edit-plan.schema.json, and
blueprintreview edit ops lists every op with its keys.
3. References
Nodes are a string or an object:
| Form | Matches |
|---|---|
"8F3A…C2" |
a NodeGuid: 32 hex digits, as dump prints it |
"@print" |
a node an earlier op created with "id": "print". A function's id also gives @id.entry and @id.result, an override's @id.parent, a collapse's @id.entry and @id.result (and graph id.graph), and a paste's @id.1, @id.2, … |
{"guid": …} / {"alias": …} |
the same, as objects |
{"name": "K2Node_CallFunction_3", "graph": "EventGraph"} |
the object name. Names are unique only per graph |
{"class": "K2Node_CustomEvent", "member": "OnHit"} |
a class (or any base class), plus a function / event / variable / macro name |
{"class": "K2Node_CallFunction", "title": "Print String"} |
a class plus its list-view title; a last resort |
Zero or several matches is an error, and the error lists every candidate. An op's id also names the graph it
creates, anywhere a graph name goes.
Pins are "<node>:<pin>", where <node> is a GUID or @alias and <pin> is a PinId or a pin name. So a
dump link, nodeGuid:pinId, is a pin reference as it stands. The object form is
{"node": <node>, "pin": "<name>" | "id": "<PinId>", "direction": "input" | "output"}.
A pin name matches exactly, then case-insensitively, then by the label the editor shows. exec means execute and
return means ReturnValue, unless a pin really has that name. Some nodes get new pin ids when they're rebuilt,
so a pin name is the safer reference to a node an earlier op changed.
4. Types and values
Types are bp-model PinType objects, exactly as dump prints them:
{ "category": "real", "sub_category": "double" }
{ "category": "object", "sub_category_object": "/Script/Engine.Actor", "container": "array" }
{ "category": "byte", "sub_category_object": "/Game/Data/E_Mood.E_Mood" }
{ "category": "name", "container": "map", "value_type": { "category": "int" } }
- Categories are UEdGraphSchema_K2's names:
bool int int64 real byte name string text struct enum object class interface softobject softclass. realneeds"sub_category": "float"or"double". UE5's Float variables aredouble.- Paths are full object paths.
execandwildcardonly go in macro pins andassert_pin.
Pin defaults use the three fields dump prints: default_value (most pins), default_object (object and
class pins, an asset path) and default_text (text pins).
Property values take one of two keys:
"value": a bp-modelValue({"Float": 2.5},{"Struct": {"ty": "Vector", "fields": [...]}},{"Object": "/Game/A.A"}, ...);"text": the property's ExportText form ("(X=1,Y=2,Z=3)","True").
5. Ops
Every op checks what the editor would refuse (a name that's taken, a pin type a function can't have, a node a user
can't delete), and fails rather than half-doing it. Ops that would silently change something else need saying so
(link over an existing exec link: "replace": true; a conversion node: "allow_conversion": true). The keys
of each op are in blueprintreview edit ops.
| Area | Ops |
|---|---|
| Variables | add_variable, set_variable_flags (every Details setting: instance editable, expose on spawn, private, read only, replication + condition, cinematics, config, transient, save game, advanced, deprecated, tooltip, category, friendly name), rename_variable, remove_variable, set_variable_type, set_variable_default, set_variable_metadata, move_variable, duplicate_variable, replace_variable_references, and the local versions: add_local_variable, remove_local_variable, rename_local_variable, set_local_variable_type, set_local_variable_default |
| Functions and macros | add_function, add_function_override, add_macro, add_function_inputs / add_function_outputs and remove_function_inputs / remove_function_outputs (functions, macros and dispatchers), set_param (rename, type, by reference, default), move_param, set_function_flags, set_function_category, set_function_metadata (tooltip, keywords, compact title, call in editor, deprecated, thread safe, access, const, pure), rename_function |
| Graphs | add_event_graph, remove_graph, rename_graph, collapse_nodes (into a collapsed graph, a function or a macro), expand_node, convert_function_to_event, convert_event_to_function |
| Events and dispatchers | add_event_inputs, remove_event_inputs, rename_event, add_dispatcher, remove_dispatcher, rename_dispatcher; custom-event settings through set_param, move_param, set_function_flags and set_function_metadata with "node" |
| Class | implement_interface, remove_interface, reparent, set_class_default, set_class_setting (Class Settings), compile |
| Components | add_component, remove_component, rename_component, attach_component (with socket), set_root_component, set_component_property (inherited components through their override template) |
| Nodes | spawn_node, remove_node, paste_t3d (the editor's copy text, e.g. from blueprintreview copy), move_node, align_nodes, set_node_enabled, set_node_comment, refresh_nodes, replace_node, set_node_property, insert_into_exec_chain |
| Pins | link, break_link, break_all_links, set_pin_default, add_input_pins, remove_pin, split_pin, recombine_pin, promote_to_variable, move_links, insert_reroute |
| Comments | add_comment, set_comment (text, colour, font size, bubble, move mode), resize_comment |
| Timelines | spawn_node kind timeline, set_timeline, add_timeline_track (float, vector, colour, event; or an external curve), remove_timeline_track, rename_timeline_track, set_timeline_keys, rename_timeline |
| Enums | add_enumerator, remove_enumerator, rename_enumerator, move_enumerator, set_enum (description, bitflags, tooltips) |
| Structs | add_struct_field, remove_struct_field, rename_struct_field, set_struct_field (type, default, tooltip), move_struct_field, set_struct_tooltip |
| Data tables | add_row (optionally a copy), remove_row, rename_row, set_row, move_row, import_rows (JSON merge or replace, CSV replace), set_row_struct |
Whole assets (asset_ops) |
rename_asset (leaves a redirector), duplicate_asset, delete_asset (refused while referenced, unless force) |
| Checks | assert_linked, assert_pin: read-only preconditions. Put one before a rewire, so a graph that changed since the plan was written fails loudly |
spawn_node kinds:
kind |
Argument object |
|---|---|
call_function |
function: {name, owner?}: owner is a class path, or self by default; covers library nodes (math, strings, arrays, Print String) |
call_parent |
function: {name?}: the graph's own function by default |
message |
function: {owner: <interface>, name} |
macro |
macro: {name, library?}: StandardMacros by default (IsValid, ForEachLoop, DoOnce, Gate, …); "self" for this Blueprint's |
variable_get / variable_set |
variable: {name, owner?, local?} |
custom_event / override_event |
event: {name}: an override is a parent or interface event, e.g. ReceiveBeginPlay |
cast |
cast: {class, pure?} |
make_struct / break_struct / switch_enum |
struct: {path} / enum: {path} |
dispatcher |
dispatcher: {name, action: call | bind | unbind | unbind_all, owner?} |
timeline |
timeline: {name, length?, loop?, auto_play?, float_tracks?, event_tracks?} |
generic |
generic: {class, properties?}: any K2 node class (K2Node_IfThenElse, K2Node_ExecutionSequence, K2Node_SpawnActorFromClass, /Script/UMGEditor.K2Node_CreateWidget, …), its properties set before its pins are made |
select, knot |
none |
A node is placed with pos: [x, y] or near: {node, dx?, dy?}. With neither, it goes to the right of everything.
An example, test-data/edit-plans/greeter.json, builds a new actor:
{ "op": "add_function", "id": "greet", "name": "Greet", "inputs": [{ "name": "Text", "type": { "category": "string" } }] },
{ "op": "spawn_node", "id": "branch", "graph": "greet", "kind": "generic", "generic": { "class": "K2Node_IfThenElse" }, "near": { "node": "@greet.entry" } },
{ "op": "link", "from": "@greet.entry:then", "to": "@branch:execute" },
{ "op": "spawn_node", "id": "begin", "graph": "EventGraph", "kind": "override_event", "event": { "name": "ReceiveBeginPlay" } },
{ "op": "spawn_node", "id": "call", "graph": "EventGraph", "kind": "call_function", "function": { "name": "Greet" }, "near": { "node": "@begin" } },
{ "op": "link", "from": "@begin:then", "to": "@call:execute" }
An edit to an existing asset names its nodes by GUID, taken from dump:
{ "op": "set_pin_default", "pin": "3EEB4C2F4F04211C138B75930D26AEE0:5A2D…E1", "default_value": "false" },
{ "op": "insert_into_exec_chain", "node": "@log", "after": "27024A2D4FB7114544D059B86EB15A6A:then" }
6. Layout
A node the plan creates or moves never overlaps another node. The one exception is a comment over the nodes it encloses.
posis kept exactly; whatever is in the way is pushed right as a block.near, or no hint: the node may slide down into a free slot.insert_into_exec_chainmoves the node into its wire and pushes the downstream nodes right.- A node that grows (new pins, a split struct) pushes away only what its new area covers.
- Comments: a comment moves with the nodes it encloses, and a new or resized comment pushes unrelated nodes out from under itself.
The rule is checked after the ops run, not trusted, and any overlap fails the plan. Commandlets have no Slate to measure nodes with, so node sizes are estimated from titles and pin labels, on the generous side.
7. The report
Every run writes <Project>/Saved/BlueprintEdit/<plan>.<nonce>.report.json (BlueprintEditReport/2). It holds:
- the status and the nonce;
- what each op did;
- every created node's alias, GUID and pins (
id,name,direction), so a follow-up plan can name them; - the compile results;
- for each Blueprint, its canonical JSON before and after the edit (
before,after).blueprintreview diff <before> <after>shows the change.
8. The edit server
With the editor open, the plugin runs a local HTTP server, so a plan applies in seconds rather than after a commandlet's start-up. The change goes into the editor's undo history, and you review it there.
- Binding: it listens on
127.0.0.1only, on port 30910 or the next free one. - Token: every request needs
Authorization: Bearer <token>, with the token from<Project>/Saved/BlueprintEdit/server.json. Anything else gets 401. - Settings: Editor Preferences > Plugins > Blueprint Edit Automation turns it off or changes the port.
BlueprintEdit.Server.StartandBlueprintEdit.Server.Stoptoggle it for the session.
| Route | |
|---|---|
POST /plan?dry_run=1&save=1&nonce=<hex> |
the body is a plan; the response is its report |
GET /export?asset=/Game/Dir/BP_Foo |
the asset's canonical JSON |
GET /ops |
the op table |
GET /status |
engine, project, process id, whether Play In Editor runs |
Plans are refused while Play In Editor runs. Any process that can read your project's Saved folder can use the
server, so turn it off on a shared machine.
9. Not covered
- Graphs that aren't Blueprint script graphs: AnimGraph and state machines, the Widget Designer, Behavior Trees, materials and Niagara. Their editors use other schemas and node models.
- Creating Widget or Anim Blueprints.
- Breakpoints and watches, which are debug state rather than asset state.