Summary
BlueprintTools.read_graph_dsl (and its counterpart write_graph_dsl) silently drops the third and any additional inputs when a Blueprint operator node has more than two inputs (A/B/C/...).
This affects n-ary / variadic operator nodes — e.g. BooleanAND, BooleanOR, Add (+), Subtract (-), Multiply (*) — which Unreal allows to carry more than two pins.
Example: a Multiply node with three inputs should decompile to:
(* A B C)
but instead returned (before the fix):
(* A B)
…with the third operand silently missing. Any AI/agent or tooling that reads the Blueprint through read_graph_dsl therefore receives an incorrect, lossy representation of the graph — a real correctness hazard (exactly the kind of thing that has historically caused “AI wrote the wrong logic” incidents).
What Type of Bug are you experiencing?
Editor
Steps to Reproduce
Provide a numbered list of steps that we can follow to reproduce this issue:
- In a UE 5.8 project, open a Blueprint function that contains any operator node with 3+ inputs. For example:
Math | Boolean | ANDBooleanwith three inputs (A/B/C), orUtilities | Operators | Multiply(orAdd/Subtract) with three inputs.
- Query that function graph with Unreal MCP (
http://localhost:8000/mcp) by calling:
(ReplaceBlueprintTools.read_graph_dsl(graph={"refPath": "/Game/YourPath/YourBP.YourBP:YourFunction"})YourPath/YourBP/YourFunctionwith an actual asset/function that contains a 3+ input operator node.) - Inspect the returned DSL for the operator node.
- If any extra input (
B/C/D…) is actually wired in the Blueprint editor, none of the inputs after the second one is reflected in the output.
Concrete minimal reproduction (observed on this machine):
- A
Multiplynode (Utilities | Operators | Multiply) with three wired inputs(A, B, C).
Expected Result
Please provide what you think the expected behavior would be when following the steps listed above:
read_graph_dslshould emit every wired input of an n-ary operator node.- For the three-input
Multiplyexample, the expected DSL is:
i.e. all three operands present, and in a form that(* A B C)write_graph_dslcan round-trip back to the same three-input node.
Observed Result
Please provide the actual result of the steps listed above:
- Only the first two inputs are emitted; the 3rd+ input is silently dropped.
- For the three-input
Multiplyexample, the actual output (before the fix) is:(* A B) - The third operand is missing with no warning or error, and the same failure affects
BooleanAND/BooleanOR,Add,Subtract, andMultiply— i.e. the whole n-ary operator family.
Note: verified against a real UE 5.8 Blueprint — a 3-input
Multiplynode. Before the fix it decompiled to(* A B)and dropped the third operand; after the fix it correctly returns(* A B C).
Affects Versions
5.8
Platform(s)
Windows
Additional Notes
Root Cause
The problem is in the engine’s experimental plugin:
Engine/Plugins/Experimental/Toolsets/EditorToolset/Content/Python/editor_toolset/toolsets/blueprint_dsl.py
Two places hard-code the assumption that every operator node is strictly binary (exactly two inputs):
1. Decompiler (read_graph_dsl direction) — _emit_binop_expr
def _emit_binop_expr(self, type_id, src_ni, bindings, _visiting) -> str:
op = _TYPE_ID_TO_OP[type_id]
ins = _data_ins(src_ni) # returns ALL non-exec input pins
left = self._emit_expr(ins[0], ...) if len(ins) > 0 else '_'
right = self._emit_expr(ins[1], ...) if len(ins) > 1 else '_'
...
return f'({op} {left} {right})'
_data_ins() already returns every non-exec input pin, but _emit_binop_expr only ever reads ins[0] / ins[1], so ins[2:] (the 3rd+ inputs) are discarded.
2. Transpiler (write_graph_dsl direction) — _eval_binop
if len(form) != 3:
raise RuntimeError(f'({op} ...) requires exactly 2 arguments, got {len(form) - 1}')
...
if len(ins) > 1:
self._wire_with_pre(form[2], ins[1], pre2, bindings, ctx)
The transpiler rejects any form with more than two arguments, so even if you hand-write (* a b c), it raises. This also breaks round-tripping (read_graph_dsl → write_graph_dsl).
Fix
The operator family in _BIN_OPS is n-ary in Unreal — many of these nodes support arbitrary input count via add_node_pin/remove_node_pin. The fix makes both directions handle any number of inputs:
Decompiler (_emit_binop_expr)
Emit every connected input, while preserving the existing unary-minus folding (0 - x) → (- x):
def _emit_binop_expr(self, type_id, src_ni, bindings, _visiting) -> str:
op = _TYPE_ID_TO_OP[type_id]
ins = _data_ins(src_ni)
args = [self._emit_expr(p, bindings, _visiting) for p in ins]
args = args if args else ['_']
if op == '-' and len(args) == 2 and args[0] == '0':
return f'(- {args[1]})' # preserve unary minus folding
return f'({op} {" ".join(args)})'
Transpiler (_eval_binop)
Accept any number of arguments (≥2); wire the first two into the node’s initial pins, and use add_node_pin for each extra argument to dynamically add an input pin and wire it:
# accept any argument count >= 2 (old: `if len(form) != 3: raise ...`)
if len(form) < 3:
raise RuntimeError(f'({op} ...) requires at least 2 arguments, got {len(form) - 1}')
...
# after wiring ins[0] and ins[1]:
if len(form) > 3:
for extra in form[3:]:
pre = self._eval_expr(extra, bindings, ctx) if isinstance(extra, list) else None
new_pin = self._add_node_pin(node) # dynamic input pin
pin_info = PinInfo()
pin_info.pin_id = new_pin
self._wire_with_pre(extra, pin_info, pre, bindings, ctx)
Wiring add_node_pin into the transpiler
Transpiler.__init__ gains an optional add_node_pin_fn callback, and BlueprintTools.write_graph_dsl passes it:
blueprint_dsl.Transpiler(
graph,
BlueprintTools.create_node,
BlueprintTools.connect_pins,
BlueprintTools._get_node_info,
BlueprintTools.set_pin_value,
lambda g: BlueprintTools.find_nodes(g),
delete_node_fn=BlueprintTools.delete_node,
find_node_types_fn=lambda f: BlueprintTools.find_node_types(graph, f),
add_node_pin_fn=BlueprintTools.add_node_pin, # <-- added
).transpile(code)
Note:
BlueprintTools.add_node_pinreturns a rawPinID; it is wrapped in aPinInfobefore being handed to the shared_wire_with_prehelper.
Because _BIN_OPS in UE includes all extensible operators (Add, Subtract, Multiply, BooleanAND/OR/XOR, …), this covers the whole family — not just booleans. Operators that are genuinely fixed-two-input (e.g. comparisons) are unaffected because they never have >2 arguments.
Verification
After applying the fix (as a plugin fork — see below) and restarting the editor, the same three-input Multiply node from the reproduction case now decompiles correctly:
(fn TestFn ()
(return (* A B C)))
All three operands are present. A standalone smoke test over _emit_binop_expr covering 2- and 3-input +, -, *, and, or (plus the 0 - x → (- x) unary case) passes.
How to apply the fix without modifying the engine
- Do not edit the engine directly (
Engine/Plugins/Experimental/Toolsets/EditorToolset/...). - Fork the experimental plugin into your project at
{Project}/Plugins/EditorToolset/(uplugin +Source/+Content/), which takes precedence over the built-in engine plugin, then apply the patch to the project copy’sContent/Python/editor_toolset/toolsets/blueprint_dsl.py. - No C++ compilation needed (pure Python), but restart the editor for the Python module to reload.