Behavior trees in robotics

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:

  1. Export tree from editor as BT.CPP XML
  2. Load tree in simulation environment
  3. Run automated test scenarios
  4. Capture NDJSON logs via WebSocket
  5. 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:

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:

Use decorators to throttle tick rates:

Decorator "RateLimit" (hz=10)
└─ Action "UpdateLocalPlanner"

Memory efficiency

For embedded systems with limited RAM:

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.

Design principle

The Rule of Three: For every critical action, have at least three levels of fallback:

  1. Primary method (optimal performance)
  2. Secondary method (degraded but functional)
  3. Safe stop (preserve hardware and environment)

Related resources

← BT.CPP interop → Docs index