Behaviour trees on the robot

xparo_ros contains its own behaviour tree engine. When a task runs, the robot builds the tree from its XML, ticks it 10 times per second, streams every node’s state to the dashboard, and ends with one result that explains what happened. This page is the reference for what that engine accepts.

You normally build trees visually on the Behaviour page; this page explains what the XML underneath means.

The format

Trees use the BehaviorTree.CPP v4 XML format, the same one Groot2 uses. A tree is a nest of nodes:

<Sequence name="deliver_medicine">
  <ScriptCondition name="battery_ok" code="battery > 20" />
  <Script name="pick_room" code="room := destination" />
  <SubTree ID="go_to_room" target="{room}" />
  <Wait name="wait_at_door" seconds="1" />
</Sequence>
  • Tag (Sequence, Wait, …): which node it is.

  • name: your label. The dashboard uses it to show which node is running, so give every node you care about a unique name.

  • Other attributes are the node’s ports (inputs and outputs). seconds="1" is a fixed value; target="{room}" reads the blackboard entry room.

The dashboard stores just the inside of the tree (the part above). The robot also accepts a whole Groot2 file (<root><BehaviorTree ID="...">), running its main_tree_to_execute tree with the others available as SubTrees.

The blackboard

The blackboard is the tree’s shared memory, a set of named values.

  • A task fills it before the run starts: every {name} your tree reads is listed on the task’s Task Behaviour tab, where you choose where its value comes from (see Tasks (Step 2)).

  • Script and SetBlackboard nodes, and output ports, write to it while the tree runs.

  • Values that come from XML or task fields arrive as text; comparisons treat number-like text as numbers, so battery > 20 works when battery is "55".

The run’s result includes the blackboard before and after, and what changed.

Nodes you can use

All BehaviorTree.CPP v4 built-in nodes (the ones Groot2 shows) run on the robot:

Kind

Nodes

Control

Sequence, Fallback, ReactiveSequence, ReactiveFallback, SequenceWithMemory, AsyncSequence, AsyncFallback, Parallel, ParallelAll, IfThenElse, WhileDoElse, Switch2 to Switch6. Older names Selector and SequenceStar are accepted too.

Decorator

Inverter, ForceSuccess, ForceFailure, Repeat, RetryUntilSuccessful, KeepRunningUntilFailure, Timeout, Delay, RunOnce, Precondition, SkipUnlessUpdated, WaitValueUpdate, LoopInt, LoopDouble, LoopBool, LoopString

Action

AlwaysSuccess, AlwaysFailure, Script, SetBlackboard, UnsetBlackboard, Sleep

Condition

ScriptCondition, WasEntryUpdated

Other

SubTree runs another tree of the project in place (see below).

Plus XPARO’s own nodes:

Node

Ports

What it does

Wait

seconds (default 1)

Stays RUNNING for that many seconds, then SUCCESS.

ParamSet

node_name, param_name, param_value, param_type

Sets a ROS 2 parameter on another node through its set_parameters service. param_type: bool, int, double, string, or the _array form of each.

CheckBatteryLevel

min_level

Placeholder (see the warning below).

NavigateTo

location

Placeholder.

DockRobot

dock_method

Placeholder.

SpeakText

text

Placeholder.

PlayAudio

file_path

Placeholder.

NotifyPatient

tray

Placeholder.

LoadNextDelivery

(none)

Placeholder.

Warning

The seven placeholder nodes do not move hardware yet. They check that their required ports have values, wait 0.3 seconds and return SUCCESS (CheckBatteryLevel always passes, it does not read a battery). They let you build and test the flow of a tree today. For real behaviour, use your own custom nodes (Python, C++, JavaScript or Bash), ParamSet, or your own ROS 2 nodes listening on topics.

ManualSelector is the one BehaviorTree.CPP node that is not supported: it needs a person at the robot’s console to choose a branch.

Your own nodes run exactly like these. See Custom nodes.

Scripts and conditions

Script, ScriptCondition and the _skipIf/_successIf/ _failureIf/_while attributes use a small, safe expression language (not Python):

You can write

Example

blackboard names

battery, destination

text, numbers, true/false

'ward_3', 42, 2.5, true

arithmetic

speed * 2, path + '/sound.mp3'

comparisons

==, !=, <, <=, >, >=

logic

&& / and, || / or, ! / not

assignments (Script only)

room := destination; also =, +=, -=, *=, /=; several separated by ;

Function calls, attribute access, indexing and imports are not allowed.

ScriptCondition returns SUCCESS when its code is true and FAILURE when it is false. A name that is not on the blackboard makes it fail with undefined variable.

Pre-conditions. Any node can carry these attributes, evaluated before it runs (a missing variable counts as false here):

Attribute

Effect when the expression is true

_skipIf

