Overview
Behavior Trees (BT) provide flexible decision-making for robot navigation. Nav2 uses BTs to coordinate planning, control, and recovery behaviors.
Behavior Trees are more flexible than state machines, allowing easy modification of navigation logic without code changes.
What is a Behavior Tree?
Hierarchical structure of nodes that execute in sequence:
Root
├── Sequence (execute children in order)
│ ├── Action 1
│ ├── Action 2
│ └── Fallback (try alternatives if fail)
│ ├── Action 3
│ └── Action 4 (backup)
└── Parallel (execute simultaneously)
├── Action 5
└── Action 6
Node Types
Action
Execute behavior (e.g., compute path, follow path)
- Returns: Success, Failure, Running
Condition
Check state (e.g., goal reached, path blocked)
- Returns: Success or Failure
Control
Organize children (Sequence, Fallback, Parallel)
Decorator
Modify child behavior (Retry, Invert, Delay)
Default Nav2 Behavior Tree
File: navigate_to_pose_w_replanning_and_recovery.xml
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<RecoveryNode number_of_retries="6" name="NavigateRecovery">
<!-- Main navigation sequence -->
<PipelineSequence name="NavigateWithReplanning">
<!-- Compute path to goal -->
<RateController hz="1.0">
<RecoveryNode number_of_retries="1" name="ComputePathToPose">
<ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
<ClearEntireCostmap name="ClearGlobalCostmap-Context" service_name="global_costmap/clear_entirely_global_costmap"/>
</RecoveryNode>
</RateController>
<!-- Follow the computed path -->
<RecoveryNode number_of_retries="1" name="FollowPath">
<FollowPath path="{path}" controller_id="FollowPath"/>
<ClearEntireCostmap name="ClearLocalCostmap-Context" service_name="local_costmap/clear_entirely_local_costmap"/>
</RecoveryNode>
</PipelineSequence>
<!-- Recovery behaviors if navigation fails -->
<ReactiveFallback name="RecoveryFallback">
<GoalUpdated/>
<SequenceStar name="RecoveryActions">
<ClearEntireCostmap name="ClearLocalCostmap-Subtree" service_name="local_costmap/clear_entirely_local_costmap"/>
<ClearEntireCostmap name="ClearGlobalCostmap-Subtree" service_name="global_costmap/clear_entirely_global_costmap"/>
<Spin spin_dist="1.57"/>
<Wait wait_duration="5"/>
<BackUp backup_dist="0.30" backup_speed="0.05"/>
</SequenceStar>
</ReactiveFallback>
</RecoveryNode>
</BehaviorTree>
</root>
How It Works
-
ComputePathToPose: Plan global path
- If fails → Clear global costmap, retry
-
FollowPath: Execute path with local planner
- If fails → Clear local costmap, retry
-
If still failing (after retries):
- Clear both costmaps
- Spin in place (clear area)
- Wait 5 seconds
- Backup
- Retry from step 1
-
Retry up to 6 times before giving up
Custom Behavior Trees
Simple Navigate (No Recovery)
File: navigate_simple.xml
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<PipelineSequence name="NavigateSimple">
<!-- Plan path (no retry) -->
<ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
<!-- Follow path (no retry) -->
<FollowPath path="{path}" controller_id="FollowPath"/>
</PipelineSequence>
</BehaviorTree>
</root>
Use case: Testing, simple environments, fast failure needed
Navigate with Replanning
Continuous replanning for dynamic environments:
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<PipelineSequence name="NavigateWithReplanning">
<!-- Replan every 1 second -->
<RateController hz="1.0">
<ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
</RateController>
<!-- Follow latest path -->
<FollowPath path="{path}" controller_id="FollowPath"/>
</PipelineSequence>
</BehaviorTree>
</root>
Benefit: Adapts to moving obstacles
Navigate Through Poses (Waypoints)
Visit multiple waypoints in sequence:
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<RecoveryNode number_of_retries="1" name="NavigateThroughPoses">
<PipelineSequence name="NavigateWithReplanning">
<RateController hz="1.0">
<RecoveryNode number_of_retries="1" name="ComputePathThroughPoses">
<ComputePathThroughPoses goals="{goals}" path="{path}" planner_id="GridBased"/>
<ClearEntireCostmap name="ClearGlobalCostmap-Context" service_name="global_costmap/clear_entirely_global_costmap"/>
</RecoveryNode>
</RateController>
<RecoveryNode number_of_retries="1" name="FollowPath">
<FollowPath path="{path}" controller_id="FollowPath"/>
<ClearEntireCostmap name="ClearLocalCostmap-Context" service_name="local_costmap/clear_entirely_local_costmap"/>
</RecoveryNode>
</PipelineSequence>
<!-- Recovery behaviors -->
<ReactiveFallback name="RecoveryFallback">
<GoalUpdated/>
<RoundRobin name="RecoveryActions">
<Sequence name="ClearingActions">
<ClearEntireCostmap name="ClearLocalCostmap-Subtree" service_name="local_costmap/clear_entirely_local_costmap"/>
<ClearEntireCostmap name="ClearGlobalCostmap-Subtree" service_name="global_costmap/clear_entirely_global_costmap"/>
</Sequence>
<Spin spin_dist="1.57"/>
<Wait wait_duration="5"/>
<BackUp backup_dist="0.30" backup_speed="0.05"/>
</RoundRobin>
</ReactiveFallback>
</RecoveryNode>
</BehaviorTree>
</root>
BT Nodes Reference
Navigation Actions
ComputePathToPose
<ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
- Calls planner server to compute path
- Input: goal (PoseStamped)
- Output: path (nav_msgs/Path)
FollowPath
<FollowPath path="{path}" controller_id="FollowPath"/>
- Calls controller server to follow path
- Input: path
- Output: cmd_vel commands
ComputePathThroughPoses
<ComputePathThroughPoses goals="{goals}" path="{path}" planner_id="GridBased"/>
- Plans path through multiple waypoints
- Input: goals (array of PoseStamped)
Recovery Actions
Spin
- Rotates robot in place
spin_dist: Radians to rotate (1.57 = 90°, 3.14 = 180°)
BackUp
<BackUp backup_dist="0.30" backup_speed="0.05"/>
- Moves robot backward
backup_dist: Distance (meters)
backup_speed: Speed (m/s)
Wait
<Wait wait_duration="5"/>
- Waits for specified time (seconds)
ClearEntireCostmap
<ClearEntireCostmap service_name="local_costmap/clear_entirely_local_costmap"/>
- Clears all obstacles from costmap
ClearCostmapExceptRegion
<ClearCostmapExceptRegion service_name="local_costmap/clear_except_region" reset_distance="1.0"/>
- Clears costmap except area around robot
Control Flow Nodes
Sequence
<Sequence>
<Action1/>
<Action2/>
<Action3/>
</Sequence>
- Executes children in order
- Fails if any child fails
Fallback
<Fallback>
<Action1/> <!-- Try this first -->
<Action2/> <!-- If fails, try this -->
<Action3/> <!-- Last resort -->
</Fallback>
- Tries children until one succeeds
Parallel
<Parallel>
<Action1/> <!-- Execute simultaneously -->
<Action2/>
</Parallel>
- Executes children concurrently
RecoveryNode
<RecoveryNode number_of_retries="3">
<MainAction/>
<RecoveryAction/>
</RecoveryNode>
- Tries main action
- If fails, runs recovery, then retries main
Decorators
RateController
<RateController hz="1.0">
<Action/>
</RateController>
RetryUntilSuccessful
<RetryUntilSuccessful num_attempts="5">
<Action/>
</RetryUntilSuccessful>
- Retries until success or max attempts
Inverter
<Inverter>
<Condition/>
</Inverter>
- Inverts result (Success ↔ Failure)
Loading Custom BT
Configuration
In nav2_params.yaml:
bt_navigator:
ros__parameters:
use_sim_time: False
global_frame: map
robot_base_frame: base_link
odom_topic: /odometry/filtered
bt_loop_duration: 10
default_server_timeout: 20
# Specify custom BT file
default_nav_to_pose_bt_xml: /path/to/navigate_simple.xml
default_nav_through_poses_bt_xml: /path/to/navigate_waypoints.xml
# Plugin libraries (for custom BT nodes)
plugin_lib_names:
- nav2_compute_path_to_pose_action_bt_node
- nav2_compute_path_through_poses_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
# Add custom plugins here
Package Structure
mecanum_description/
├── behavior_trees/
│ ├── navigate_simple.xml
│ ├── navigate_waypoints.xml
│ └── custom_recovery.xml
├── config/
│ └── nav2_params.yaml
└── ...
Install BT Files
In CMakeLists.txt:
install(DIRECTORY behavior_trees
DESTINATION share/${PROJECT_NAME}/)
Debugging Behavior Trees
Enable BT Logging
bt_navigator:
ros__parameters:
# Enable detailed logging
debug_logging: true
Monitor BT State
# Watch BT execution
ros2 topic echo /behavior_tree_log
Visualize with Groot
Install Groot:
sudo apt install ros-jazzy-groot
Launch:
# Monitor live BT execution
groot
# Or record to file
ros2 bag record /behavior_tree_log -o bt_recording
Features:
- Real-time BT visualization
- Node status (running, success, failure)
- Execution flow diagram
Advanced: Custom BT Nodes
Create Custom Action
C++ plugin example:
#include "behaviortree_cpp_v3/action_node.h"
#include "nav2_behavior_tree/bt_action_node.hpp"
namespace my_bt_nodes
{
class CustomAction : public BT::ActionNodeBase
{
public:
CustomAction(const std::string & xml_tag_name,
const BT::NodeConfiguration & conf)
: BT::ActionNodeBase(xml_tag_name, conf)
{
}
static BT::PortsList providedPorts()
{
return {
BT::InputPort<std::string>("param1", "Description of param1"),
BT::OutputPort<bool>("result", "Output result")
};
}
BT::NodeStatus tick() override
{
// Get input
std::string param1;
getInput("param1", param1);
// Execute custom logic
bool success = doSomething(param1);
// Set output
setOutput("result", success);
return success ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE;
}
private:
bool doSomething(const std::string & param)
{
// Custom behavior implementation
return true;
}
};
} // namespace my_bt_nodes
Register Plugin
In plugin XML:
<library path="my_bt_nodes">
<class name="my_bt_nodes/CustomAction" type="my_bt_nodes::CustomAction" base_class_type="BT::ActionNodeBase">
<description>Custom action description</description>
</class>
</library>
Use in BT
<CustomAction param1="value" result="{output_var}"/>
Example Use Cases
Patrol Route
<root main_tree_to_execute="PatrolRoute">
<BehaviorTree ID="PatrolRoute">
<Repeat num_cycles="-1"> <!-- Infinite loop -->
<Sequence>
<NavigateToPose goal="{waypoint1}"/>
<Wait wait_duration="5"/> <!-- Pause at waypoint -->
<NavigateToPose goal="{waypoint2}"/>
<Wait wait_duration="5"/>
<NavigateToPose goal="{waypoint3}"/>
<Wait wait_duration="5"/>
</Sequence>
</Repeat>
</BehaviorTree>
</root>
Exploration
<root main_tree_to_execute="Explore">
<BehaviorTree ID="Explore">
<Repeat num_cycles="-1">
<Sequence>
<!-- Select random frontier -->
<SelectFrontier goal="{frontier_goal}"/>
<!-- Navigate to frontier -->
<NavigateToPose goal="{frontier_goal}"/>
<!-- Pause to scan area -->
<Spin spin_dist="6.28"/> <!-- 360° -->
</Sequence>
</Repeat>
</BehaviorTree>
</root>
Delivery Mission
<root main_tree_to_execute="Delivery">
<BehaviorTree ID="Delivery">
<Sequence>
<!-- Go to pickup location -->
<NavigateToPose goal="{pickup_location}"/>
<Wait wait_duration="10"/> <!-- Wait for loading -->
<!-- Go to delivery location -->
<NavigateToPose goal="{delivery_location}"/>
<Wait wait_duration="10"/> <!-- Wait for unloading -->
<!-- Return to home -->
<NavigateToPose goal="{home_location}"/>
</Sequence>
</BehaviorTree>
</root>
Next Steps
References
[1] Behavior Trees: https://www.behaviortree.dev/
[2] Nav2 BT Navigator: https://docs.nav2.org/configuration/packages/configuring-bt-navigator.html
[3] BT.CPP Library: https://github.com/BehaviorTree/BehaviorTree.CPP