Behavior Trees for Robot Task Planning: A Reference Architecture (2026)

Behavior Trees for Robot Task Planning: A Reference Architecture (2026)

Behavior Trees for Robot Task Planning: A Reference Architecture (2026)

A warehouse picking robot loses its localization mid-task. A delivery robot’s path gets blocked by a pallet nobody logged. A manipulator’s gripper fails to close on the third attempt. None of these are edge cases — they are Tuesday. The architecture that decides what the robot does next, without a programmer hand-coding every transition, is almost always a behavior tree: a hierarchy of nodes, ticked at a fixed rate from the root, where every node returns SUCCESS, FAILURE, or RUNNING and that return value is the only signal the tree needs to stay reactive to a changing world. Behavior trees replaced hand-rolled finite state machines (FSMs) as the default task-execution layer in ROS 2 robotics — Nav2’s recovery and navigation logic, most mobile manipulation stacks, and a growing share of multi-robot orchestration all sit on top of BehaviorTree.CPP. That dominance is not an accident of tooling fashion; it comes from specific structural properties that FSMs lack.

What this covers: the node taxonomy (control, decorator, leaf) and why the tick is the whole mechanism; a reference architecture diagram with a working BehaviorTree.CPP XML snippet; a walk-through of the Nav2 navigate-with-recovery tree and how tick/return-status actually propagates through a Sequence and a Fallback with a RUNNING action; a decision matrix for control-node semantics; an honest BT-vs-FSM comparison including where BTs lose; and the failure modes — non-idempotent re-ticks, blackboard-as-hidden-global-state, condition thrashing — that show up in production and rarely in tutorials.

Context and Background

Before behavior trees, the default way to sequence robot tasks was a finite state machine: a set of named states (APPROACH, GRASP, RETREAT) and a transition table mapping (state, event) pairs to the next state. FSMs are simple to reason about for small numbers of states, and they remain the right tool for tightly-scoped, strictly sequential protocols — a communication handshake, a charging-dock docking sequence, a safety interlock. The trouble starts when robotics tasks grow: every new failure mode or recovery path multiplies the transition table, because a transition has to be added from every state that could plausibly encounter that failure. A robot with 15 states and even a handful of cross-cutting failure conditions (battery low, obstacle detected, sensor timeout) needs transitions fanned out from most of those states, and the graph becomes unreadable and untestable well before it becomes wrong.

Rodney Brooks’ subsumption architecture (1986) was an early attempt at reactive, layered robot control, and teleo-reactive programs and decision trees followed. Behavior trees, as used in game AI since the mid-2000s (Halo 2’s AI is a commonly cited early production use) and formalized for robotics by Michele Colledanchise and Petter Ögren in Behavior Trees in Robotics and AI (CRC Press, 2018), generalized this into a tree of ticked nodes with well-defined composition rules. ROS 2’s Navigation2 (Nav2) stack adopted BehaviorTree.CPP as its task-execution layer starting with the ROS 2 port of the navigation stack, and by 2026 BT.CPP v4 with Groot2 as the visual editor/monitor is the de facto standard for open-source mobile robot task orchestration. If you’re navigating a warehouse fleet with Nav2, see our Nav2 warehouse navigation walkthrough for the perception and costmap side of that stack; this article focuses on the task-planning layer sitting above it.

Navigation is the canonical example, but it is not the only one. MoveIt-based manipulation pipelines increasingly wrap grasp planning, approach, and place sequences in a behavior tree rather than a bespoke state machine, for exactly the same reuse and recovery reasons that drove Nav2’s choice — a PickObject subtree with typed ports for target_object and grasp_pose composes into a larger warehouse-picking tree the same way a Nav2 recovery subtree composes into a larger delivery tree. Multi-robot task orchestration layers this one level higher: a fleet-level task allocator assigns “robot 3, go pick from bin 12” style tasks across the fleet, but each robot still runs its own local behavior tree to execute and reactively recover from its assigned task — the allocator reasons about who does what, while each robot’s BT reasons about how that robot does it right now, which keeps fleet-level planning decoupled from per-robot execution detail.

