Behavior Trees for Robotics: A Practical Introduction
Every robot that does more than one thing needs a way to decide what to do next. A warehouse robot must navigate to a shelf, pick up a package, avoid obstacles, and return to a loading dock -- all while monitoring its battery. A search-and-rescue drone must sweep an area, identify survivors, report positions, and land safely when fuel is low.
The question is not whether your robot needs a decision-making framework. The question is which one.
For decades, finite state machines were the standard answer. They work, but they scale poorly. Behavior trees offer a better alternative: they are modular, readable, and reusable. This post explains what behavior trees are, how they compare to state machines, and how to implement one from scratch in Python.
Why Robots Need Decision-Making Frameworks
A robot's low-level controllers -- PID loops, motor drivers, sensor filters -- handle individual tasks well. But they do not answer higher-level questions. Should the robot navigate to waypoint A or waypoint B? Should it stop to recharge? What happens if an obstacle blocks the planned path?
Without a structured decision-making layer, you end up with a tangle of if/else chains that grow unmanageable as the robot's responsibilities increase. Decision-making frameworks impose structure on this complexity. They give you a formal way to compose simple behaviors into complex, goal-directed action.
The two most common frameworks are finite state machines and behavior trees.
State Machines vs Behavior Trees
Finite State Machines
A finite state machine (FSM) models robot behavior as a set of states connected by transitions. The robot is always in exactly one state, and transitions are triggered by events or conditions.
For a patrol robot, the FSM might look like this:
Advantages of FSMs:
- Simple to understand for small systems
- Easy to implement
- Deterministic -- given a state and an event, the next state is always the same
Disadvantages of FSMs:
- State explosion: Adding a new behavior requires adding transitions to and from every existing state. A robot with 10 behaviors can have up to 90 transitions.
- Poor reusability: States are tightly coupled to their transitions. You cannot easily extract a behavior and drop it into a different robot.
- Hard to read at scale: A state machine with 20+ states becomes a spaghetti diagram that is difficult to reason about or modify.
- Rigid structure: Changing the priority of one behavior often requires restructuring multiple transitions.
Behavior Trees
A behavior tree (BT) models robot behavior as a tree of nodes. The tree is traversed from root to leaves on every update cycle (called a "tick"). Each node returns one of three statuses: Success, Failure, or Running.
The same patrol robot behavior, expressed as a behavior tree:
Advantages of behavior trees:
- Modular: Each subtree is a self-contained behavior that can be developed, tested, and reused independently.
- Scalable: Adding a new behavior means adding a new branch -- existing branches do not need modification.
- Readable: The tree structure makes it easy to see the robot's decision logic at a glance, even for complex systems.
- Prioritized: Higher-priority behaviors appear earlier in the tree and are evaluated first.
Disadvantages of behavior trees:
- Slightly more complex to implement than a basic FSM
- Debugging can be harder without visualization tools
- Stateful behaviors (those that remember context across ticks) require careful handling
For any robot with more than three or four distinct behaviors, behavior trees are almost always the better choice.
Node Types
A behavior tree is composed of six fundamental node types. Understanding these is the key to designing effective trees.
Action Nodes (Leaves)
Action nodes perform actual work: moving the robot, activating a gripper, sending a message. They return Success when the action completes, Failure if it cannot be completed, or Running if it is still in progress.
Examples: NavigateToWaypoint, PickUpObject, PlaySound, SendAlert.
Condition Nodes (Leaves)
Condition nodes check a fact about the world. They return Success if the condition is true and Failure if it is false. They never return Running -- a condition either holds or it does not.
Examples: IsBatteryLow, IsObstacleDetected, IsObjectVisible, IsAtGoal.
Sequence Nodes (Composites)
A Sequence node ticks its children from left to right. If a child returns Success, the Sequence moves on to the next child. If a child returns Failure, the Sequence immediately returns Failure without ticking the remaining children. If a child returns Running, the Sequence returns Running.
Think of a Sequence as a logical AND: all children must succeed for the Sequence to succeed.
Selector Nodes (Composites)
A Selector (also called a Fallback) ticks its children from left to right. If a child returns Success, the Selector immediately returns Success without ticking the remaining children. If a child returns Failure, the Selector moves on to the next child. If all children fail, the Selector returns Failure.
Think of a Selector as a logical OR: the Selector succeeds if any one child succeeds. Children are ordered by priority -- the first child is the most preferred behavior.
Decorator Nodes
A Decorator has exactly one child and modifies its behavior. Common decorators include:
- Inverter: Flips Success to Failure and Failure to Success. Useful for negating conditions (e.g., wrapping
IsBatteryLowwith an Inverter to createIsBatteryNotLow). - Repeater: Ticks the child a fixed number of times or indefinitely.
- RetryUntilSuccess: Keeps ticking the child until it succeeds, up to a maximum number of attempts.
- Timeout: Returns Failure if the child does not succeed within a time limit.
Parallel Nodes
A Parallel node ticks all its children simultaneously on each tick. The completion policy varies by implementation -- common options are:
- Success on all: Returns Success only when every child has succeeded.
- Success on one: Returns Success as soon as any one child succeeds.
Parallel nodes are useful for behaviors that must happen concurrently, like monitoring battery level while navigating.
The Tick-Based Execution Model
Behavior trees use a tick-based execution model. On each tick, the tree is traversed starting from the root. The tick propagates down through composite nodes to the leaves, and statuses propagate back up.
The tick rate is typically tied to the robot's control loop. A robot running at 10 Hz ticks its behavior tree 10 times per second. Nodes that represent long-running actions (like navigating to a waypoint) return Running on each tick until the action completes.
This design has an important consequence: the tree is re-evaluated from the root on every tick. This means that if a higher-priority condition becomes true (say, the battery drops below threshold), the tree will naturally switch to the higher-priority branch on the very next tick. There is no need to explicitly define interrupt transitions as you would in a state machine.
Tick 1: Root -> Selector -> Sequence(Charge) -> Condition(BatteryLow?) -> FAILURE
-> Sequence(Patrol) -> Navigate -> RUNNING
Tick 2: Root -> Selector -> Sequence(Charge) -> Condition(BatteryLow?) -> FAILURE
-> Sequence(Patrol) -> Navigate -> RUNNING
...
Tick N: Root -> Selector -> Sequence(Charge) -> Condition(BatteryLow?) -> SUCCESS
-> GoToCharger -> RUNNINGNotice how the tree automatically preempts the patrol behavior when the battery condition changes, with no extra wiring required.
Python Implementation
Here is a minimal but complete behavior tree implementation in Python. It defines the core node types and demonstrates how they compose.
from enum import Enum
class Status(Enum):
SUCCESS = "SUCCESS"
FAILURE = "FAILURE"
RUNNING = "RUNNING"
class Node:
"""Base class for all behavior tree nodes."""
def __init__(self, name):
self.name = name
def tick(self):
raise NotImplementedError
class Action(Node):
"""Leaf node that performs work via a callback."""
def __init__(self, name, action_fn):
super().__init__(name)
self.action_fn = action_fn
def tick(self):
return self.action_fn()
class Condition(Node):
"""Leaf node that checks a boolean condition."""
def __init__(self, name, condition_fn):
super().__init__(name)
self.condition_fn = condition_fn
def tick(self):
return Status.SUCCESS if self.condition_fn() else Status.FAILURE
class Sequence(Node):
"""Composite node: succeeds only if all children succeed (AND)."""
def __init__(self, name, children):
super().__init__(name)
self.children = children
def tick(self):
for child in self.children:
status = child.tick()
if status != Status.SUCCESS:
return status
return Status.SUCCESS
class Selector(Node):
"""Composite node: succeeds if any child succeeds (OR)."""
def __init__(self, name, children):
super().__init__(name)
self.children = children
def tick(self):
for child in self.children:
status = child.tick()
if status != Status.FAILURE:
return status
return Status.FAILURE
class Inverter(Node):
"""Decorator: flips SUCCESS to FAILURE and vice versa."""
def __init__(self, name, child):
super().__init__(name)
self.child = child
def tick(self):
status = self.child.tick()
if status == Status.SUCCESS:
return Status.FAILURE
elif status == Status.FAILURE:
return Status.SUCCESS
return Status.RUNNING
class RepeatUntilSuccess(Node):
"""Decorator: retries child up to max_attempts times."""
def __init__(self, name, child, max_attempts=5):
super().__init__(name)
self.child = child
self.max_attempts = max_attempts
self.attempts = 0
def tick(self):
if self.attempts >= self.max_attempts:
self.attempts = 0
return Status.FAILURE
status = self.child.tick()
if status == Status.SUCCESS:
self.attempts = 0
return Status.SUCCESS
self.attempts += 1
return Status.RUNNINGThis is roughly 90 lines of code, and it gives you a fully functional behavior tree engine. The Action and Condition nodes accept callback functions, so you can wire them to any robot API.
Practical Example: Patrol Robot with Battery Management
Let us build a complete behavior tree for a patrol robot. The robot has three behaviors, ordered by priority:
- Charge -- If the battery is low, navigate to the charging station and charge.
- Avoid obstacle -- If an obstacle is detected, perform an avoidance maneuver.
- Patrol -- Navigate through a list of waypoints in order.
Here is the tree structure:
And here is the full implementation using the classes defined above:
import time
class PatrolRobot:
def __init__(self):
self.battery = 100.0
self.position = [0.0, 0.0]
self.waypoints = [
[10.0, 0.0],
[10.0, 10.0],
[0.0, 10.0],
[0.0, 0.0],
]
self.current_waypoint = 0
self.obstacle_ahead = False
self.charging = False
def is_battery_low(self):
return self.battery < 20.0
def is_obstacle_ahead(self):
return self.obstacle_ahead
def has_waypoints(self):
return self.current_waypoint < len(self.waypoints)
def navigate_to_charger(self):
print(f" Navigating to charger (battery: {self.battery:.1f}%)")
self.battery -= 0.5
# Simulate arrival after a few ticks
if self.battery < 15.0:
print(" Arrived at charger.")
return Status.SUCCESS
return Status.RUNNING
def charge_battery(self):
self.battery += 5.0
print(f" Charging... (battery: {self.battery:.1f}%)")
if self.battery >= 95.0:
self.battery = 100.0
print(" Battery full.")
return Status.SUCCESS
return Status.RUNNING
def avoid_obstacle(self):
print(" Performing obstacle avoidance maneuver.")
self.obstacle_ahead = False # Obstacle cleared after maneuver
return Status.SUCCESS
def navigate_to_waypoint(self):
target = self.waypoints[self.current_waypoint]
print(f" Moving toward waypoint {self.current_waypoint}: {target}")
self.battery -= 1.0
# Simulate movement: snap to waypoint after one tick
self.position = target[:]
self.current_waypoint += 1
if self.current_waypoint >= len(self.waypoints):
self.current_waypoint = 0 # Loop the patrol
return Status.SUCCESS
def build_patrol_tree(robot):
charge_sequence = Sequence("Charge", [
Condition("Battery Low?", robot.is_battery_low),
Action("Go to Charger", robot.navigate_to_charger),
Action("Charge", robot.charge_battery),
])
avoid_sequence = Sequence("Avoid Obstacle", [
Condition("Obstacle Ahead?", robot.is_obstacle_ahead),
Action("Avoid", robot.avoid_obstacle),
])
patrol_sequence = Sequence("Patrol", [
Condition("Has Waypoints?", robot.has_waypoints),
Action("Navigate", robot.navigate_to_waypoint),
])
root = Selector("Root", [
charge_sequence,
avoid_sequence,
patrol_sequence,
])
return root
# Run the behavior tree
robot = PatrolRobot()
tree = build_patrol_tree(robot)
for tick_number in range(30):
print(f"\n--- Tick {tick_number} ---")
# Simulate an obstacle appearing at tick 5
if tick_number == 5:
robot.obstacle_ahead = True
status = tree.tick()
print(f" Tree returned: {status.value}")
time.sleep(0.1)Running this code produces output showing the robot patrolling through waypoints, reacting to an obstacle at tick 5, and switching to the charging behavior when the battery drops below 20%. The tree handles all of this with no explicit transition logic -- priorities are encoded in the tree structure itself.
Real-World Usage
Behavior trees are not just an academic concept. They are used heavily in production systems across multiple domains.
Game AI. Behavior trees became the industry standard for game AI in the 2000s. The Halo series, Unreal Engine, and Unity all use behavior tree systems to control non-player characters. Game AI was the proving ground that demonstrated behavior trees could handle complex, reactive decision-making at scale.
ROS and BehaviorTree.CPP. In the ROS (Robot Operating System) ecosystem, the BehaviorTree.CPP library is widely used for robot task planning. Navigation2, the standard navigation stack for ROS 2, uses behavior trees as its core orchestration mechanism. This means behavior trees are controlling real robots in warehouses, hospitals, and outdoor environments today.
Autonomous vehicles. Self-driving car stacks use behavior trees (or closely related architectures) to manage high-level driving decisions: when to change lanes, when to yield, when to stop for pedestrians. The modularity of behavior trees is critical here, because safety-critical behaviors must be testable in isolation.
Drones and UAVs. Mission planning for autonomous drones frequently uses behavior trees to compose flight segments, sensor activation, data collection, and emergency procedures into coherent missions.
Design Tips
A few guidelines for building effective behavior trees in practice:
Keep leaf nodes small. Each Action or Condition should do exactly one thing. "Navigate to goal" is a good action. "Navigate to goal and pick up the object and bring it back" is three actions that should be three separate nodes.
Use Selectors for priorities. The leftmost child of a Selector is the highest-priority behavior. Structure your trees so that safety-critical behaviors (emergency stop, battery management, collision avoidance) appear as early branches.
Use Sequences for procedures. A Sequence represents a series of steps that must all succeed. If step 2 fails, the Sequence fails, and the parent node can try an alternative.
Use blackboards for shared state. Rather than passing data through the tree, use a shared dictionary (a "blackboard") that nodes can read from and write to. This keeps nodes decoupled from each other.
Visualize your trees. As trees grow, visualizing them becomes essential for debugging. Tools like Groot (the GUI for BehaviorTree.CPP) let you watch the tree execute in real time, which is invaluable for understanding why a robot made a particular decision.
Try It Yourself
Head to the Behavior Trees lesson module in our curriculum. You will build a behavior tree for a mobile robot in the Robotics From Zero Playground, starting with simple Sequence and Selector compositions and progressing to a full patrol-and-recharge behavior.
The module includes interactive exercises where you can add and remove branches from a running tree and immediately see how the robot's behavior changes. There is no better way to build intuition for behavior tree design than to experiment with a live system.