Nav2 Behavior Tree Guide 2026
Master Nav2's Behavior Tree system: BT.cpp fundamentals, XML tree files, ComputePathToPose and FollowPath nodes, custom C++ plugins, Blackboard data flow, recovery behaviors, and Groot2 debugging.
Why Nav2 Uses Behavior Trees
Nav2 replaced hand-coded state machines with Behavior Trees because BTs are modular, reusable, and inspectable. You can modify the robot's navigation strategy without touching C++ source — just edit an XML file and re-launch. Recovery behaviors (spin, back up, clear costmap) compose naturally as BT subtrees. Groot2 renders the live tree so you can see exactly which node is executing and why navigation failed.
Behavior Tree Fundamentals
A Behavior Tree (BT) is a tree of nodes that tick from the root each cycle. Each node returns Success, Failure, or Running. Nav2 uses BT.cpp as its BT library.
Behavior Tree node types:
─────────────────────────────────────────────────────────────
Action node │ Does work (move robot, compute path)
│ Returns Running while executing, then Success/Failure
─────────────────────────────────────────────────────────────
Condition node │ Checks a condition (is goal reached? is battery low?)
│ Returns Success or Failure immediately (no Running)
─────────────────────────────────────────────────────────────
Control nodes │ Orchestrate children:
Sequence │ → tick children left-to-right, fail on first Failure
Fallback │ → tick children left-to-right, succeed on first Success
Parallel │ → tick all children simultaneously
─────────────────────────────────────────────────────────────
Decorator nodes │ Wrap one child:
Retry(N) │ → retry child up to N times on Failure
Inverter │ → flip Success↔Failure
ForceSuccess │ → always return Success regardless of child
─────────────────────────────────────────────────────────────
Tick mechanism:
root.tick() is called every BT cycle (typically 1-10 Hz in Nav2)
A Running node is "re-ticked" next cycle without restarting
A Halted node gets a halt() call when its branch is abandoned⚡ Nav2 uses BT.cpp (behaviortree.CPP v4). Install: sudo apt install ros-${ROS_DISTRO}-behaviortree-cpp-v3 (Humble) or ros-${ROS_DISTRO}-nav2-behaviors (Jazzy).
Nav2 BT XML File Structure
Nav2 reads behavior trees from XML files. The default BT is navigate_w_replanning_and_recovery.xml. You can write custom BTs or modify the default.
<!--
Custom Nav2 Behavior Tree — simple navigation with one recovery.
Save to: my_pkg/behavior_trees/navigate_simple.xml
-->
<root BTCPP_format="4" main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<!-- Fallback: try Navigate, fall back to Spin+Navigate -->
<Fallback name="NavigateOrRecover">
<!-- Sequence: compute path AND follow it -->
<Sequence name="NavigateWithReplanning">
<RateController hz="1.0">
<ComputePathToPose
goal="{goal}"
path="{path}"
planner_id="GridBased"
error_code_id="{compute_path_error_code}"/>
</RateController>
<FollowPath
path="{path}"
controller_id="FollowPath"
error_code_id="{follow_path_error_code}"/>
</Sequence>
<!-- Recovery: spin 1.57 rad, then retry navigate -->
<Sequence name="RecoverySequence">
<ClearEntireCostmap
name="ClearLocalCostmap-Recovery"
service_name="local_costmap/clear_entirely_local_costmap"/>
<Spin spin_dist="1.57" error_code_id="{spin_error}"/>
<Wait wait_duration="1.0"/>
</Sequence>
</Fallback>
</BehaviorTree>
</root>⚡ Variables in braces ({goal}, {path}) are BT Blackboard entries — shared memory between nodes. goal is written by bt_navigator from the action server goal.
Writing a Custom BT Node Plugin
Extend Nav2 with custom BT nodes by implementing BT::SyncActionNode or BT::StatefulActionNode and registering them as ROS 2 plugins.
#include "behaviortree_cpp_v3/action_node.h"
#include "rclcpp/rclcpp.hpp"
// ─── Custom condition: check if robot is near a charging station ───
class IsNearCharger : public BT::ConditionNode {
public:
IsNearCharger(const std::string& name, const BT::NodeConfiguration& config)
: BT::ConditionNode(name, config) {}
// Declare the ports (inputs and outputs)
static BT::PortsList providedPorts() {
return {
BT::InputPort<float>("threshold_dist", 0.5f, "Distance threshold in meters"),
};
}
BT::NodeStatus tick() override {
float threshold;
getInput("threshold_dist", threshold);
// --- your real distance check here ---
float dist_to_charger = computeDistToCharger();
if (dist_to_charger <= threshold) {
RCLCPP_INFO(rclcpp::get_logger("IsNearCharger"), "Near charger!");
return BT::NodeStatus::SUCCESS;
}
return BT::NodeStatus::FAILURE;
}
private:
float computeDistToCharger() { return 0.3f; /* stub */ }
};
// ─── Custom action: beep the robot buzzer ─────────────────────────
class BeepBuzzer : public BT::SyncActionNode {
public:
BeepBuzzer(const std::string& name, const BT::NodeConfiguration& config)
: BT::SyncActionNode(name, config) {}
static BT::PortsList providedPorts() {
return {
BT::InputPort<int>("beep_count", 1, "Number of beeps"),
};
}
BT::NodeStatus tick() override {
int count;
getInput("beep_count", count);
RCLCPP_INFO(rclcpp::get_logger("BeepBuzzer"), "BEEP x%d", count);
// --- publish to /buzzer topic here ---
return BT::NodeStatus::SUCCESS;
}
};
// ─── Register as Nav2 BT plugin ───────────────────────────────────
#include "behaviortree_cpp_v3/bt_factory.h"
BT_REGISTER_NODES(factory) {
factory.registerNodeType<IsNearCharger>("IsNearCharger");
factory.registerNodeType<BeepBuzzer>("BeepBuzzer");
}⚡ Add the plugin to CMakeLists.txt as a shared library and list it in bt_navigator's plugin_lib_names parameter. The plugin is loaded at runtime via pluginlib.
BT Blackboard — Shared Memory Between Nodes
The Blackboard is a key-value store shared across all BT nodes in a tree. Use it to pass data (path, goal, error codes) between Action and Condition nodes.
<!-- Blackboard entries are referenced as {key_name} in XML -->
<root BTCPP_format="4" main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence>
<!-- ComputePathToPose WRITES {path} to the blackboard -->
<ComputePathToPose
goal="{goal}"
path="{path}"
planner_id="GridBased"
error_code_id="{compute_path_error_code}"/>
<!-- FollowPath READS {path} from the blackboard -->
<FollowPath
path="{path}"
controller_id="FollowPath"
error_code_id="{follow_path_error_code}"/>
<!-- Custom node reads error code from blackboard -->
<LogErrorCode error_code="{follow_path_error_code}"/>
</Sequence>
</BehaviorTree>
</root>⚡ The 'goal' blackboard entry is automatically populated by bt_navigator from the incoming NavigateToPose action goal. You never need to set it yourself.
Debugging with Groot2 (BT Visualizer)
Groot2 is a free GUI tool that renders live Nav2 behavior trees, shows the current tick state, and lets you edit XML trees visually.
# Download Groot2 (free, Linux AppImage)
# https://www.behaviortree.dev/groot/
# Enable BT logging in bt_navigator params:
# bt_navigator:
# ros__parameters:
# enable_groot_monitoring: true
# groot_zmq_publisher_port: 1666
# groot_zmq_server_port: 1667
# Launch Groot2, connect to localhost:1666
# → Live tree shows: green=Success, red=Failure, yellow=Running
# → Each tick updates the highlighted nodes
# Alternatively, log BT execution to a file for offline analysis:
# bt_navigator:
# ros__parameters:
# default_server_timeout: 20
# log_behaviors: true
# The BT logger writes to: /tmp/bt_trace.fbl (binary BT log)
# Open in Groot2: File > Load Log → select .fbl file⚡ Groot2 replaced Groot (v1). It supports BT.cpp v4 XML format. If Nav2's bt_navigator uses BT.cpp v3, use the original Groot instead.
Quick Reference
| Config Item | How To Set It |
|---|---|
| BT tick rate | Set bt_loop_duration: 0.1 (10 Hz) in bt_navigator params |
| Custom BT XML path | default_nav_to_pose_bt_xml in bt_navigator ros__parameters |
| Add BT plugin | plugin_lib_names list in bt_navigator params + shared library in CMakeLists |
| Blackboard write | Output port in XML: path="{path}" — node writes to this key |
| Blackboard read | Input port in XML: path="{path}" — node reads this key |
| Recovery pattern | Fallback(Sequence(Plan+Follow), Sequence(ClearCostmap+Spin+Wait)) |
| Lifecycle order | map_server → amcl → controller → planner → behavior → bt_navigator |
| Live debugging | Groot2 + enable_groot_monitoring: true in bt_navigator params |
| BT node types | ActionNodeBase (async), SyncActionNode (sync), ConditionNode (instant) |
| Plugin register | BT_REGISTER_NODES(factory) { factory.registerNodeType<MyNode>("MyNode"); } |