Behavior Trees for Robotics
Ask a roboticist who has shipped an autonomy stack what holds the high-level logic together, and in 2026 the answer is almost always a behavior tree. Not a finite state machine, not a rule engine, not a pile of if statements in a control loop — a behavior tree, ticked at a fixed rate, deciding moment to moment whether the robot should keep navigating, abandon the goal, clear its costmap, back up, or give up and ask for help. The pattern arrived from game AI, where it organized the enemies in Halo, and it crossed into robotics because it solved a specific, painful problem that finite state machines create the moment a robot has to be reactive: the explosion of transitions. This post is about why that explosion happens, the formalism that defuses it, how it looks in BehaviorTree.CPP and ROS2 Nav2, and — the part the tutorials skip — where behavior trees are genuinely the right tool and where reaching for them is a mistake.
The honest framing up front: a behavior tree is not a planner, not a control law, and not magic. It is a reactive execution architecture — a structured, composable way to decide which action runs right now given the current world state. It scales where FSMs collapse, it visualizes beautifully, and a badly designed tree is every bit as much of a swamp as a badly designed state machine. The advantage is real but conditional, and the conditions are the whole story.
Why finite state machines fall apart
A finite state machine is the obvious first tool: enumerate the robot’s modes — Idle, Navigating, Picking, Charging — and draw arrows for the transitions. For a handful of states it is clear and correct. The trouble starts when the robot must react to events that can fire in any state.
Suppose a delivery robot must, from any activity, respond to: battery low, obstacle detected, emergency stop, and operator cancel. Each of those events needs a transition out of every state. With N states and M cross-cutting events you are heading toward N × M transitions, and every new state you add must remember to wire up all M interrupts or it will silently ignore the emergency stop. This is the state-explosion problem, and it is not a failure of discipline — it is inherent in flat FSMs encoding reactive behavior. Hierarchical state machines (HFSMs, like ROS1’s SMACH) help by letting a superstate own the common transitions, but they reintroduce their own rigidity: the hierarchy is fixed at design time, and reusing a subtree in a new context means surgery on transitions.
FSM, 4 states, 4 cross-cutting events -> up to 16 interrupt transitions
plus the "normal" arrows; every new state must re-wire all 4 events.
estop ──┐ ┌── estop every state needs the
Idle <────────┼─┼────────> Charging same escape hatches,
^ │ │ ^ drawn explicitly, again
└── Navigating ─── Picking ┘ and again and again...
What you want is for the escape hatches to be expressed once and apply everywhere, and for sub-behaviors to be reusable building blocks you can drop into any context without rewiring. That is exactly what a behavior tree provides.
The formalism: ticks, statuses, and four node types
A behavior tree is a directed tree executed by a signal called a tick that starts at the root and propagates depth-first, left to right. Every node, when ticked, runs and returns one of three statuses:
- SUCCESS — the node accomplished its job.
- FAILURE — the node could not.
- RUNNING — the node needs more time; ask again next tick.
That third status is the whole trick. Because the root is re-ticked at a fixed frequency (say 10–20 Hz) and a long-running action returns RUNNING rather than blocking, the tree re-evaluates its decisions continuously. Conditions higher in the tree are re-checked on every tick, so the robot reacts to a changing world without a single explicit transition. Reactivity falls out of the execution model for free.
There are two families of nodes. Control-flow nodes (internal) route ticks to their children and combine the results. Leaf nodes do the actual work or sensing.
| Node | Symbol | Ticks children… | Returns SUCCESS when… | Returns FAILURE when… |
|---|---|---|---|---|
| Sequence | → | left to right, stop on failure | all children succeed | any child fails |
| Fallback (Selector) | ? | left to right, stop on success | any child succeeds | all children fail |
| Parallel | ⇉ | all children each tick | ≥ M children succeed | > N−M children fail |
| Decorator | ◇ | its single child | per its policy | per its policy |
| Action | □ (leaf) | — does work | task done | task impossible |
| Condition | ○ (leaf) | — checks state | predicate true | predicate false |
A Sequence is logical AND with short-circuit: it runs its children in order and fails the instant one fails. A Fallback (also called Selector) is logical OR: it tries children in order until one succeeds, implementing “attempt plan A; if that fails, plan B; else plan C.” Decorators wrap a single child to modify it — Inverter flips success and failure, RetryUntilSuccessful re-runs on failure up to N times, Timeout fails the child if it runs too long, RateController throttles how often the child is ticked. Actions are the verbs (drive, grasp, speak) and may return RUNNING for as long as they need; Conditions are instantaneous predicates that only ever return SUCCESS or FAILURE.
Here is a small but realistic patrol-and-recharge tree:
? (Fallback: keep robot safe & useful)
/ \
→ (Sequence: handle low batt) → (Sequence: do the patrol)
/ \ / \
○ BatteryLow? □ GoToDock ○ HaveGoal? ⇉ (Parallel)
(condition) (action, RUNNING / \
until docked) □ FollowPath ○ PathClear?
Read it as the tree is ticked: if the battery is low, go dock (and because the battery condition is re-checked every tick from the root, the instant it is no longer low the left branch fails and control flows right). Otherwise, if there is a goal, follow the path in parallel with continuously verifying the path is clear — and if PathClear? ever returns failure, the parallel fails, the patrol sequence fails, and the tree can fall through to a recovery branch. No explicit transition was ever drawn; the structure encodes the priorities, and the tick re-evaluates them constantly.
One subtlety worth internalizing: reactive control nodes re-tick from their first child every cycle (re-checking conditions), while memory variants (SequenceWithMemory) remember which child was RUNNING and resume there. Choosing reactive versus memory per node is where correctness lives. A reactive Sequence is what gives you the “drop everything when the battery dies” behavior; a memory Sequence is what stops you from restarting a multi-step manipulation from scratch on every tick.
BehaviorTree.CPP: the de facto implementation
In the ROS world the standard library is BehaviorTree.CPP (Davide Faconti), now at version 4.x. It separates the tree’s structure (declared in XML, editable by non-programmers and by the Groot2 visual editor) from the behavior of leaf nodes (registered C++ classes). The XML for the patrol tree above:
|
|
A long-running action is implemented as a StatefulActionNode, which exposes the lifecycle the tick model needs — start it once, poll it while it runs, and clean up if it is interrupted (halted) by a higher-priority branch:
|
|
Data flows between nodes through the blackboard, a shared key-value store referenced by the {current_goal} and {planned_path} port-remapping syntax in the XML. A planner node writes planned_path to the blackboard; FollowPath reads it. This decouples nodes — the same FollowPath works with any path producer — which is exactly the reusability FSMs lacked. The cost, which the honest section below returns to, is that data dependencies become implicit: nothing in the tree’s shape tells you that FollowPath will misbehave if no one populated planned_path.
The onHalted() method is the part people underestimate. When a higher-priority branch (battery low) preempts a running action, the tree calls halt down the abandoned branch, and you are responsible for cancelling the underlying goal cleanly. Forget it and you leak commands to the motors.
ROS2 integration: Nav2 runs on a behavior tree
The clearest proof that behavior trees won in robotics is that the ROS2 navigation stack, Nav2, is architected around one. The bt_navigator node loads a BehaviorTree.CPP tree whose leaves are ROS action clients, and the nav2_behavior_tree package ships dozens of plugin nodes — ComputePathToPose, FollowPath, Spin, BackUp, Wait, ClearCostmap — plus custom control nodes like RecoveryNode and PipelineSequence. The default navigate-with-recovery tree captures a pattern that would be a nightmare of transitions in an FSM:
|
|
Read it and the strategy is plain: replan the path at 1 Hz while following it; if anything in that pipeline fails, fall through to recovery — but first check whether the goal was updated (in which case do not recover, just replan); otherwise cycle through escalating recoveries (clear the costmap, spin in place, back up, wait) in round-robin, retrying the whole navigation up to six times before declaring failure. Adding a new recovery behavior is one line of XML. Doing the equivalent in a state machine means re-deriving the transition table. The path planning under ComputePathToPose is itself a separate algorithm — A*, Dijkstra, or a sampling planner — and the tree merely orchestrates it, a distinction the honest section leans on. For the wider stack this rides on, see ROS and ROS2 Explained, and for the localization that feeds {goal} and the costmaps, SLAM in Practice.
Tooling completes the picture: Groot2 renders the running tree live, highlighting which node is RUNNING, SUCCESS, or FAILURE on each tick, which turns “why did my robot spin in place” from a logging archaeology dig into a glance at a diagram. Beyond navigation, manipulation frameworks have followed — Picknik’s MoveIt Pro orchestrates manipulation skills with BehaviorTree.CPP — and the same pattern increasingly wraps the foundation-model “skills” appearing in humanoid robots and industrial robotics.
Where behavior trees are genuinely better — and where they are not
The case for behavior trees is specific and strong. They are better than FSMs at reactive task orchestration, because re-ticking from the root re-evaluates priorities every cycle with no explicit interrupt transitions. They are better at modularity and reuse, because any subtree is a self-contained unit with a clean status contract that drops into a new tree unchanged. They are better at runtime composability and readability, because the XML structure is the priority logic and tools like Groot2 make it inspectable live. And they handle graceful degradation naturally — the Fallback node is a first-class “try, else recover” construct.
The case for honesty is equally specific. A behavior tree is not a planner. It executes and reacts; it does not search a state space for a sequence of actions to achieve a goal. When the problem is “figure out what sequence of steps reaches the goal,” you need a deliberative planner (A*, a PDDL task planner, an optimization) and the BT orchestrates the result. People who try to encode planning inside a giant tree rebuild the FSM swamp they fled. A behavior tree is also not a control law: the smooth, fast inner loop that keeps a joint on a trajectory is still PID or MPC, as covered in PID Control from First Principles; the BT decides that you should follow a path, not the torque on each motor each millisecond. The blackboard hides coupling — implicit data dependencies between nodes are invisible in the tree’s shape and bite during refactors. And nothing prevents a badly designed tree: deep nesting, copy-pasted subtrees, and reactive-versus-memory confusion produce logic as tangled as any state machine. The tree’s discipline is a tool, not a guarantee.
| Architecture | Best for | Weakness | Typical home |
|---|---|---|---|
| Flat FSM | A few modes, few interrupts | State/transition explosion under reactivity | Simple embedded controllers |
| Hierarchical FSM (SMACH) | Structured sequential tasks | Rigid hierarchy, hard to reuse subtrees | Legacy ROS1 task logic |
| Behavior tree | Reactive orchestration of reusable skills | Not a planner; implicit blackboard coupling | ROS2 Nav2, MoveIt Pro, game AI |
| Task/PDDL planner | Deriving what sequence achieves a goal | Slow, needs a model; not reactive | Deliberative layer above a BT |
| Control loop (PID/MPC) | Continuous low-level tracking | No high-level logic at all | Inner motor/joint loops |
When to reach for a behavior tree
| Your situation | Use a BT? | Why |
|---|---|---|
| Many cross-cutting interrupts (estop, low battery) over several activities | Yes | Reactive re-ticking expresses escape hatches once, not N×M times |
| Reusable skills composed differently per mission | Yes | Subtrees are portable units with a clean status contract |
| Three states, one transition, no interrupts | No — use an FSM | A BT is overkill; a switch statement is clearer |
| Need to derive a multi-step plan to a goal | No — add a planner | BTs execute plans; they do not search for them |
| Tight continuous tracking of a trajectory | No — use PID/MPC | Control loops run far faster than a tick and need no branching logic |
| Orchestrating planner + controllers + recoveries reactively | Yes | This is exactly Nav2’s job and the BT sweet spot |
The deciding questions: Is the logic reactive and interrupt-heavy? (BTs shine), Are you composing reusable skills? (BTs shine), Are you trying to plan or to control? (neither is a BT’s job — pair it with the right layer instead).
Verdict
Behavior trees earned their place as the default high-level robot architecture by solving the one thing finite state machines do worst: reacting to a changing world without drowning in transitions. The tick-and-status model — SUCCESS, FAILURE, RUNNING, re-ticked from the root — makes reactivity, modularity, and graceful fallback fall out of the execution semantics instead of being hand-wired, and the ecosystem around BehaviorTree.CPP, ROS2 Nav2, and Groot2 has matured to the point that you get live visualization and a library of ready-made nodes for free. That is a genuine advance, and for orchestrating reusable skills under cross-cutting interrupts there is no better tool in 2026. But the honest engineer keeps the boundaries crisp: a behavior tree is a reactive executor, not a planner and not a controller. Put the search in a planner above it, put the fast tracking in a PID or MPC loop below it, and let the tree do what it is uniquely good at — deciding, every tick, which skill should run right now. Use it there and it is transformative; stretch it into planning or control and you will rebuild the very tangle you adopted it to escape.
Sources
- Colledanchise & Ögren, “Behavior Trees in Robotics and AI: An Introduction” (2018) — https://arxiv.org/abs/1709.00084
- BehaviorTree.CPP documentation — https://www.behaviortree.dev/
- BehaviorTree.CPP source — https://github.com/BehaviorTree/BehaviorTree.CPP
- Groot2 visual editor — https://www.behaviortree.dev/groot/
- Nav2 Behavior Trees overview — https://docs.nav2.org/behavior_trees/index.html
- nav2_behavior_tree plugin nodes — https://docs.nav2.org/plugins/index.html#behavior-tree-nodes
- Isla, “Handling Complexity in the Halo 2 AI” (GDC 2005) — https://www.gamedevs.org/uploads/handling-complexity-in-the-halo-2-ai.pdf
- Marzinotto et al., “Towards a Unified Behavior Trees Framework for Robot Control” (ICRA 2014) — https://ieeexplore.ieee.org/document/6907656
- MoveIt Pro (behavior-tree-based manipulation) — https://picknik.ai/moveit-pro/
Comments