The node is skipped and reports SUCCESS.

_failureIf

The node is not run and reports FAILURE.

_successIf

The node is not run and reports SUCCESS.

_while

The node runs only while it stays true; if it becomes false the node is stopped.

SubTrees

<SubTree ID="go_to_room" target="{room}" /> runs the project’s tree named go_to_room in place:

  • It shares the parent’s blackboard, so it sees every value.

  • Port attributes are copied in when it starts (target="{room}" sets the subtree’s target to the parent’s room; target="kitchen" sets the text), and {name} ports are copied back out when it ends.

  • The robot uses the tree as synced to it. A SubTree naming a tree the robot does not have is an invalid_tree error.

  • SubTrees can nest up to 16 levels; a tree including itself is an error.

Limits and differences from BehaviorTree.CPP

  • A tree may have up to 4,000 nodes and 200 levels of nesting.

  • There is no SKIPPED status: a skipped node reports SUCCESS and the run report says it was skipped.

  • Loops whose child finishes instantly (Repeat, RetryUntilSuccessful, KeepRunningUntilFailure, Loop*) run at most 50 rounds per tick, so an endless loop cannot freeze the robot.

  • Parallel and Async* nodes take turns within the engine’s single tick loop; they are not separate threads.

Checking before running

Before ticking anything, the robot checks the whole tree: unknown node names (a typo, or a custom node that failed to sync), the wrong number of children, a SubTree it does not have, a Script that cannot work, an empty tree. If anything is wrong, nothing runs and the result lists every problem with its path:

outcome: invalid_tree
error:   <MoveArm> isn't a node this robot knows. If it's one of your custom nodes,
         check it synced to the robot without errors; otherwise check the spelling
         (at Sequence > MoveArm)

Run results

Every run ends with exactly one result (TASK_RESULT). These examples come from running the tree at the top of this page:

{"success": true, "outcome": "success", "retryable": true, "error": "",
 "explanation": "The tree finished with SUCCESS.", "failed_node": null}

With battery = "10":

{"success": false, "outcome": "failed", "retryable": true, "error": "",
 "explanation": "The tree finished with FAILURE at battery_ok (ScriptCondition): 'battery > 20' is False.",
 "failed_node": {"name": "battery_ok", "tag": "ScriptCondition",
                 "path": "Sequence(deliver_medicine) > ScriptCondition(battery_ok)",
                 "reason": "'battery > 20' is False"}}

A Wait of 5 seconds in a task with a 1 second time limit:

{"success": false, "outcome": "timeout", "retryable": true,
 "error": "Stopped because it hit its 1 s time limit, while long_wait was still running",
 "explanation": "The task was still running when its time limit ran out, so it was stopped.",
 "failed_node": {"name": "long_wait", "tag": "Wait", "path": "Sequence > Wait(long_wait)",
                 "reason": "was still running when it hit its 1 s time limit"}}

A result also carries task_id, run_id, duration_s and stats (how many nodes there are and how many ticks ran).

Outcomes

Outcome

Retried?

Meaning and what to do

success

no need

The tree returned SUCCESS.

failed

yes

The tree returned FAILURE. failed_node says which node and why. Add a Fallback or RetryUntilSuccessful where failure is expected.

invalid_tree

no

Nothing ran because the tree has errors (see above). Fix it on the Behaviour page.

node_error

yes

A node raised an error; the run was stopped and everything running was halted. The task history report has the details.

timeout

yes

The task’s time limit ran out; failed_node is what was still running. Raise the limit or find out why that node never finishes.

cancelled

no

Someone pressed Cancel on the dashboard.

stage_mismatch

no

The task’s stage is not one this robot’s xparo_stage accepts. Promote the task or start the robot with another stage.

unknown_task

no

/xparo/run_task named a task this robot has not synced.

no_executor

no

The XPARO engine is running without its ROS 2 node, so it cannot run trees. Start it with ros2 launch xparo xparo_launch.py.

robot_offline

yes

Sent by the server, not the robot: Run now targeted a robot with no heartbeat in the last 90 seconds, so nothing was sent.

Retried? is the retryable flag. When a task has Restart on failure turned on, the server sends a failed run back to the same robot, but never for an outcome marked “no”, which would fail the same way again.

With Save task history on (the default), the robot also stores the full run report in the project’s Tasks database: every node in tree order with its status, tick count and timings, a status timeline, the problems found before running, and the blackboard before and after. The Tasks database page shows it as a readable report.

Watching a run live

While a tree runs, the robot sends every node state change. The Behaviour page lights up the running tree on its canvas, matching nodes by name: two nodes with the same tag and no name look identical there, so give each node a unique name. The same live view works for Nav2’s own behaviour tree and for your own executor publishing on /bt_xparo_log (see ROS 2 topics).