Dataflow
Dataflow makes values on the Blackboard usable as part of a process execution. The most common use case is data flow between skills, where a skill's return value is used within another skill's parameters.
In general, Dataflow consists of some nodes providing output values in the form of protobuf messages and some nodes using these outputs as part of their inputs. Upon executing a node that produces an output, the executive writes this output to a blackboard key that is specified in the behavior tree. Input values for a node are used in parameter assignments or conditions. Inputs are specified in the behavior tree via expressions over blackboard keys. When a node with an input is being executed, the executive retrieves the values from the blackboard and inserts these into the expression specified for the input.
This guide describes how and when the executive writes output values from nodes. It also describes when input values are read by the executive and how they are provided as inputs to a node.
Practical examples show how Dataflow is handled in Flowstate or the solution building library.
Output values
During execution different nodes can produce output values. These are the return values of Skills, Python script nodes, or Data nodes, and Loop and Retry counters. If a node produces an output from its execution the executive writes this output to the blackboard using the blackboard key defined in the behavior tree.
Return values
Each skill or Python script node in a process can provide a so-called return value.
The behavior tree defines a return value name, which is the key of the blackboard value to be written.
The return value is a proto that contains the results of the execution.
The executive writes this to the blackboard after a node successfully finishes its execution.
The Flowstate frontend or the solution building library will generate a unique key for each node
automatically. The key can be inspected and changed
in the UI by selecting a node and looking at the Details tab.
If a node is executed multiple times (e.g., in a loop) it will override its return value each time on the same key. The return value of the previous execution is not retained anywhere.
Loop and retry counters
Loop and retry nodes define a blackboard key for their counters. Counter variables are only set while a loop or retry node is executing. These are often used to identify the i-th value when looping over a list of return values.
Input values
Values from the blackboard are read in multiple contexts, e.g., when assigning values to a skill or Python script node, or when evaluating branch node or loop conditions. In each case, the values are read when the reading node is executed. An error occurs if a value is not available.
CEL expressions
Dataflow relies on the CEL expression language. The Common Expression Language (CEL) allows you to write expressions over protobuf messages.
A simple example like skill_a_return.foo_value returns the foo_value field in the skill_a_return message. Identifiers within a CEL expression (here, e.g., skill_a_return)
are keys of the blackboard. For example, they correspond to the return value name defined for a skill's return value.
For more complex expressions, consult the CEL website.
Currently, Dataflow involving map values is not fully supported. In particular assignments to/from maps values are not possible.
Conditions
A condition with a BlackboardExpression
specifies a CEL expression. A CEL expression in a condition must always evaluate to a Boolean value, e.g., skill_a_return.foo_value === skill_b_return.bar_value.
Assignments
Assignments are used to assign fields in a skill or Python script node parameter message from values on the Blackboard, e.g., return values of other skills. A ParameterAssignment is a pair consisting of parameter path and CEL expression.
The parameter path defines which field in the parameters is being assigned. For example, foo.bar_value selects the bar_value field within the foo message of the parameter proto.
For this to work the parameter proto message must have a foo field and the message type of foo must have a bar_value field.
The CEL expression must evaluate to a type that can be assigned to the field at the parameter path. For example, when foo.bar_value is a string field, the result of the CEL expression must be a string.
Assignments between full proto messages are also possible. The message being assigned and the message being assigned from must be binary compatible.
Examples
Simple assignment
message FooReturn {
string foo_value = 1;
}
message BarParameters {
string bar_str = 42;
int64 bar_int = 99;
}
We are executing skill com.example.foo that has a return value type FooReturn in a Task node that defines a return value name of foo_result.
The skill returns foo_value: "Intrinsic".
After that we are executing com.example.bar, which has parameters of type BarParameters. We define its parameters in the behavior tree as: bar_str: "hello" bar_int: 7.
In addition we define the ParameterAssignment with a parameter path of bar_str and a CEL expression of foo_result.foo_value.
The skill com.example.bar is called with bar_str: "Intrinsic" bar_int: 7.
Message assignment
message CustomValue {
string my_value = 1;
}
message FooReturn {
CustomValue foo_value = 1;
}
message BarParameters {
CustomValue bar_value = 42;
int64 bar_int = 99;
}
We are executing skill com.example.foo that has a return value type FooReturn in a Task node that defines a return value name of foo_result.
The skill returns foo_value {my_value: "Intrinsic"}.
After that, we are executing com.example.bar, which has parameters of type BarParameters. We define its parameters in the behavior tree as: bar_value: {my_value: "hello"} bar_int: 7.
In addition we define the ParameterAssignment with a parameter path of bar_value and a CEL expression of foo_result.foo_value.
The skill com.example.bar is called with bar_value: {my_value: "Intrinsic"} bar_int: 7. Note that both skills use the same message definition for CustomValue.
List assignment
message CustomValue {
string my_value = 1;
}
message FooReturn {
repeated CustomValue foo_value = 1;
}
message BarParameters {
CustomValue bar_value = 42;
int64 bar_int = 99;
}
We are first executing skill com.example.foo that has a return value type FooReturn in a Task node that defines a return value name of foo_result.
The skills return foo_value {my_value: "Intrinsic 1"}``foo_value {my_value: "Intrinsic 2"}``foo_value {my_value: "Intrinsic 3"}.
After that we are executing com.example.bar, which has parameters of type BarParameters in a loop node with 3 iterations. The loop defines its loop counter key as loop_counter.
We define com.example.bar's parameters in the behavior tree as: bar_value: {my_value: "hello"} bar_int: 7.
In addition we define the ParameterAssignment with a parameter path of bar_value and a CEL expression of foo_result.foo_value[loop_counter].
The skill com.example.bar will be called three times with
bar_value: {my_value: "Intrinsic 1"} bar_int: 7,
bar_value: {my_value: "Intrinsic 2"} bar_int: 7, and
bar_value: {my_value: "Intrinsic 3"} bar_int: 7.