Skip to main content
🌲 Nav2 BTROS 2 · June 2026

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.

1

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.

text
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).

2

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.

xml
<!--
  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.

4

bt_navigator Configuration Parameters

Configure bt_navigator in your nav2_params.yaml to select the BT XML file, set tick frequency, and load BT node plugins.

yaml
# nav2_params.yaml

bt_navigator:
  ros__parameters:
    use_sim_time: False

    # Path to the BT XML file — use FindPackageShare in launch files
    default_nav_to_pose_bt_xml: "$(find-pkg-share my_pkg)/behavior_trees/navigate_simple.xml"
    default_nav_through_poses_bt_xml: "$(find-pkg-share nav2_bt_navigator)/behavior_trees/navigate_through_poses_w_replanning_and_recovery.xml"

    # BT tick frequency (Hz)
    # Lower = less CPU, but slower recovery detection
    # Higher = more responsive, more CPU
    bt_loop_duration: 0.1      # 10 Hz tick rate (seconds per tick)
    default_server_timeout: 20 # seconds before action server times out

    # Navigators to load (which action servers to expose)
    navigators:
      - nav_to_pose
      - nav_through_poses

    # BT node plugins — all nav2 BT nodes must be listed here
    plugin_lib_names:
      - nav2_compute_path_to_pose_action_bt_node
      - nav2_follow_path_action_bt_node
      - nav2_spin_action_bt_node
      - nav2_wait_action_bt_node
      - nav2_back_up_action_bt_node
      - nav2_clear_costmap_service_bt_node
      - nav2_goal_reached_condition_bt_node
      - nav2_goal_updated_condition_bt_node
      - nav2_is_stuck_condition_bt_node
      - nav2_rate_controller_bt_node
      - nav2_recovery_node_bt_node
      - nav2_pipeline_sequence_bt_node
      - nav2_round_robin_node_bt_node
5

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.

cpp
#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.

6

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.

xml
<!-- 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.

8

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.

bash
# 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 ItemHow To Set It
BT tick rateSet bt_loop_duration: 0.1 (10 Hz) in bt_navigator params
Custom BT XML pathdefault_nav_to_pose_bt_xml in bt_navigator ros__parameters
Add BT pluginplugin_lib_names list in bt_navigator params + shared library in CMakeLists
Blackboard writeOutput port in XML: path="{path}" — node writes to this key
Blackboard readInput port in XML: path="{path}" — node reads this key
Recovery patternFallback(Sequence(Plan+Follow), Sequence(ClearCostmap+Spin+Wait))
Lifecycle ordermap_server → amcl → controller → planner → behavior → bt_navigator
Live debuggingGroot2 + enable_groot_monitoring: true in bt_navigator params
BT node typesActionNodeBase (async), SyncActionNode (sync), ConditionNode (instant)
Plugin registerBT_REGISTER_NODES(factory) { factory.registerNodeType<MyNode>("MyNode"); }