Behavior trees for robot autonomy
The structure every modern robot-autonomy stack (Nav2, NASA, Sony) converged on. Why trees beat state machines, and how to actually use BT.cpp / py_trees in production.
In 2010, robot autonomy was state machines. In 2026, it's behavior trees. The shift took 15 years; the reasons are clear in hindsight. Behavior trees compose better, recover better, and read better than the equivalent state machine. They're now the structural backbone of Nav2's navigator, Sony's AIBO, NASA mission planners, and almost every game-AI system. Here's why and how.
The problem with state machines
A state machine encodes "after action A, transition to action B if condition C." For 10 states, you have 100 possible transitions. For 50 states, ~2500 transitions — none of which are obvious to a new reader.
Worse: when you add a new state, you must update transitions everywhere. The graph becomes unmaintainable. Edge cases (what if the robot is mid-action when an emergency triggers?) require explicit transitions from every state. Dual-condition logic explodes.
The behavior tree alternative
Decompose behavior into a tree. Each node is one of:
- Action (leaf): execute something — move, grasp, wait.
- Condition (leaf): check a fact — is the gripper closed? Is there an obstacle?
- Sequence (interior, named ":"): execute children left-to-right; succeed if all do; fail on first failure.
- Selector / Fallback (interior, named "?"): try children left-to-right; succeed on first success; fail if all fail.
- Parallel: run children concurrently.
- Decorator: modify a child's result (invert, retry, repeat).
The root is "tick"-ed at high frequency (~30 Hz). Each tick, the tree re-evaluates from the root, producing one of: SUCCESS, FAILURE, RUNNING.
The pick-and-place example
Selector:
Sequence "PickAndPlace":
Condition "object detected"
Action "approach"
Action "grasp"
Action "lift"
Action "transit"
Action "place"
Action "explore" # fallback if pick-and-place fails
If "object detected" fails, the Sequence fails immediately; the Selector falls back to "explore." If any sub-action fails, the Sequence fails; same fallback.
To add a new behavior — e.g., "if low battery, go to dock" — wrap the existing tree in another Selector with a "go to dock" branch. No state-machine refactor.
Why trees compose better
- Modular sub-behaviors: each subtree can be developed and tested in isolation; plug into the main tree later.
- Reactivity: the tree is re-ticked every step. New high-priority conditions (emergency stop, low battery) interrupt cleanly.
- Recovery: failures naturally propagate up; selectors provide fallbacks at every level.
- Readable: the tree is the documentation. Visual editors (Groot for BT.cpp) make the structure inspectable.
The 2026 production libraries
| Library | Language | Used by |
|---|---|---|
| BT.cpp | C++ (Python bindings) | Nav2, MoveIt 2 task constructor; production standard |
| py_trees | Python | ROS 1/2 research; clean Python API |
| FlexBE | Python | Hierarchical BTs with state-machine elements |
| Behavior3 | JS/Lua | Game AI |
For ROS 2 robotics in 2026, BT.cpp is the standard. Nav2's behavior tree XML is the canonical example.
The Nav2 behavior tree
Nav2's default navigate_to_pose.xml is a behavior tree. Simplified:
<BehaviorTree ID="MainTree">
<RecoveryNode number_of_retries="6" name="NavigateRecovery">
<PipelineSequence name="NavigateWithReplanning">
<ComputePathToPose goal="{goal}" path="{path}"/>
<FollowPath path="{path}" controller_id="FollowPath"/>
</PipelineSequence>
<ReactiveFallback name="RecoveryFallback">
<ClearLocalCostmap/>
<ClearGlobalCostmap/>
<Spin spin_dist="1.57"/>
<BackUp backup_dist="0.30" backup_speed="0.05"/>
</ReactiveFallback>
</RecoveryNode>
</BehaviorTree>
Read it: try compute path → follow path. If anything fails up to 6 times, do recovery (clear costmaps, spin, back up). All in one tree. Nav2 ships dozens of node types; you customize by combining them.
Common BT patterns
Retry with timeout
RetryUntilSuccessful (max_attempts: 5):
Timeout (5 seconds):
Action "open_door"
Attempt the action; if it doesn't finish in 5s, fail and retry. After 5 retries, give up.
Reactive priority
Selector:
Sequence "Emergency":
Condition "battery low"
Action "go to dock"
Sequence "Mission":
Action "do task"
Re-evaluated every tick. If battery drops mid-task, switches immediately.
Parallel tasks
Parallel (success: 1, fail: any):
Action "drive to goal"
Action "monitor sensors"
Drive while monitoring. If monitor reports a fault, parallel fails; outer fallback handles.
State machines vs behavior trees
Behavior trees aren't always better. State machines win when:
- Behavior is genuinely sequential with no fallbacks (a simple boot sequence).
- Discrete state transitions are the natural model (game character: idle/walk/run).
- You're integrating with hardware that exposes state-machine APIs.
For most modern robotics autonomy, BTs win because:
- Real autonomy is rife with fallbacks and recoveries.
- Behaviors are repeatable across many tasks (recovery is the same; tasks change).
- The structure is visible and editable without rewriting.
The blackboard
BTs need a way to share data between nodes (planner outputs path, controller consumes it). Solution: the blackboard — a key-value store accessible from every node.
// Planner writes
blackboard["path"] = computed_path;
// Controller reads
auto path = blackboard.get("path");
Keep the blackboard small. Large state on the blackboard is a code-smell; the BT becomes spaghetti.
Common gotchas
- Blocking actions: an action that blocks for 10 seconds prevents the tree from re-ticking, breaking reactivity. Make actions non-blocking; return RUNNING and check status next tick.
- Tree size growth: production trees can hit thousands of nodes. Decompose into subtrees; reuse via composition.
- Tick rate: too low → reactive but jittery; too high → CPU waste. 10–30 Hz is typical.
- Hidden state: pure functional BTs are clean; nodes with internal state can surprise you. Document carefully.
Tooling
- Groot: visual editor for BT.cpp trees. Live trace of which node is currently executing. Production debug tool.
- BT.cpp logger: text/JSON log of every tick; great for post-hoc debugging.
- py_trees-rendered SVG: pretty visualizations.
Exercise
Build a behavior tree in BT.cpp for a Roomba-style robot: clean, dock when low battery, resume when charged. ~20 nodes. Run in simulation. Then add an "emergency stop" branch as the highest-priority selector child. Watch how cleanly the new behavior integrates without modifying existing branches. That composability is what BTs deliver.
Next
Planning under uncertainty — POMDPs, belief-space planning, and the algorithms for robots that can't see everything.
Comments
Sign in to post a comment.