The blackboard is the shared data store for a behavior 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.
This decoupling is intentional. It means you can compose nodes freely without creating dependencies between them.
from bteng import Blackboard
bb = Blackboard.create("robot_state") # named singleton
bb.set("goal", (1.0, 2.0))
bb.set("speed", 0.5)
bb.set("flag", None) # None is a valid stored value
bb.get("goal") # (1.0, 2.0)
bb.get("speed") # 0.5
bb.get("flag") # None (the stored None, not the missing-key default)
bb.get("missing", "default") # "default" (key absent — fallback used)
bb.has("goal") # True
bb.has("flag") # True — None counts as set
bb.has("missing") # False
bb.remove("goal")
bb.has("goal") # False
!!! note “Named singletons”
Blackboard.create("name") always returns the same object for a given name.
Two parts of your code can both call Blackboard.create("robot_state") and they
get the same blackboard. This makes wiring up nodes easy, but it also means state
leaks between tests unless you call Blackboard.reset("name") in teardown.
The blackboard is passed to TreeBuilder at construction time and is available to all
nodes in the tree through self.blackboard:
from bteng import Blackboard, NodeStatus, TreeBuilder, TreeExecutor
bb = Blackboard.create("demo")
bb.set("ready", True)
bb.set("goal", "dock")
tree = (
TreeBuilder(blackboard=bb)
.sequence("root")
.condition("Ready", lambda: bb.get("ready", False))
.action("PrintGoal", lambda: print(bb.get("goal")) or NodeStatus.SUCCESS)
.end()
.build()
)
executor = TreeExecutor()
executor.set_tree(tree)
print(executor.tick_until_result(max_ticks=10))
Expected output:
dock
NodeStatus.SUCCESS
Inside a custom node class, access the blackboard via self.blackboard:
from bteng import ActionNode, ConditionNode, NodeStatus
class ReadGoal(ActionNode):
def tick(self):
goal = self.blackboard.get("goal", None)
if goal is None:
return NodeStatus.FAILURE
print(f"Moving to {goal}")
return NodeStatus.SUCCESS
class SetResult(ActionNode):
def tick(self):
self.blackboard.set("mission_done", True)
return NodeStatus.SUCCESS
For stricter data contracts with validation, use ports (InputPort / OutputPort)
instead of direct get/set calls. See Actions and Conditions
for port basics.
Take a point-in-time snapshot of all current keys and values:
snapshot = bb.snapshot() # dict copy — safe to inspect after the run
print(snapshot)
The blackboard records every write in a history log. Useful for debugging and post-run analysis:
history = bb.history("goal") # list of BlackboardHistoryRecord
for record in history:
print(record.timestamp, record.writer, record.value)
BlackboardHistoryRecord fields:
| Field | Type | Meaning |
|---|---|---|
timestamp |
float |
Unix timestamp of the write |
writer |
str |
Name of the node that wrote (empty string if unset) |
value |
Any | The value that was written |
key |
str |
The blackboard key |
Subscribe a callback to be notified on any write:
sub_id = bb.subscribe(lambda key, val: print(f" {key} → {val}"))
bb.set("speed", 1.5) # prints: speed → 1.5
bb.set("goal", "home") # prints: goal → home
bb.unsubscribe(sub_id)
Subscriptions are used internally by ReactiveSequenceNode and ReactiveFallbackNode
to detect blackboard writes and trigger re-evaluation. When you use reactive nodes, the
subscription mechanism runs transparently in the background.
Because Blackboard.create() returns a singleton, tests can leak state into each
other if you do not clean up. Always reset named blackboards in test teardown:
import pytest
from bteng import Blackboard
@pytest.fixture(autouse=True)
def clean_blackboard():
yield
Blackboard.reset("robot_state")
Or reset manually between test runs:
def teardown_function():
Blackboard.reset("robot_state")
Blackboard.reset("name") clears all keys, history, and subscriptions for that
blackboard and removes it from the singleton registry.
For subtree isolation, a blackboard can have child scopes — isolated namespaces
with explicit key remapping to the parent. This is used by SubTree to prevent the
subtree’s internal keys from leaking into the parent blackboard.
parent = Blackboard.create("robot")
parent.set("goal", (1.0, 2.0))
# "local_goal" inside the child maps to "goal" in the parent
child = parent.create_child_scope("subtree", remapping={"local_goal": "goal"})
child.get("local_goal") # reads parent["goal"] → (1.0, 2.0)
child.set("local_goal", (5.0, 0.0)) # writes parent["goal"]
print(parent.get("goal")) # (5.0, 0.0)
Rules:
See Blackboard scoping for the full details and subtree examples.
| Operation | Method |
|---|---|
| Store a value | bb.set("key", value) |
| Read a value | bb.get("key", default) |
| Check presence | bb.has("key") |
| Remove a key | bb.remove("key") |
| Snapshot all keys | bb.snapshot() |
| Read write history | bb.history("key") |
| Subscribe to writes | bb.subscribe(callback) |
| Clear for tests | Blackboard.reset("name") |
Next: TreeBuilder tutorial.