Summary
The EditorToolset experimental plugin (UE 5.8, Engine/Plugins/Experimental/Toolsets/EditorToolset) exposes a Blueprint-graph DSL: write_graph_dsl(graph, code) writes a graph and read_graph_dsl(graph) reads it back. Its docstring states the read output “can be edited and passed back to write_graph_dsl to modify the graph” — i.e. a round-trip contract.
The round-trip is broken for any graph containing an exec node with two or more data output pins where a downstream node consumes a NON-FIRST output. The decompiler emits a single (bind name (Call ...)) form whose variable name is taken from the actually-wired output pin, but the DSL’s single-target bind always assigns the name to the node’s first data output. On write-back the name is therefore bound to the wrong pin, and wiring it into the original consumer fails with a pin type-mismatch — so read_graph_dsl output is not a valid write_graph_dsl input.
Because virtually every real gameplay graph contains multi-output nodes (casts, Map Find, break/get nodes, any function returning bool bSuccess + a value, etc.), the documented “read → edit → write” editing workflow is unusable in practice.
What Type of Bug are you experiencing?
Foundation (C++ Tools, Profiling, & Pipeline)
Steps to Reproduce
- In any Blueprint, add a function (or use any engine node) with two data output pins where the FIRST output is NOT the one you use — e.g. a function
GetThingwith outputs(bool DidSucceed, <SomeStruct> Value). - In the Event Graph, call it from an exec chain and wire ONLY the second output (
Value) into a downstream node that consumes that type (leaveDidSucceedunconnected). - Call
read_graph_dsl(EventGraph)— note the emitted form is a single bind, e.g.(bind _value (GetThing ...)). - Pass that exact returned DSL string straight back into
write_graph_dsl(EventGraph, <that string>). - write_graph_dsl raises a pin-connection error (below) — the round-trip fails.
Expected Result
read_graph_dsl output is always a valid write_graph_dsl input (the documented round-trip). For a node whose consumed output is not the first data output, the reader should emit a positional destructuring bind — e.g. (bind (_didsucceed _value) (GetThing ...)) — so that write-back binds _value to the correct (second) data output. No error.
Observed Result
read_graph_dsl emits a single (bind _value (GetThing ...)). On write-back, the DSL binds _value to the node’s FIRST data output (DidSucceed, a bool) instead of the second (Value); wiring _value into the original struct/typed consumer then fails:
Could not connect pin DidSucceed to <ConsumerInput>. The pins may be incompatible types.
The write is transactional (the graph is left intact on failure), but the round-trip is impossible for any graph with such a node.
Affects Versions
5.8
Platform(s)
Windows
For crash reports, include your callstack
write_graph_dsl error:
Could not connect pin DidSucceed to <ConsumerInput>. The pins may be incompatible types.
read_graph_dsl emitted (single bind, name from the wired 2nd output but bound to the 1st on write-back):
(event EventBeginPlay
(bind _value (GetThing ...))
(<consumer> _value))
Additional Notes
Root-cause analysis (from the 5.8 EditorToolset Python source)
File: Engine/Plugins/Experimental/Toolsets/EditorToolset/Content/Python/editor_toolset/toolsets/blueprint_dsl.py
-
Decompiler (read side).
Decompiler._collect_assigned_outputs(~L2256-2268) walks the node’s data outputs and, for each output that is consumed downstream (needed_outputs), allocates a name — but it returns a flat list of names with no positional information._emit_call_stmt(~L2278-2291) then does:
lhs = '(' + ' '.join(assigned) + ')' if len(assigned) > 1 else assigned[0]
So when exactly ONE output is consumed, it emits a single bind(bind <name> (Call ...)), regardless of WHICH data-output index that name corresponds to. -
Transpiler (write side).
BlueprintGraphTranspiler._process_bind(~L1334-1363):- Multi-target
(bind (a b) node)binds positionally over_data_outs(ni):for lhs, pin in zip(target, outs): bindings[str(lhs)] = pin.pin_id. - Single-target
(bind name node)binds toself._eval_expr(...)→_eval_node_call→ the first data output PinID (~L1141-1142).
- Multi-target
-
The mismatch. When the consumed output is not the first data output, the reader emits a single bind (step 1) that the writer maps to the first output (step 2). The name (read from the real pin) is now bound to the wrong pin → downstream wiring fails with a pin type mismatch.
Note the writer already supports positional destructuring with placeholders — _data_outs order is identical on both sides ([p for p in output_pins if type_id != EXEC and name != 'OutputDelegate'], L614-616), so the fix is entirely on the read side.
Suggested fix direction
In the decompiler, emit a positional destructuring bind covering data-output indices 0..maxNeededIndex, filling consumed outputs with their names and un-consumed leading outputs with unique placeholder names (allocate via the existing _alloc_tmp, which guarantees uniqueness and avoids the “cannot rebind” collision). Only fall back to a single (bind name ...) when the sole consumed output is data-output index 0. This makes every read_graph_dsl output a valid write_graph_dsl input.
Workaround (verified)
Do not rely on the read→edit→write round-trip for graphs with multi-output nodes. To modify an existing graph, use the additive node API (create_node / connect_pins) and leave existing chains untouched; use read_graph_dsl for read-only inspection only.
Environment
UE 5.8.0 (binary Launcher build), Windows 11 Pro. EditorToolset experimental plugin, driven via the ModelContextProtocol MCP server (BlueprintTools.read_graph_dsl / write_graph_dsl). Reproduced independently on a scratch Actor Blueprint.