The Reference Architecture: Anatomy of a Behavior Tree

A behavior tree is a directed tree ticked at a fixed rate (typically 10–100 Hz) from a single root; each tick propagates top-down through control nodes and decorators until it reaches a leaf, and each node’s SUCCESS, FAILURE, or RUNNING return value flows back up to determine what the parent does on the next tick — that propagation, re-evaluated every cycle rather than driven by discrete events, is what makes the tree reactive instead of merely sequential.

Node taxonomy: control, decorator, leaf

Every behavior tree is built from three categories of node, and the entire expressive power of the formalism comes from how few composition primitives it needs.

Control nodes have one or more children and decide which children to tick and how to interpret their results. The three you’ll use in nearly every tree:

  • Sequence behaves like a short-circuiting AND. It ticks its children left to right; if a child returns FAILURE, the Sequence immediately returns FAILURE and does not tick the remaining children. If a child returns RUNNING, the Sequence returns RUNNING and, critically, will re-tick from the first child on the next cycle (unless it’s a memory variant — see below). Only when all children return SUCCESS does the Sequence return SUCCESS.
  • Fallback (also called Selector) is the OR: it ticks children left to right and returns SUCCESS as soon as one child succeeds, trying the next child only if the previous one failed. It returns FAILURE only if every child fails. This is the node that encodes “try the primary approach, then try each recovery in order.”
  • Parallel ticks all children on every tick and returns SUCCESS or FAILURE based on configurable thresholds — e.g., succeed when M of N children succeed, fail when any one fails. Used for things like “keep monitoring for an obstacle while executing a motion” where both branches need to run concurrently rather than in strict order.

Decorators wrap a single child and modify its result or its ticking behavior: Inverter flips SUCCESS/FAILURE (never RUNNING); Retry re-ticks a failing child up to N times before propagating FAILURE; RateController throttles how often the child is actually ticked, useful for expensive checks; Timeout forces FAILURE if the child stays RUNNING past a deadline; Repeat re-runs a succeeding child N times or indefinitely. Decorators are where most of the “policy” logic in a tree lives — they let you compose retry/timeout/rate behavior without writing bespoke C++ for each combination.

Leaf nodes do the actual work. An Action node can return RUNNING — it represents work in progress, like a navigation goal or a gripper close command, and the tree must be designed to keep re-ticking it correctly. A Condition node is a pure, synchronous check (battery level, sensor validity) that should never block and never returns RUNNING; conditions are what a Fallback typically guards its expensive Action children with.

BehaviorTree.CPP v4 added two features worth knowing before architecting a large tree. Scripting lets simple expressions run inline in the XML — port-to-port arithmetic, _skipIf/_while pre- and post-conditions — through a small embedded expression language, so trivial glue logic like incrementing a retry counter or comparing two blackboard values doesn’t need a dedicated C++ node class. Substitution rules let a tree swap a real node for a mock or a simplified stand-in at load time, driven by a JSON config, which is what makes it practical to run the exact production tree XML against a simulator or a hardware-in-the-loop rig without hand-maintaining a second, drift-prone “test” copy of the tree. Both features exist to keep the tree itself the single source of truth for task logic rather than splitting it between XML structure and scattered C++ helper code.

Reactive Sequence vs. Sequence-with-memory

