This page explains the fundamental ideas behind behavior trees and how BTEng implements them. Read this before the beginner guide if you are new to behavior trees, or use it as a reference for specific topics.
A behavior tree is a directed tree of nodes. On each tick, the tree is traversed from the root downward. Each node reports a status to its parent, and control flows up the tree based on those statuses.
Every node returns exactly one of three statuses:
| Status | Meaning |
|---|---|
SUCCESS |
The node completed successfully |
FAILURE |
The node failed or its condition was false |
RUNNING |
The node is still working; tick it again next frame |
RUNNING is what makes behavior trees different from decision trees. A node can span
many ticks — for example, a navigation action that takes several seconds to complete
returns RUNNING on each tick until it arrives.
Leaf nodes do the actual work. There are two kinds:
Action — performs a task. May return RUNNING across many ticks.
from bteng import ActionNode, NodeStatus
class Navigate(ActionNode):
def tick(self):
return NodeStatus.RUNNING
Condition — checks a predicate. Returns SUCCESS or FAILURE immediately; a
condition should never return RUNNING.
from bteng import ConditionNode, NodeStatus
class BatteryOK(ConditionNode):
def tick(self):
level = self.blackboard.get("battery_level", 0)
return NodeStatus.SUCCESS if level > 20 else NodeStatus.FAILURE
For one-off inline logic, BTEng also supports lambda-based leaves:
.action("Navigate", lambda: NodeStatus.RUNNING)
.condition("BatteryOK", lambda: bb.get("battery_level", 0) > 20)
Lambdas may return NodeStatus or a boolean. True maps to SUCCESS, False maps
to FAILURE.
Control nodes route ticks to their children based on results. They never do work directly — they orchestrate.
Ticks children left-to-right. Succeeds only when all children succeed. Stops at the first failure.
Sequence
├── A → SUCCESS (move to next child)
├── B → RUNNING (return RUNNING; resume here next tick)
└── C (not reached yet)
A Sequence is the “do A, then B, then C” pattern. Think of it as a checklist —
if any step fails, the whole sequence fails.
from bteng import Blackboard, NodeStatus, TreeBuilder, TreeExecutor
bb = Blackboard.create("sequence_example")
bb.set("battery_ok", True)
bb.set("path_clear", True)
tree = (
TreeBuilder(blackboard=bb)
.sequence("mission")
.condition("BatteryOK", lambda: bb.get("battery_ok", False))
.condition("PathClear", lambda: bb.get("path_clear", False))
.action("Navigate", lambda: NodeStatus.SUCCESS)
.end()
.build()
)
executor = TreeExecutor()
executor.set_tree(tree)
print(executor.tick_until_result(max_ticks=10))
# NodeStatus.SUCCESS
Ticks children left-to-right. Succeeds when the first child succeeds. Returns
FAILURE only when all children fail.
Fallback
├── A → FAILURE (try next)
├── B → SUCCESS (return SUCCESS, skip C)
└── C (not reached)
A Fallback is the “try this, or if that fails, try something else” pattern. Think
of it as a ranked list of alternatives.
bb = Blackboard.create("fallback_example")
bb.set("path_clear", False)
tree = (
TreeBuilder(blackboard=bb)
.fallback("navigate_or_stop")
.sequence("main_path")
.condition("PathClear", lambda: bb.get("path_clear", False))
.action("Navigate", lambda: NodeStatus.SUCCESS)
.end()
.action("Stop", lambda: NodeStatus.SUCCESS)
.end()
.build()
)
executor = TreeExecutor()
executor.set_tree(tree)
print(executor.tick_until_result(max_ticks=10))
# NodeStatus.SUCCESS (Stop succeeds because Navigate is blocked)
Ticks all children on every tick, in the same thread, sequentially. Returns based on configurable success and failure thresholds.
from bteng import ParallelNode
parallel = ParallelNode(
"monitor_and_act",
success_threshold=2, # succeed when 2 children succeed
failure_threshold=1, # fail when 1 child fails
children=[monitor, move, report],
)
Use Parallel to run conceptually simultaneous behaviors — monitoring sensors while
executing a motion, for example. Note that it is not true concurrency; for background
threads, use AsyncActionNode.
A decorator wraps a single child and modifies its result or its execution rate.
| Decorator | Effect |
|---|---|
Inverter |
Flips SUCCESS ↔ FAILURE; passes RUNNING through |
Retry(n) |
Re-ticks child on FAILURE up to n times; returns FAILURE after all attempts |
Timeout(t) |
Returns FAILURE if the child takes longer than t seconds |
RateController(hz) |
Limits how often the child is ticked |
ForceSuccess |
Always returns SUCCESS; passes RUNNING through |
ForceFailure |
Always returns FAILURE; passes RUNNING through |
# Retry an unreliable action up to three times
tree = (
TreeBuilder(blackboard=bb)
.retry(max_attempts=3)
.action("TryConnect", lambda: NodeStatus.FAILURE)
.end()
.build()
)
Standard SequenceNode and FallbackNode remember which child was RUNNING and
resume there on the next tick. This is efficient, but it means earlier conditions are
not re-checked while a later action is still running.
Reactive variants fix this by restarting from child[0] every tick.
| Node | Behavior |
|---|---|
ReactiveSequenceNode |
Re-evaluates earlier conditions so they can interrupt running actions |
ReactiveFallbackNode |
Re-evaluates higher-priority branches so priorities can preempt work |
BTEng implements reactive re-evaluation using dirty-flag subscriptions rather than
naive polling. When an action enters RUNNING, the reactive parent subscribes to all
Blackboard instances in its subtree. Any blackboard write sets a dirty flag,
triggering a full re-evaluation from child[0] on the next tick. Without a write, the
fast path skips conditions and ticks only the running action.
# PathClear is re-checked every tick while Navigate is running.
# If path_clear becomes False, Navigate is halted immediately.
tree = (
TreeBuilder(blackboard=bb)
.reactive_sequence("guarded_navigate")
.condition("PathClear", lambda: bb.get("path_clear", False))
.action("Navigate", NavigateAction)
.end()
.build()
)
See Reactive execution internals for the full implementation details.
The blackboard is the shared data store for a tree. Nodes communicate through it rather than through direct references — an action writes a result, a condition reads it, and neither node knows the other exists.
from bteng import Blackboard
bb = Blackboard.create("robot_state")
bb.set("goal", (1.0, 2.0))
bb.get("goal") # (1.0, 2.0)
bb.get("missing", "default") # "default"
bb.has("goal") # True
Blackboard.create("name") is a named singleton. The same name always returns the
same blackboard instance. This makes it easy to share state across nodes that are
constructed in different parts of your code.
For subtree port isolation, child scopes allow scoped namespaces with explicit key remapping. See Blackboard scoping.
Ports declare what data a node needs (inputs) and produces (outputs). They are the typed interface between a node and the blackboard.
from bteng import ActionNode, InputPort, OutputPort, NodeStatus
class Navigate(ActionNode):
@classmethod
def provided_ports(cls):
return [
InputPort("goal", description="Target position"),
OutputPort("result", description="Navigation outcome"),
]
def tick(self):
goal = self.get_input("goal")
# ... navigate to goal ...
self.set_output("result", "arrived")
return NodeStatus.SUCCESS
Port mappings connect a node’s named ports to blackboard keys. They are set at
construction time via TreeBuilder.map() / .literal() or via XML attributes.
TreeExecutor.set_tree() validates all declared ports before the first tick.
See Ports and validation for details.
The tick loop is how the tree advances. One call to executor.tick_once() triggers
one complete traversal from the root.
executor.tick_once()
└─ tree.tick_once()
└─ root.execute_tick()
└─ (children called recursively)
The tree is fully synchronous — one tick completes before the next begins. This makes execution deterministic and easy to trace.
For background work (I/O, blocking calls, slow hardware), use AsyncActionNode. It
runs its work in a separate thread while the main tick loop continues returning
RUNNING.
TreeExecutor provides several execution modes:
from bteng import TreeExecutor
executor = TreeExecutor()
executor.set_tree(tree)
# Tick exactly once
status = executor.tick_once()
# Tick until SUCCESS or FAILURE (or max_ticks exceeded)
status = executor.tick_until_result(max_ticks=100)
# Background event loop with configurable tick rate
executor.start_event_loop()
# ... your application runs ...
executor.stop_event_loop()
| Concept | Role |
|---|---|
NodeStatus |
The return value of every tick: SUCCESS, FAILURE, or RUNNING |
| Leaf node | Does real work: ActionNode (tasks) or ConditionNode (checks) |
| Control node | Routes ticks: Sequence (AND), Fallback (OR), Parallel (all) |
| Decorator | Wraps one child and modifies its behavior |
| Blackboard | Shared data store; nodes communicate through named keys |
| Port | Typed declaration of what a node reads from and writes to the blackboard |
TreeBuilder |
Python API for constructing trees fluently |
TreeExecutor |
Runtime that ticks the tree and wires introspection tools |
Next: Install, then 5-minute Quick Start.