Programming Guide#

Purpose: Task-oriented example scripts for common Behavior Tree workflows — running trees, building descriptors programmatically, creating node libraries, and scripting blackboards. For the complete API surface, see the Behavior Tree API Reference. For the visual authoring workflow, see the User Guide.

Tree Execution#

USD-Managed Trees#

The primary way to run behavior trees is through the UI and USD:

  1. In the tree editor, open a behavior tree JSON file and add it to compatible prims.

  2. Press Play — the runtime creates tree instances and ticks them each frame with the timeline.

From Python, use Kit commands to set up tree prims:

import omni.kit.commands
from pxr import Sdf

# Apply BehaviorTreeAPI to a prim with a descriptor file
omni.kit.commands.execute(
    "ApplyBehaviorTreeAPICommand",
    prim_path=Sdf.Path("/World/MyRobot"),
    tree_file_path=Sdf.AssetPath("path/to/tree.json"),
)

# Create a blackboard prim
omni.kit.commands.execute("CreateBlackboardCommand")

During Play, access live trees through the runtime:

import omni.behavior.tree.core as bt

runtime = bt.get_bt_runtime()
tree = runtime.get_tree("/World/MyRobot")
if tree:
    bb = tree.get_blackboard()
    bb["target"] = "/World/Goal"

The runtime also manages stage-scoped node libraries (BehaviorTreeNodeLibrary prims) and can reload them at runtime.

See Kit Commands for the full list of available commands.

Manual Tree Execution#

For advanced use cases — unit tests, headless simulation, or trees not tied to USD — create and tick trees directly through the factory:

import omni.behavior.tree.core as bt

factory = bt.get_factory()

# Build a tree programmatically
lib = factory.get_node_library("omni.behavior.tree.core").get()
seq = lib.get_node_type_handle("Sequence")
wait = lib.get_node_type_handle("Wait")

desc = factory.create_descriptor()
desc.push(seq, "root")
desc.add(wait, "step1")
desc.add(wait, "step2")
desc.pop()

tree = factory.create_tree(desc)
while tree.get_status() != bt.NodeStatus.SUCCESS:
    tree.tick(delta_time=1.0 / 60.0)

Trees can also be created from JSON:

json_str = open("tree.json").read()
desc = factory.create_descriptor_from_string(json_str)
tree = factory.create_tree(desc)

Building a Patrol Tree#

This example builds the following tree programmatically, combining core composites with behavior simulation nodes:

Patrol tree: Sequence with Repeat, MoveTo with RandomNavMeshPoint, and Wait

The agent picks a random point on the NavMesh, walks there, pauses, and repeats forever.

Note

The omni.anim.behavior.tree extension must be enabled for its node library to be registered. The get_node_library() call and all type handles obtained from it will fail if the extension is not loaded.

import omni.behavior.tree.core as bt

factory = bt.get_factory()

# --- 1. Acquire node libraries and type handles ---

core = factory.get_node_library("omni.behavior.tree.core").get()
anim = factory.get_node_library("omni.anim.behavior.tree").get()

seq_type      = core.get_node_type_handle("Sequence")
wait_type     = core.get_node_type_handle("Wait")
move_to_type  = anim.get_node_type_handle("MoveTo")

repeat_type    = core.get_modifier_type_handle("Repeat")
rand_pt_type   = anim.get_modifier_type_handle("RandomNavMeshPoint")

# --- 2. Build the tree topology ---
# push() opens a scope for children, add() creates a leaf, pop() closes.

desc = factory.create_descriptor()

root = desc.push(seq_type, "patrol")       # Sequence
go   = desc.add(move_to_type, "go")        #   MoveTo
wait = desc.add(wait_type, "pause")        #   Wait
desc.pop()                                 # close Sequence

# --- 3. Set port values on nodes ---
# override_node_instance_port(node, port_name, value_type, value)

desc.override_node_instance_port(
    wait, "duration", bt.NodePortValueType.VALUE, 2.0
)

# --- 4. Attach modifiers ---
# attach_modifier(owner, modifier_type, name, abort_mode)

rpt = desc.attach_modifier(root, repeat_type, "loop")

# count=0 means repeat forever
desc.override_modifier_port(
    rpt, "count", bt.NodePortValueType.VALUE, 0
)

rand_pt = desc.attach_modifier(go, rand_pt_type, "pick_dest")

# --- 5. Wire modifier output → node input ---
# RandomNavMeshPoint produces "point"; feed it into MoveTo's "target".

desc.override_node_instance_port_modifier(
    go, "target", "pick_dest", "point"
)

# --- 6. Save and apply to a prim ---
# Character animation nodes run through the USD runtime, so serialize
# the descriptor to a file and attach it to the agent prim.

import omni.kit.commands
from pxr import Sdf

json_str = factory.serialize_descriptor(desc)
desc_path = "/path/to/patrol_tree.json"
with open(desc_path, "w") as f:
    f.write(json_str)

omni.kit.commands.execute(
    "ApplyBehaviorTreeAPICommand",
    prim_path=Sdf.Path("/World/Character/SkelRoot"),
    tree_file_path=Sdf.AssetPath(desc_path),
)