This distinction trips up nearly everyone writing their first non-trivial tree. A plain reactive Sequence re-ticks every child from the first one on each cycle, even children that already returned SUCCESS in a previous tick — this is what gives the tree its reactivity: if a precondition that succeeded a moment ago stops holding, the Sequence notices on the very next tick and fails fast, aborting the RUNNING action downstream. A Sequence-with-memory (BehaviorTree.CPP calls this SequenceStar / ReactiveSequence vs. Sequence depending on version and naming convention — v4 clarified this into Sequence [memory, skips already-succeeded children] and ReactiveSequence [no memory, re-evaluates all children]) instead remembers which children already succeeded and skips straight to the still-RUNNING or not-yet-attempted child. Memory matters specifically because of long-running Action nodes: if you have Sequence(CheckBatteryOK, Approach, Grasp) and Approach is RUNNING for several seconds, a memory-less reactive Sequence re-ticks CheckBatteryOK every cycle (correct — you want to abort if the battery craters mid-approach) but a naive memory-less Sequence would also re-tick Approach from scratch rather than resuming it, which is wrong for an Action that isn’t idempotent. BehaviorTree.CPP’s actual semantics tick the RUNNING action again (not “from scratch” — the action’s own onRunning() handles resumption), so the real design question is which preceding conditions get re-checked, not whether the running action restarts.

The blackboard: shared state without spaghetti wiring

Nodes need to pass data — a computed grasp pose, a path, a retry counter — without every node holding a direct reference to every other node. The blackboard is a shared key-value store, and BehaviorTree.CPP nodes declare typed ports (input, output, or bidirectional) that read from and write to blackboard entries by name. A ComputeGraspPose action might declare an output port target_pose; a downstream MoveTo action declares an input port also mapped to target_pose, and the XML wiring (target_pose="{target_pose}") is what connects them — no C++ coupling between the two node classes. Subtrees can remap port names at the point of inclusion, so the same reusable PickObject subtree can be dropped into different larger trees and rewired to different blackboard keys without touching its internals. This is the single biggest reason BTs compose better than FSMs: a subtree is a black box with a typed data contract (its ports), not a set of named states that must be globally unique across the whole robot’s behavior.

<!-- BehaviorTree.CPP v4 XML: pick-and-place with battery guard and retry -->
<root BTCPP_format="4">
  <BehaviorTree ID="PickAndPlace">
    <Fallback name="pick_and_place_root">
      <Sequence name="main_task">
        <Condition ID="IsBatteryOK" threshold="20.0"/>
        <RetryUntilSuccessful num_attempts="3">
          <Action ID="MoveTo" target="{approach_pose}"/>
        </RetryUntilSuccessful>
        <Action ID="ComputeGraspPose" object="{target_object}"
                target_pose="{grasp_pose}"/>
        <Action ID="MoveTo" target="{grasp_pose}"/>
        <Action ID="CloseGripper"/>
        <Action ID="MoveTo" target="{place_pose}"/>
        <Action ID="OpenGripper"/>
      </Sequence>
      <Sequence name="recovery">
        <Action ID="OpenGripper"/>
        <Action ID="RetreatToHome"/>
      </Sequence>
    </Fallback>
  </BehaviorTree>
</root>

The outer Fallback is the tree’s top-level safety net: if the main-task Sequence fails anywhere — the battery condition fails, three grasp-approach retries are exhausted, the grasp pose can’t be computed — control falls through to the recovery Sequence, which releases the gripper and retreats. Every port in braces ({grasp_pose}) is a blackboard reference; target_object presumably arrives from a parent tree or an initial blackboard write, which is exactly the decoupling described above: ComputeGraspPose doesn’t know or care who consumes grasp_pose.

Behavior tree architecture showing control nodes, decorators, leaf nodes and the shared blackboard
Figure 1: Anatomy of a behavior tree — a root Fallback chooses between a main-task Sequence and a recovery Sequence; a Retry decorator wraps a long-running Action; Condition and Action leaves read and write a shared blackboard.

The diagram makes explicit what the XML implies textually: the blackboard is not a node in the tree’s control-flow graph at all — it’s an out-of-band channel every leaf can touch, which is powerful for decoupling and, as covered in the trade-offs section below, is also the tree’s biggest source of hidden coupling if it’s overused.

Deeper Walk-through: Nav2’s Recovery Tree and Tick Propagation

