🧠 Brains complement: IF an unreal-engine-skills-manager tool (external MCP) exists in this
session, call it with {action: "load", skill: "ai-and-navigation"} for UE domain knowledge on
Behavior Trees — architecture, node semantics, best practices. If no such tool is available, skip
this line entirely and proceed with this skill alone — do NOT attempt the call.
Behavior Trees Skill
Two services, reachable from Python (unreal.BehaviorTreeService.*, unreal.BlackboardService.*)
and as AICallable tools:
BlackboardService → UBlackboardData assets: create, key CRUD, metadata, re-parenting
BehaviorTreeService → UBehaviorTree assets: structure, sub-nodes, properties, build, validate
Discover exact signatures with discover_python_class('unreal.BehaviorTreeService') — this skill
teaches the concepts and the gotchas, not the call shapes.
The golden workflow
import unreal
BT = unreal.BehaviorTreeService
BB = unreal.BlackboardService
# 1. Blackboard first — key bindings resolve against the TREE'S board, so assign it at creation.
BB.create_blackboard("/Game/AI/BB_Guard")
BB.add_blackboard_key("/Game/AI/BB_Guard", "TargetActor", "Object")
BB.set_blackboard_key_object_class("/Game/AI/BB_Guard", "TargetActor", "/Script/Engine.Actor")
# 2. Tree on that board.
BT.create_behavior_tree("/Game/AI/BT_Guard", "/Game/AI/BB_Guard")
# 3. Structure. Every mutator returns the new node's PATH (or "ERROR: ...").
sel = BT.add_node("/Game/AI/BT_Guard", "Root", "BTComposite_Selector") # Root/Selector[0]
seq = BT.add_node("/Game/AI/BT_Guard", sel, "BTComposite_Sequence")
wait = BT.add_node("/Game/AI/BT_Guard", seq, "BTTask_Wait")
# 4. Properties and bindings.
BT.set_node_property_value("/Game/AI/BT_Guard", wait, "WaitTime", '(DefaultValue=3.5,Key="")')
dec = BT.add_decorator("/Game/AI/BT_Guard", seq, "BTDecorator_Blackboard")
BT.set_node_blackboard_key("/Game/AI/BT_Guard", dec, "BlackboardKey", "TargetActor")
# 5. Verify.
print(BT.validate_tree("/Game/AI/BT_Guard")) # [] == clean
print(BT.get_tree("/Game/AI/BT_Guard")) # whole tree as JSON
Every mutator saves the asset itself — there is no separate "commit" step to forget.
compile_and_save exists for re-running layout + save on a tree edited some other way.
Node paths — the addressing scheme
Root/Selector[0]/Sequence[1]/Wait[0] — segments are node TITLES with an index among
same-named siblings. Root is a keyword (the graph root), not a title.
- Sub-nodes:
<owner path>/@decorator[n] / <owner path>/@service[n] — by slot, in order.
- Paths are positional, not identities. Inserting/removing a sibling renumbers later ones;
renaming a node changes its segment. Hold the node's
guid (from GetTree/GetNodeInfo) across
edits, and re-read GetTree after a rename. set_node_name refuses names the grammar cannot
express (/, leading @, trailing [n]) — on both the direct route and via the NodeName
property.
Things that are not obvious and will bite you
- WaitTime and friends are structs. In UE 5.8 many numeric BT properties are
FValueOrBBKey_*: pass '(DefaultValue=3.5,Key="")', not '3.5'. Read the current value
first (get_node_property_value) and mirror its shape.
- Blackboard key selectors are not values. Always
set_node_blackboard_key, never
set_node_property_value, for anything get_node_property_names flags
bIsBlackboardKeySelector. The service validates the key's TYPE against the selector's filter
up front — a mistyped binding would otherwise resolve, read back fine, and be silently cleared
by the engine on the next save.
- SimpleParallel has two fixed slots, not a child list: slot 0 = main task (tasks only),
slot 1 = background branch. The engine refills an empty background slot with a Wait task on
every save, so the background branch can only ever be REPLACED (
add_node(parallel, cls, 1)),
never removed or moved away.
- Root-level decorators go on the top composite (the node under Root), not on
Root itself —
add_decorator(asset, "Root/Selector", ...) is the correct call and the refusal message on
Root says so.
set_node_property_value verifies after the save. ValueAfterWrite is re-read from the
asset, not echoed; if the engine rewrote the value at save time the call reports failure and
tells you what is actually on disk. Trust it.
- Injected nodes (from RunBehavior subtrees) are read-only everywhere. Edit the subtree asset.
- Comments:
set_node_comment writes the graph comment bubble — annotation only, survives
GetTree→BuildTree round-trips. Use it to record WHY a subtree exists.
Round-trips, repair, safety
get_tree → edit the JSON → build_tree(target, json, bReplaceExisting) replays it through
the ordinary mutators. One save per node/property — fine for tens of nodes, not a bulk-import
path for thousands. With bReplaceExisting=true the result's PreReplaceSnapshot holds the
target's previous tree; if the build fails midway, feed that snapshot back to build_tree to
restore.
- Composite (logic-operator) decorators cannot be recreated by BuildTree (editor-only sub-graphs;
reported per-node) — author those in the BT editor.
- Writes are refused while PIE runs, while an editor is open on the asset, and when the commit
would destroy or crash on the runtime tree (sparse graph / instance-less root). Refusals name
the mechanism and the fix; read them.
repair_graph_from_runtime_tree rebuilds a sparse editor graph from the runtime tree —
explicit, never implicit. validate_tree is the read-only diagnostic sweep (orphans, broken
key bindings at any nesting depth, empty composites, misplaced sub-nodes).
Blackboards beyond key CRUD
set_blackboard_key_instance_synced, set_blackboard_key_metadata (category + description),
set_blackboard_parent (cycle- and shadowing-checked) — all post-creation editable.
find_blackboard_key_references(board, key) lists every BT selector bound to a key, formatted
<btPath>:<node>.<property> — run it BEFORE remove_blackboard_key or
rename_blackboard_key; rename returns the same list after the fact (those bindings broke).
SelfActor on a parent-less board is engine-persistent and cannot be removed.
- Blueprint node classes (
BTT_*, BTD_*, BTS_*) are created with the blueprints skill
(parent class BTTask_BlueprintBase etc.); get_available_node_types lists them once they
exist, and add_node resolves them by short name, _C name, or full object path.
1---2name: behavior-trees3description: Create, inspect, and edit Behavior Tree and Blackboard assets — composites, tasks, decorators, services, node properties, blackboard key bindings, key CRUD and metadata, tree round-trips and validation (BehaviorTreeService + BlackboardService). Use when the user asks to build a Behavior Tree, add BT nodes or decorators, bind blackboard keys, author a blackboard, or validate/repair a BT asset.4---56> 🧠 **Brains complement:** IF an `unreal-engine-skills-manager` tool (external MCP) exists in this7> session, call it with `{action: "load", skill: "ai-and-navigation"}` for UE domain knowledge on8> Behavior Trees — architecture, node semantics, best practices. If no such tool is available, skip9> this line entirely and proceed with this skill alone — do NOT attempt the call.1011# Behavior Trees Skill1213Two services, reachable from Python (`unreal.BehaviorTreeService.*`, `unreal.BlackboardService.*`)14and as AICallable tools:1516```17BlackboardService → UBlackboardData assets: create, key CRUD, metadata, re-parenting18BehaviorTreeService → UBehaviorTree assets: structure, sub-nodes, properties, build, validate19```2021Discover exact signatures with `discover_python_class('unreal.BehaviorTreeService')` — this skill22teaches the concepts and the gotchas, not the call shapes.2324## The golden workflow2526```python27import unreal28BT = unreal.BehaviorTreeService29BB = unreal.BlackboardService3031# 1. Blackboard first — key bindings resolve against the TREE'S board, so assign it at creation.32BB.create_blackboard("/Game/AI/BB_Guard")33BB.add_blackboard_key("/Game/AI/BB_Guard", "TargetActor", "Object")34BB.set_blackboard_key_object_class("/Game/AI/BB_Guard", "TargetActor", "/Script/Engine.Actor")3536# 2. Tree on that board.37BT.create_behavior_tree("/Game/AI/BT_Guard", "/Game/AI/BB_Guard")3839# 3. Structure. Every mutator returns the new node's PATH (or "ERROR: ...").40sel = BT.add_node("/Game/AI/BT_Guard", "Root", "BTComposite_Selector") # Root/Selector[0]41seq = BT.add_node("/Game/AI/BT_Guard", sel, "BTComposite_Sequence")42wait = BT.add_node("/Game/AI/BT_Guard", seq, "BTTask_Wait")4344# 4. Properties and bindings.45BT.set_node_property_value("/Game/AI/BT_Guard", wait, "WaitTime", '(DefaultValue=3.5,Key="")')46dec = BT.add_decorator("/Game/AI/BT_Guard", seq, "BTDecorator_Blackboard")47BT.set_node_blackboard_key("/Game/AI/BT_Guard", dec, "BlackboardKey", "TargetActor")4849# 5. Verify.50print(BT.validate_tree("/Game/AI/BT_Guard")) # [] == clean51print(BT.get_tree("/Game/AI/BT_Guard")) # whole tree as JSON52```5354Every mutator saves the asset itself — there is no separate "commit" step to forget.55`compile_and_save` exists for re-running layout + save on a tree edited some other way.5657## Node paths — the addressing scheme5859- `Root/Selector[0]/Sequence[1]/Wait[0]` — segments are node TITLES with an index among60 same-named siblings. `Root` is a keyword (the graph root), not a title.61- Sub-nodes: `<owner path>/@decorator[n]` / `<owner path>/@service[n]` — by slot, in order.62- **Paths are positional, not identities.** Inserting/removing a sibling renumbers later ones;63 renaming a node changes its segment. Hold the node's `guid` (from GetTree/GetNodeInfo) across64 edits, and re-read GetTree after a rename. `set_node_name` refuses names the grammar cannot65 express (`/`, leading `@`, trailing `[n]`) — on both the direct route and via the `NodeName`66 property.6768## Things that are not obvious and will bite you6970- **WaitTime and friends are structs.** In UE 5.8 many numeric BT properties are71 `FValueOrBBKey_*`: pass `'(DefaultValue=3.5,Key="")'`, not `'3.5'`. Read the current value72 first (`get_node_property_value`) and mirror its shape.73- **Blackboard key selectors are not values.** Always `set_node_blackboard_key`, never74 `set_node_property_value`, for anything `get_node_property_names` flags75 `bIsBlackboardKeySelector`. The service validates the key's TYPE against the selector's filter76 up front — a mistyped binding would otherwise resolve, read back fine, and be silently cleared77 by the engine on the next save.78- **SimpleParallel has two fixed slots**, not a child list: slot 0 = main task (tasks only),79 slot 1 = background branch. The engine refills an empty background slot with a Wait task on80 every save, so the background branch can only ever be REPLACED (`add_node(parallel, cls, 1)`),81 never removed or moved away.82- **Root-level decorators go on the top composite** (the node under Root), not on `Root` itself —83 `add_decorator(asset, "Root/Selector", ...)` is the correct call and the refusal message on84 `Root` says so.85- **`set_node_property_value` verifies after the save.** `ValueAfterWrite` is re-read from the86 asset, not echoed; if the engine rewrote the value at save time the call reports failure and87 tells you what is actually on disk. Trust it.88- **Injected nodes (from RunBehavior subtrees) are read-only everywhere.** Edit the subtree asset.89- **Comments**: `set_node_comment` writes the graph comment bubble — annotation only, survives90 GetTree→BuildTree round-trips. Use it to record WHY a subtree exists.9192## Round-trips, repair, safety9394- `get_tree` → edit the JSON → `build_tree(target, json, bReplaceExisting)` replays it through95 the ordinary mutators. One save per node/property — fine for tens of nodes, not a bulk-import96 path for thousands. With `bReplaceExisting=true` the result's `PreReplaceSnapshot` holds the97 target's previous tree; if the build fails midway, feed that snapshot back to `build_tree` to98 restore.99- Composite (logic-operator) decorators cannot be recreated by BuildTree (editor-only sub-graphs;100 reported per-node) — author those in the BT editor.101- Writes are refused while PIE runs, while an editor is open on the asset, and when the commit102 would destroy or crash on the runtime tree (sparse graph / instance-less root). Refusals name103 the mechanism and the fix; read them.104- `repair_graph_from_runtime_tree` rebuilds a sparse editor graph from the runtime tree —105 explicit, never implicit. `validate_tree` is the read-only diagnostic sweep (orphans, broken106 key bindings at any nesting depth, empty composites, misplaced sub-nodes).107108## Blackboards beyond key CRUD109110- `set_blackboard_key_instance_synced`, `set_blackboard_key_metadata` (category + description),111 `set_blackboard_parent` (cycle- and shadowing-checked) — all post-creation editable.112- `find_blackboard_key_references(board, key)` lists every BT selector bound to a key, formatted113 `<btPath>:<node>.<property>` — run it BEFORE `remove_blackboard_key` or114 `rename_blackboard_key`; rename returns the same list after the fact (those bindings broke).115- `SelfActor` on a parent-less board is engine-persistent and cannot be removed.116- Blueprint node classes (`BTT_*`, `BTD_*`, `BTS_*`) are created with the **blueprints** skill117 (parent class `BTTask_BlueprintBase` etc.); `get_available_node_types` lists them once they118 exist, and `add_node` resolves them by short name, `_C` name, or full object path.