# --- 7. Play the timeline ---
# The runtime ticks all trees automatically each frame.

import omni.timeline
timeline = omni.timeline.get_timeline_interface()
timeline.play()

Creating Node Libraries#

Node libraries declare the node types available to tree descriptors. Libraries are owned by Omniverse extensions that manage their lifecycle.

C++ Extension#

For best performance, implement nodes as a C++ Carbonite plugin. Define nodes with BT_NODE_DESC (see C++ API) and register them via BehaviorNodeLibraryBuilder. A thin Python extension layer calls into the plugin to manage the lifecycle.

C++ plugin:

// nodes/GoToAction.inl
struct GoToAction : public IBehaviorActionNode
{
    BT_NODE_DESC(
        type = "GoTo",
        doc = "Move agent toward target position",
        constraints = BT_CONSTRAINTS("BehaviorAgentBaseAPI"),
        ports = BT_PORTS(
            valuePort("target", PortValue({ eFloat3, eSdfPath }, Variant{})),
            valuePort("speed", 1.0f)))

    NodeStatus onTick(IBehaviorActionNodeContext& ctx) override { /* ... */ }
};

// Extension.cpp
static UniqueNodeLibraryPtr g_library;

void registerNodeLibrary()
{
    g_library = BehaviorNodeLibraryBuilder("my_extension.nodes")
                    .registerNode<GoToAction>()
                    .validate()
                    .build();
}

void unregisterNodeLibrary()
{
    g_library.reset();
}

Python extension — acquires the plugin interface and calls register/unregister:

# extension.py
import omni.ext
import my_extension.bindings as _bindings

class MyNodesExtension(omni.ext.IExt):
    def on_startup(self, ext_id):
        self._iface = _bindings.acquire_interface()
        self._iface.register_node_library()

    def on_shutdown(self):
        self._iface.unregister_node_library()
        _bindings.release_interface(self._iface)
        self._iface = None

Python Extension#

For rapid prototyping or ad-hoc nodes that don’t need native performance, implement nodes in Python. The extension creates the library on startup, registers decorated node classes, and releases the library on shutdown.

# extension.py
import omni.ext
import omni.behavior.tree.core as bt

from .my_nodes import GoTo, Patrol

class MyNodesExtension(omni.ext.IExt):
    def on_startup(self, ext_id):
        factory = bt.get_factory()
        self._lib = factory.create_node_library("my_extension.nodes")
        bt.register_node(self._lib, GoTo)
        bt.register_node(self._lib, Patrol)

    def on_shutdown(self):
        self._lib = None  # releases the library

Node classes are defined in separate modules using the @bt.node decorator. Alternatively, the extension can load a script file with bind_node_library_with_script:

import pathlib

script = (pathlib.Path(__file__).parent / "my_nodes.py").read_text()
factory.bind_node_library_with_script(self._lib, script)

Get and Set Stage Blackboard Values#

A BehaviorTreeBlackboard prim has two representations, depending on whether the timeline is playing:

  • During Simulation — the runtime creates a live IBehaviorBlackboard from the prim. Look it up by prim path and use dict-like access. The live blackboard is destroyed when the timeline stops; changes made during Play are not written back to the prim.

  • While stopped — only the persisted state exists: a JSON string in the prim’s data attribute. Deserialize it into a blackboard through the factory, modify it, and serialize it back.

Get and set values on the live blackboard during Play:

import omni.behavior.tree.core as bt
from pxr import Sdf

runtime = bt.get_bt_runtime()

# Returns None unless the timeline is playing
bb = runtime.get_blackboard("/World/BehaviorTreeBlackboard")
if bb:
    # Read values
    for key in bb.get_keys():
        print(key, "=", bb[key])

    # Write values — nodes reading these keys pick them up on their next tick
    bb["count"] = 42
    bb["target_prim"] = Sdf.Path("/World/Goal")

Get and set the values persisted on the prim while the timeline is stopped:

import carb
import omni.behavior.tree.core as bt
import omni.usd
from pxr import Sdf

stage = omni.usd.get_context().get_stage()
prim = stage.GetPrimAtPath("/World/BehaviorTreeBlackboard")

# Deserialize the prim's data into a blackboard (empty data -> fresh blackboard)
factory = bt.get_factory()
data = bt.get_blackboard_data(prim)
bb = factory.create_blackboard_from_string(data) if data else factory.create_blackboard()

# Read values
for key in bb.get_keys():
    print(key, "=", bb[key])

# Set values
bb["count"] = 42
bb["position"] = carb.Float3(1.0, 2.0, 3.0)
bb["name"] = "robot"
bb["target_prim"] = Sdf.Path("/World/Target")

# Serialize back to the prim — this is what the runtime reads on the next Play
bt.set_blackboard_data(prim, factory.serialize_blackboard(bb))

A live blackboard can hold any Python object. Persisting to the prim is more restrictive: every value must have a serializable variant type — numbers, booleans, strings, carb.Float2/3/4, Sdf.Path, lists of these, or a custom variant type whose serializer is registered through the C++ registerVariantSerializer factory API. serialize_blackboard raises an error if the blackboard contains any other value, such as a plain Python object.