Behavior trees in robotics
Behavior trees have become the de facto standard for high-level robot control in both research and industry. From warehouse robots navigating dynamic environments to surgical robots performing delicate procedures, behavior trees provide a structured, debuggable, and composable way to encode complex autonomous behaviors. This guide explores practical patterns for applying behavior trees across different robotic domains.
Why behavior trees for robotics?
Traditional approaches to robot control include finite state machines (FSMs), hierarchical FSMs, and scripted behaviors. Behavior trees offer several advantages:
Modularity and reusability
Complex behaviors decompose into small, testable nodes. A
NavigateToWaypoint node can be reused across patrol,
delivery, and exploration tasks without modification.
Reactivity
Unlike sequential scripts, behavior trees continuously re-evaluate conditions. If an obstacle appears during navigation, a priority selector can immediately switch to avoidance behavior without waiting for the current action to complete.
Debuggability
The tree structure provides natural visualization of execution flow. You can see exactly which branch is active, which conditions failed, and where the robot is stuck. The WebSocket monitor makes this visible in real-time.
Scalability
Adding new capabilities means adding new branches, not rewriting existing logic. A delivery robot can gain inspection capabilities by adding a parallel subtree, without touching navigation code.
Mobile robot navigation
Navigation is the most common application of behavior trees in mobile robotics. Here's a typical pattern for autonomous navigation with safety checks:
Basic navigation tree
Selector "NavigateOrRecover"
├─ Sequence "NormalNavigation"
│ ├─ Condition "IsGoalValid"
│ ├─ Action "ComputePath"
│ ├─ Action "FollowPath"
│ └─ Condition "AtGoal"
│
└─ Sequence "RecoveryBehavior"
├─ Action "StopMotion"
├─ Selector "RecoveryStrategy"
│ ├─ Action "BackUp"
│ ├─ Action "SpinInPlace"
│ └─ Action "WaitAndRetry"
└─ Action "ClearCostmap"
How it works: The top-level selector tries normal navigation first. If any step fails (invalid goal, path planning failure, obstacle blocking path), it falls through to recovery behaviors. Recovery strategies are attempted in order until one succeeds, then the tree restarts from the top.
Safety-first navigation
For robots operating near humans or in unstructured environments, safety must take priority over task completion:
Selector "SafeNavigation"
├─ Sequence "EmergencyStop"
│ ├─ Condition "IsEmergencyButtonPressed"
│ └─ Action "ImmediateStop"
│
├─ Sequence "ObstacleAvoidance"
│ ├─ Condition "IsObstacleNearby"
│ ├─ Action "SlowDown"
│ └─ Action "NavigateAround"
│
└─ Sequence "GoalDirectedMotion"
├─ Action "PlanPath"
└─ Action "ExecutePath"
Key insight: Higher-priority branches check safety conditions first. The tree naturally enforces that emergency stops override everything, obstacle avoidance overrides normal navigation, and goal-directed motion only runs when safe.
Multi-goal patrolling
For security robots or automated guided vehicles (AGVs) that visit multiple waypoints:
Sequence "PatrolLoop"
├─ Decorator "RepeatForever"
│ └─ Sequence "VisitAllWaypoints"
│ ├─ Action "GoToWaypoint_A"
│ ├─ Action "InspectArea_A"
│ ├─ Action "GoToWaypoint_B"
│ ├─ Action "InspectArea_B"
│ ├─ Action "GoToWaypoint_C"
│ └─ Action "InspectArea_C"
│
└─ Condition "IsBatteryLow"
└─ Action "ReturnToCharger"
Note: The battery check runs in parallel (or as a decorator condition). When battery drops below threshold, the patrol is interrupted and the robot returns to charge.
Manipulation and grasping
Manipulation tasks require precise sequencing and error handling. Behavior trees excel at managing the many failure modes of grasping, lifting, and placing objects.
Pick-and-place tree
Sequence "PickAndPlace"
├─ Sequence "Perception"
│ ├─ Action "CapturePointCloud"
│ ├─ Action "DetectObjects"
│ └─ Condition "ObjectDetected"
│
├─ Sequence "Approach"
│ ├─ Action "ComputeGraspPose"
│ ├─ Action "MoveToPreGrasp"
│ └─ Condition "AtPreGraspPose"
│
├─ Sequence "Grasp"
│ ├─ Action "CloseGripper"
│ ├─ Action "LiftObject"
│ └─ Condition "ObjectInGripper"
│
├─ Sequence "Transport"
│ ├─ Action "PlanPlacementPath"
│ ├─ Action "MoveToPlacePose"
│ └─ Condition "AtPlacePose"
│
└─ Sequence "Release"
├─ Action "OpenGripper"
├─ Action "RetractArm"
└─ Condition "ObjectReleased"
Error handling strategy: Each sequence can fail at multiple points. Wrap the entire tree in a retry decorator:
Decorator "RetryWithBackoff" (max_attempts=3, delay=2s)
└─ Sequence "PickAndPlace"
... (as above)
Force-controlled insertion
Delicate assembly tasks require force feedback and adaptive control:
Sequence "PegInHole"
├─ Action "AlignPegWithHole"
├─ Decorator "WhileFailure" (max_iterations=50)
│ └─ Sequence "InsertionAttempt"
│ ├─ Action "ApplyDownwardForce"
│ ├─ Condition "ContactDetected"
│ ├─ Action "AdjustOrientation"
│ └─ Condition "PegInserted"
│
└─ Action "VerifyInsertion"
Adaptive behavior: The while-failure decorator repeatedly adjusts orientation based on force sensor readings until the peg seats properly or max attempts reached.
Multi-robot coordination
Coordinating multiple robots introduces challenges in task allocation, collision avoidance, and communication. Behavior trees can manage both individual robot behaviors and team-level coordination.
Distributed task allocation
Each robot runs an identical tree but with different parameters:
Selector "TaskExecutor"
├─ Sequence "AssignedTask"
│ ├─ Condition "HasAssignedTask"
│ ├─ Action "ReceiveTaskParameters"
│ └─ Action "ExecuteTask"
│
├─ Sequence "SearchForWork"
│ ├─ Action "BroadcastAvailability"
│ ├─ Condition "TaskOfferReceived"
│ └─ Action "AcceptTask"
│
└─ Action "IdleBehavior"
Coordination mechanism: Robots communicate via a central coordinator or peer-to-peer protocol. When a robot completes its task, it broadcasts availability and accepts new assignments.
Formation control
For robots that must maintain geometric formations (search grids, perimeter surveillance):
Sequence "MaintainFormation"
├─ Action "GetFormationPosition"
├─ Action "NavigateToPosition"
├─ Decorator "KeepRunning"
│ └─ Sequence "StayInFormation"
│ ├─ Condition "IsLeaderVisible"
│ ├─ Condition "DistanceToNeighborsOK"
│ └─ Action "AdjustVelocity"
│
└─ Selector "HandleDisruption"
├─ Condition "LostCommunication"
│ └─ Action "SearchForTeam"
└─ Condition "ObstacleBlocking"
└─ Action "TemporaryBreakaway"
Swarm behaviors
Large swarms (10+ robots) use emergent behaviors rather than explicit coordination:
Selector "SwarmMember"
├─ Sequence "AvoidCollisions"
│ ├─ Condition "RobotNearby"
│ └─ Action "SteerAway"
│
├─ Sequence "FollowGradient"
│ ├─ Action "SenseEnvironment"
│ └─ Action "MoveUpGradient"
│
└─ Action "RandomWalk"
Emergent intelligence: Simple local rules produce complex global patterns. No central controller needed — each robot reacts only to nearby neighbors and local sensors.
ROS and ROS 2 integration
The Robot Operating System (ROS) is the dominant framework for robot software. Integrating behavior trees with ROS enables access to mature perception, navigation, and control stacks.
ROS 2 + BT.CPP pattern
Use BehaviorTree.CPP as the bridge between the visual editor and ROS 2:
#include <behaviortree_ros2/bt_service_node.hpp>
#include <nav_msgs/srv/get_plan.hpp>
using namespace BT;
class ComputePathROS : public RosServiceNode<nav_msgs::srv::GetPlan>
{
public:
ComputePathROS(const std::string& name,
const NodeConfiguration& config,
rclcpp::Node::SharedPtr node)
: RosServiceNode<nav_msgs::srv::GetPlan>(name, config, node)
{}
static PortsList providedPorts()
{
return {
InputPort<std::string>("start_frame", "Start frame ID"),
InputPort<std::string>("goal_frame", "Goal frame ID"),
OutputPort<std::vector<geometry_msgs::msg::PoseStamped>>("path")
};
}
bool setRequest(Request::SharedPtr& request) override
{
std::string start_frame, goal_frame;
getInput("start_frame", start_frame);
getInput("goal_frame", goal_frame);
request->start.pose.header.frame_id = start_frame;
request->goal.pose.header.frame_id = goal_frame;
return true;
}
NodeStatus onResponseReceived(const Response::SharedPtr& response) override
{
if (response->plan.poses.empty()) {
return NodeStatus::FAILURE;
}
setOutput("path", response->plan.poses);
return NodeStatus::SUCCESS;
}
};
Custom ROS actions
For long-running tasks (navigation, manipulation), use ROS actions:
class NavigateToPoseROS : public RosActionNode<nav2_msgs::action::NavigateToPose>
{
public:
NavigateToPoseROS(const std::string& name,
const NodeConfiguration& config,
rclcpp::Node::SharedPtr node)
: RosActionNode<nav2_msgs::action::NavigateToPose>(name, config, node)
{}
static PortsList providedPorts()
{
return {
InputPort<double>("x", "Goal X coordinate"),
InputPort<double>("y", "Goal Y coordinate"),
InputPort<double>("theta", "Goal orientation")
};
}
void onFeedback(const Feedback::SharedPtr& feedback) override
{
// Publish progress via WebSocket for live monitoring
publish_websocket_event({
type: "navigation_progress",
distance_remaining: feedback->distance_remaining
});
}
};
Launching with ROS 2
# Python launch file
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='behavior_tree_runner',
executable='bt_runner',
parameters=[
{'tree_file': '/path/to/exported_tree.xml'},
{'websocket_port': 8765}
]
)
])
Sensor fusion and perception
Modern robots combine multiple sensors (cameras, LiDAR, IMU, GPS). Behavior trees can manage sensor health monitoring and data fusion.
Sensor health monitoring
Selector "SensorManager"
├─ Sequence "CameraCheck"
│ ├─ Condition "CameraConnected"
│ ├─ Condition "ImageQualityOK"
│ └─ Action "ProcessVisualData"
│
├─ Sequence "LiDARCheck"
│ ├─ Condition "LiDARConnected"
│ ├─ Condition "PointCloudValid"
│ └─ Action "UpdateOccupancyGrid"
│
└─ Sequence "DegradedMode"
├─ Action "SwitchToFallbackSensors"
└─ Action "ReduceSpeed"
Multi-modal perception
Parallel "FusePerception" (success_threshold=2)
├─ Action "RunVisualDetection"
├─ Action "RunLiDARDetection"
└─ Action "RunThermalDetection"
└─ Decorator "IfFailure"
└─ Action "UseLastKnownPosition"
Fusion strategy: The parallel node requires at least 2 out of 3 sensors to succeed. If thermal camera fails (common in bright sunlight), visual and LiDAR still provide reliable detection.
Testing and validation
Robotic systems must be thoroughly tested before deployment. Behavior trees facilitate systematic testing at multiple levels.
Unit testing individual nodes
// Google Test example
TEST(NavigationNodes, ComputePathSuccess) {
auto blackboard = BT::Blackboard::create();
blackboard->set("start", Pose2D(0, 0, 0));
blackboard->set("goal", Pose2D(10, 5, 0));
ComputePath node("compute_path", {}, blackboard);
ASSERT_EQ(node.executeTick(), BT::NodeStatus::SUCCESS);
std::vector<Pose2D> path;
ASSERT_TRUE(blackboard->get("path", path));
ASSERT_GT(path.size(), 0);
}
Integration testing with simulation
Run the full tree in Gazebo or Isaac Sim before deploying to hardware:
- Export tree from editor as BT.CPP XML
- Load tree in simulation environment
- Run automated test scenarios
- Capture NDJSON logs via WebSocket
- Replay logs in editor replay mode for analysis
Regression testing
Store reference execution logs for known-good tree versions. After modifying a tree, replay the same scenario and compare:
- Did the robot reach the same goal?
- Were there unexpected failures?
- Did execution time increase significantly?
Performance optimization
Avoiding redundant computation
Conditions should be cheap to evaluate. Cache expensive sensor reads:
Sequence "EfficientNavigation"
├─ Decorator "CacheResult" (ttl=1s)
│ └─ Condition "IsPathClear"
│
└─ Action "FollowCachedPath"
Tick rate management
Not all subtrees need to run at the same frequency:
- Safety checks: 100 Hz (every 10ms)
- Navigation updates: 10 Hz (every 100ms)
- Task planning: 1 Hz (every second)
Use decorators to throttle tick rates:
Decorator "RateLimit" (hz=10)
└─ Action "UpdateLocalPlanner"
Memory efficiency
For embedded systems with limited RAM:
- Avoid deep nesting (>10 levels)
- Reuse blackboard keys instead of creating new ones
- Clear cached data when no longer needed
Case studies
Case study 1: Warehouse fulfillment robot
Challenge: Navigate crowded warehouse aisles, pick items from shelves, and deliver to packing stations.
Solution: Hierarchical behavior tree with separate subtrees for navigation, manipulation, and human interaction. Used priority selectors to handle interruptions (emergency stops, human requests).
Result: 99.7% task completion rate, average 2.3 recovery actions per 1000 tasks.
Case study 2: Agricultural inspection drone
Challenge: Autonomously inspect crop fields, detect diseases, and avoid obstacles (trees, power lines).
Solution: Reactive tree with continuous obstacle monitoring. Integrated computer vision for disease detection and LiDAR for obstacle avoidance.
Result: Covered 50 acres/day, detected 94% of diseased plants, zero collisions in 6 months of operation.
Case study 3: Surgical assistant robot
Challenge: Assist surgeon with precise instrument positioning while maintaining safety constraints.
Solution: Safety-critical tree with multiple redundant checks. Force-feedback loops running at 1 kHz, visual servoing at 100 Hz, high-level task planning at 10 Hz.
Result: Sub-millimeter positioning accuracy, automatic safety interventions prevented 12 potential errors in clinical trials.
Common pitfalls and how to avoid them
Pitfall 1: Overly complex trees
Symptom: Trees with 50+ nodes become hard to understand and debug.
Solution: Use subtrees to modularize. Extract reusable patterns into named subtrees. Keep individual trees under 20 nodes when possible.
Pitfall 2: Ignoring timing
Symptom: Robot oscillates between behaviors or responds too slowly to changes.
Solution: Add hysteresis to conditions (e.g., battery low at 20%, but don't recover until 25%). Use rate limiters appropriately.
Pitfall 3: Tight coupling to hardware
Symptom: Can't test tree without physical robot.
Solution: Abstract hardware interfaces behind clean APIs. Use simulation for development. Mock sensor data for unit tests.
Pitfall 4: No fallback strategies
Symptom: Robot gets stuck when unexpected events occur.
Solution: Always include recovery behaviors. Design graceful degradation paths. Log failures for post-mortem analysis.
The Rule of Three: For every critical action, have at least three levels of fallback:
- Primary method (optimal performance)
- Secondary method (degraded but functional)
- Safe stop (preserve hardware and environment)
Related resources
- Interop with BehaviorTree.CPP — C++ integration for ROS robots
- Define custom node types — create robot-specific actions and conditions
- WebSocket live monitor — debug robots in the field
- BehaviorTree.CPP documentation
- ROS 2 Navigation Stack