Nav2 ships a default navigate_w_replanning_and_recovery behavior tree (and several variants — navigate_through_poses, tree-with-recovery-only, etc.) that is a production example of exactly the pattern above, scaled to a real navigation stack. Its top level is a Fallback (recovery-fallback) between a navigation Sequence and a recovery Sequence; the navigation Sequence itself wraps ComputePathToPose and FollowPath inside a RateController-throttled reactive structure so that path validity and the global/local costmaps are re-checked without re-invoking the (expensive) planner every single tick.

<!-- Simplified excerpt in the style of Nav2's default navigate_w_replanning_and_recovery.xml -->
<root BTCPP_format="4">
  <BehaviorTree ID="MainTree">
    <RecoveryNode number_of_retries="6" name="NavigateRecovery">
      <PipelineSequence name="NavigateWithReplanning">
        <RateController hz="1.0">
          <ComputePathToPose goal="{goal}" path="{path}"
                              planner_id="GridBased"/>
        </RateController>
        <FollowPath path="{path}" controller_id="FollowPath"/>
      </PipelineSequence>
      <ReactiveFallback name="RecoveryFallback">
        <ClearEntireCostmap name="ClearLocalCostmap"
                             service_name="local_costmap/clear_entirely"/>
        <ClearEntireCostmap name="ClearGlobalCostmap"
                             service_name="global_costmap/clear_entirely"/>
        <Spin spin_dist="1.57"/>
        <BackUp backup_dist="0.3" backup_speed="0.05"/>
        <Wait wait_duration="5"/>
      </ReactiveFallback>
    </RecoveryNode>
  </BehaviorTree>
</root>

RecoveryNode is a Nav2-specific control node (not stock BT.CPP): it’s effectively a two-child Fallback with a bounded retry counter — tick the first child (the navigation pipeline); on failure, tick the second child (the recovery Fallback) once, then retry the first child, up to number_of_retries. PipelineSequence is another Nav2 extension — a Sequence variant tuned for pipelines where later stages should keep running reactively without restarting earlier ones from scratch each tick. The recovery Fallback tries clearing costmaps first (cheap, often sufficient when the failure is stale obstacle data), then a spin (clears transient blind spots), then a backup, then a last-resort wait — each recovery action is itself a Nav2 behavior server call, not inline code.

Nav2 navigate-with-recovery behavior tree showing the navigation pipeline and the recovery fallback chain
Figure 2: Nav2’s navigate-with-recovery pattern — a RecoveryNode bounds retries between a replanning navigation pipeline and an ordered recovery fallback (clear costmaps, spin, back up, wait).

How a tick actually propagates: Sequence, Fallback, and a RUNNING action

The mechanics matter because “the tree re-evaluates every cycle” is easy to say and easy to get wrong when a child is mid-execution. Consider a minimal Fallback(Sequence(Condition, Action)) where Action is a multi-second navigation call.

Tick 1: Root ticks Fallback → Fallback ticks its first (and only, in this simplified case) child, Sequence → Sequence ticks Condition → Condition returns SUCCESS synchronously → Sequence ticks Action → Action starts the navigation call and immediately returns RUNNING (it does not block the tick) → Sequence propagates RUNNING up → Fallback propagates RUNNING up → Root sees RUNNING and does nothing else this cycle.

Tick 2 (mid-navigation): Root ticks Fallback → Fallback re-ticks Sequence (a Fallback always re-ticks a RUNNING or not-yet-succeeded child) → Sequence, if reactive, re-ticks Condition first — if the condition still holds, Sequence proceeds to re-tick Action, which resumes monitoring its in-flight navigation call rather than restarting it → still RUNNING → propagates up unchanged.

Tick N (navigation completes): Action’s onRunning() detects the goal was reached, returns SUCCESS → Sequence has now had all children succeed → Sequence returns SUCCESS → Fallback, having gotten SUCCESS from its first child, does not tick its remaining children (the recovery branch) and returns SUCCESS itself.

