A guard condition is a condition node that sits before a long-running action. When the condition fails, the action is halted immediately — even if it has been running for many ticks. This is the standard way to implement safety interlocks, priority interrupts, and preemption in behavior trees.
With a standard SequenceNode, the sequence remembers which child was RUNNING and
resumes there on the next tick. Earlier conditions are not re-checked while a
later action is still running:
Standard Sequence — tick 1:
├── PathClear → SUCCESS (checked, passes)
└── Navigate → RUNNING (starts navigation)
Standard Sequence — tick 2:
└── Navigate → RUNNING (resumed here; PathClear is NOT re-checked)
If path_clear becomes False during tick 2, the robot keeps navigating — it won’t
notice until Navigate finishes on its own.
Use a ReactiveSequenceNode. It restarts from child[0] on every tick where a
blackboard write has occurred:
ReactiveSequence — tick 1:
├── PathClear → SUCCESS (checked, passes)
└── Navigate → RUNNING (starts navigation)
ReactiveSequence — tick 2 (after bb.set("path_clear", False)):
├── PathClear → FAILURE (re-checked! condition fails)
└── Navigate → (halted) (Navigate.halt() is called automatically)
ReactiveSequence returns FAILURE.
from bteng import Blackboard, NodeStatus, TreeBuilder, TreeExecutor
bb = Blackboard.create("guard_recipe")
bb.set("path_clear", True)
tree = (
TreeBuilder(blackboard=bb)
.reactive_sequence("navigate_safely")
.condition("PathClear", lambda: bb.get("path_clear", False))
.action("Navigate", lambda: NodeStatus.RUNNING)
.end()
.build()
)
executor = TreeExecutor()
executor.set_tree(tree)
# First tick — path is clear, Navigate starts
status = executor.tick_once()
print("Tick 1:", status)
# Something changes the world state
bb.set("path_clear", False)
# Second tick — PathClear is re-evaluated, Navigate is halted
status = executor.tick_once()
print("Tick 2:", status)
Expected output:
Tick 1: NodeStatus.RUNNING
Tick 2: NodeStatus.FAILURE
When Navigate enters RUNNING, the ReactiveSequenceNode subscribes to all
Blackboard instances reachable in its subtree. Any subsequent write sets a dirty
flag. On the next tick, the dirty flag causes full re-evaluation from child[0].
Without a write between ticks, the fast path applies: conditions are skipped and only the running action is ticked. This means the reactive overhead is proportional to how often the blackboard is written, not to how many ticks the action runs.
A reactive sequence can have any number of conditions before the action. All of them are re-checked when the blackboard is written:
tree = (
TreeBuilder(blackboard=bb)
.reactive_sequence("safe_navigate")
.condition("BatteryOK", lambda: bb.get("battery_level", 0) > 20)
.condition("PathClear", lambda: bb.get("path_clear", False))
.condition("NoEmergency",lambda: not bb.get("emergency_stop", False))
.node("Navigate", NavigateAction)
.end()
.build()
)
If any condition fails, the sequence returns FAILURE and Navigate is halted.
Wrap the guarded sequence in a fallback to handle interruption gracefully:
tree = (
TreeBuilder(blackboard=bb)
.fallback("navigate_or_wait")
.reactive_sequence("guarded_navigate")
.condition("PathClear", lambda: bb.get("path_clear", False))
.node("Navigate", NavigateAction)
.end()
.action("Wait", lambda: NodeStatus.RUNNING) # wait until path clears
.end()
.build()
)
When PathClear fails, the reactive sequence fails, the fallback moves to Wait,
which returns RUNNING. On the next tick, if PathClear succeeds again, the reactive
sequence succeeds and the fallback short-circuits back to it.
| Situation | Use a reactive guard? |
|---|---|
| Safety interlock that must stop motion immediately | Yes |
| Condition checked at start of task only | No — use standard Sequence |
| Priority preemption (higher-priority task becomes available) | Yes — use ReactiveFallbackNode |
| Condition that changes slowly or infrequently | Yes — reactive fast path is cheap when bb is not written |
!!! warning “Keep guard conditions cheap” Guard conditions are re-evaluated on every tick where a blackboard write occurred. Avoid slow blocking calls (network, file I/O, heavy computation) inside a condition used as a guard. Conditions should read state, not compute it.