If instead the Condition had flipped to FAILURE at tick K (say, battery dropped below threshold) while Action was still RUNNING mid-navigation, the reactive Sequence would return FAILURE at tick K without ticking Action again — and BehaviorTree.CPP’s halting semantics require that Sequence explicitly halt the previously-RUNNING Action (calling its onHalted()), because an Action that’s mid-navigation doesn’t stop just because the tree stopped ticking it — someone has to tell the underlying system (a Nav2 action-server cancel, in this example) to actually preempt. This halt-on-preemption step is the part most home-grown BT implementations get wrong: they let a node go RUNNING, then simply stop ticking it, leaving a robot arm or a mobile base still executing a command the tree has “abandoned.”

Sequence diagram of tick propagation through a Fallback and Sequence with a RUNNING action across two tick cycles
Figure 3: Tick and return-status propagation — a Condition succeeds, an Action goes RUNNING and is re-ticked on the next cycle, then resolves to SUCCESS, which the Fallback short-circuits on.

Control-node decision matrix

Control node Ticks children Succeeds when Fails when Typical use
Sequence (reactive) All, left to right, every tick, until FAILURE/RUNNING All children SUCCESS First child FAILURE AND-chain of preconditions + action
Sequence (memory) Resumes at first non-SUCCESS child All children SUCCESS (once) Any child FAILURE Multi-step task where re-checking early steps is wasteful/unsafe
Fallback Left to right until one SUCCEEDS or RUNNING First child SUCCESS All children FAILURE Try primary approach, then ordered recovery options
Parallel All children, every tick ≥ M of N children SUCCESS Failure threshold met Concurrent monitoring + execution
RecoveryNode (Nav2) Primary child; recovery child on failure Primary child eventually SUCCEEDS Retry budget exhausted Bounded retry-with-recovery wrapper

Trade-offs, Gotchas, and What Goes Wrong

BTs vs. FSMs, honestly. The case for behavior trees is modularity (a subtree is a reusable unit with a typed port contract), no combinatorial transition explosion (a new recovery path is one more Fallback child, not N new transitions), reactivity by construction (the re-tick-from-root pattern means preconditions are continuously re-checked without hand-written polling), and human readability (the tree’s shape mirrors its logic, which is why Groot2’s visual editor works so well as both an authoring and a live-monitoring tool). The honest counterpoint: for a strictly sequential protocol with a small, fixed number of steps and no real branching — a docking handshake, a boot sequence — an FSM is simpler to reason about and to prove correct, because there’s no tick-rate-dependent timing subtlety and no RUNNING-state bookkeeping. BTs also carry real ticking overhead: a large tree ticked at 100 Hz re-evaluates a lot of Condition nodes every 10 ms even when nothing changed, and if any of those conditions are non-trivial (a costmap lookup, a TF lookup) that overhead is not free — RateController decorators exist specifically to cap this. And RUNNING actions are, in effect, hidden state: the tree’s static XML doesn’t tell you which action is currently mid-flight, so debugging “why is the robot doing X” often means attaching Groot2 live rather than reading the tree structure alone.

Non-idempotent actions on re-tick. An Action’s onStart()/onRunning() split exists precisely so a node isn’t re-initialized every tick, but plenty of hand-written nodes get this wrong by doing setup work (opening a socket, incrementing a counter, publishing a “start” event) inside a code path that runs on every tick rather than only on the transition into RUNNING. The symptom is subtle: a node that behaves correctly in isolation double-fires side effects the moment it’s nested under a reactive Sequence that re-ticks it alongside a sibling Condition.

Blackboard as hidden global state. The blackboard’s decoupling benefit inverts into a liability once a team starts writing to widely-shared keys from many nodes without a naming discipline — at that point the blackboard is a global variable pool, and reasoning about “who last wrote target_pose” requires grepping the whole tree rather than reading local ports. The fix is scoping: prefer subtree-local port remapping over top-level globals, and treat any blackboard key touched by more than two or three node types as a design smell.

Condition thrashing. A Condition that flips true/false near a threshold (e.g., a battery percentage hovering at exactly the cutoff, or a costmap-derived “path clear” check near a moving obstacle) can cause a Fallback or reactive Sequence to oscillate between branches every tick, which looks like the robot “twitching” between two behaviors. Hysteresis — checking against two thresholds instead of one, or debouncing the condition’s own internal state — belongs inside the Condition node, not the tree structure.

Async I/O, priority inversion, and halting semantics. Wrapping a blocking network or service call directly inside a synchronous Action is a common mistake that stalls the entire tick — the fix is to make the call asynchronous and return RUNNING immediately, polling completion on subsequent ticks. Priority inversion shows up when a low-priority recovery branch holds a resource (a costmap lock, an actuator) that a higher-priority Fallback branch needs, and the tree has no built-in concept of priority beyond child order — this has to be designed around explicitly, often with mutual-exclusion Conditions. And as covered above, halting a RUNNING node on preemption must actually cancel the underlying command, not just stop ticking it; skipping this is the single most common cause of “the robot kept moving after the behavior tree said it stopped.”

Picking a tick rate is a real design decision, not a default. Too slow (1–2 Hz) and the tree’s reactivity advantage largely evaporates — a Condition that should abort an action within milliseconds instead lags behind by up to a full tick period, which matters for safety-critical aborts like an emergency-stop check. Too fast (500 Hz and up) and the CPU spent walking the tree and evaluating Conditions starts competing with the robot’s actual control loops, especially once the tree grows large or its Conditions perform TF or costmap lookups rather than simple flag reads. Nav2’s default of roughly 10 Hz for the top-level tree, with a RateController throttling the more expensive replanning check down to 1 Hz, is a reasonable starting point for a mobile robot: fast enough to abort promptly, throttled precisely where the per-tick cost is highest.

Testing and observability. Because so much state lives in RUNNING actions and the blackboard rather than in the tree’s static shape, unit-testing a tree in isolation from real hardware requires mocking every leaf node’s return-status sequence across multiple ticks, not just its final result — a tree that “looks right” in a single-tick review can still have a subtly wrong multi-tick interaction. Groot2’s live monitoring (which shows the currently-RUNNING path highlighted in the tree, updated from a logging/telemetry connection to the running BT.CPP process) is the practical answer, but it’s a runtime-observability tool, not a substitute for scripted multi-tick tests in CI.

Where BTs stop and planners start. A behavior tree is a task-execution and reactivity layer, not a planner: ComputePathToPose inside the Nav2 tree is a leaf that calls a geometric planner (a search over the costmap), and the tree’s job is to sequence that call, monitor it, retry it, and fall back to recovery — the tree does not itself solve the shortest-path problem. This division of labor generalizes: PDDL-style symbolic task planners solve “what sequence of high-level actions achieves this goal” over long horizons with complex preconditions, and an increasingly common 2026 pattern feeds an LLM-driven task planner’s output (a proposed action sequence, often with uncertainty) into a behavior tree that executes it reactively and handles the low-level recovery the planner never reasoned about. In other words: the planner decides what, the tree decides how to execute that reliably right now, and it’s a mistake to ask a behavior tree to do combinatorial symbolic search or to ask a task planner to handle millisecond-scale reactivity — each is solving a different problem. For manipulation stacks specifically, this same split shows up between motion planners and the BT that invokes them; see our MoveIt 2 vs. Tesseract motion planning comparison for how that boundary is drawn on the arm side.

Decision flow for choosing between a behavior tree, a finite state machine, or a symbolic task planner
Figure 4: When to reach for a behavior tree versus a finite state machine versus a symbolic/LLM task planner, based on reactivity needs, reuse requirements, and planning horizon.

Practical Recommendations

Start every new tree from the failure side, not the happy path: sketch the recovery Fallback chain first (what can go wrong, in what order should you try to fix it), then fill in the main-task Sequence, because retrofitting recovery onto an already-built happy-path tree is where most of the awkward nesting comes from. Keep individual Action nodes small and single-purpose — one node per side effect, not one node that does three things internally — so that halting semantics and retry decorators can be applied precisely where they’re needed rather than to a bundle of unrelated work. Name blackboard ports deliberately and treat cross-subtree keys as a reviewed interface, the same way you’d review a function signature; a target_pose written by four different subtrees is a liability waiting to surface as a race. Use RateController decorators on any Condition or Action that does non-trivial computation (a costmap query, a TF lookup) so a fast tick rate doesn’t turn into a hidden CPU cost. Instrument before you need to debug: wire up Groot2 (or equivalent structured BT logging) from the start, because reconstructing “which branch was RUNNING when the robot did the wrong thing” after the fact from logs alone is far harder than watching it live. Finally, write multi-tick tests, not single-tick assertions — mock a leaf’s return sequence across ticks (RUNNING, RUNNING, SUCCESS; or RUNNING, FAILURE) and assert on the parent’s behavior across that whole sequence, since single-tick tests systematically miss the class of bugs described in the gotchas above. If you’re building a humanoid or mobile-manipulation platform, see our humanoid robot control stack architecture piece for how a BT task layer typically sits above whole-body and locomotion controllers.

Pre-deployment checklist:
– Every Action node correctly distinguishes onStart() setup from onRunning() polling, with no side effects duplicated on re-tick.
– Every RUNNING node has a tested onHalted() that actually cancels the underlying command (service call, action-server goal, hardware command).
– No blackboard key is written by more than two or three node types without a documented reason.
– Every Fallback’s recovery branch has been exercised in a fault-injection test, not just reviewed by eye.
– Tick rate and any RateController-throttled nodes have been checked against real controller-loop CPU budget, not just “it worked on the dev laptop.”
– Groot2 (or equivalent) is wired to the deployed tree before first field test, not added after the first unexplained incident.

Frequently Asked Questions

What is a behavior tree in robotics?

A behavior tree is a hierarchical structure of control nodes (Sequence, Fallback, Parallel), decorators (Retry, Timeout, Inverter), and leaf nodes (Action, Condition) that is ticked at a fixed rate from its root. Each tick returns SUCCESS, FAILURE, or RUNNING, and that return value determines what happens on the next tick — giving the whole structure continuous reactivity to sensor and world state without hand-written polling loops.

Why do robots use behavior trees instead of state machines?

Behavior trees avoid the combinatorial explosion of transitions that FSMs suffer as failure modes multiply, because a new recovery path is added as one more Fallback child rather than a transition fanned out from every relevant state. They’re also more modular — a subtree with typed blackboard ports can be reused across robots — and reactive by construction, since the tree re-evaluates preconditions every tick rather than only on discrete events.

What is a blackboard in a behavior tree?

The blackboard is a shared key-value store that decouples nodes: instead of wiring node A directly to node B, node A writes a value to a named blackboard key via an output port, and node B reads it via an input port. This lets subtrees be reused and rewired through port remapping without changing their internal C++ or XML, but overusing shared keys across many unrelated nodes turns the blackboard into hidden global state.

Does Nav2 use behavior trees?

Yes. ROS 2’s Navigation2 stack uses BehaviorTree.CPP to implement its task-execution layer, including the default navigate_w_replanning_and_recovery tree, which wraps path computation and path following in a bounded-retry RecoveryNode alongside an ordered recovery Fallback (clear costmaps, spin, back up, wait). Groot2 is the companion tool for visually editing and live-monitoring these trees.

Can a behavior tree replace a task planner?

No — they solve different problems. A symbolic or LLM-driven task planner reasons over long horizons to decide what sequence of high-level actions achieves a goal, often with complex preconditions; a behavior tree executes and monitors that sequence reactively, handling low-level recovery and preemption the planner never reasoned about. A common 2026 pattern feeds a planner’s proposed action sequence into a behavior tree for execution rather than asking either layer to do the other’s job.

What is the difference between a Sequence and a Fallback node?

A Sequence behaves like a short-circuiting AND: it ticks children left to right and fails immediately if any child fails, succeeding only when all children succeed. A Fallback (Selector) behaves like an OR: it tries children left to right and succeeds as soon as one succeeds, only trying the next child after the previous one fails, and only failing itself if every child fails.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *