Skip to main content

Creating Behavior Plugins — Best Practices

Reference docs: src/docs/docs/concepts/creating_behaviors/creating_behaviors.md, src/docs/docs/how_to/custom_behaviors/adding_ports/adding_ports.md, src/behavior_interface/README_PUBLIC.md.

Choosing a Base Class

  • SharedResourcesNode<BT::SyncActionNode> — Synchronous, completes in a single tick. Use when work is fast (<1ms). Override tick().
  • AsyncBehaviorBase — For long-running tasks (planning, vision inference). Returns RUNNING until complete. Override doWork() returning tl::expected<bool, std::string>.
  • GetMessageFromTopicBehaviorBase<MsgT> — Subscribes to a ROS topic and outputs the latest message.
  • ServiceClientBehaviorBase<SrvT> — Calls a ROS service. Override getServiceName(), createRequest(), processResponse().
  • ActionClientBehaviorBase<ActionT> — Calls a ROS action server.
  • For Behaviors that don't need ROS at all, inherit directly from BT::SyncActionNode or BT::StatefulActionNode.

Class Naming

  • Use PascalCase: ComputeTrayPlacePositionsUsingAprilTags, not compute_tray_place_positions.
  • Be descriptive and specific. Prefer GetCenterMostAprilTag over GetTag. Include the method/sensor when relevant (e.g., UsingAprilTags, FromPointCloud).
  • File names use snake_case matching the class: compute_tray_place_positions_using_apriltags.hpp/.cpp.

Header File (include/.../my_behavior.hpp)

  • Include the PickNik copyright header (use the current year).
  • Use #pragma once.
  • Mark the class final unless designed for inheritance.
  • Add a @brief one-liner and @details block with a markdown port table listing every port:
     /**
      * @brief Brief description of what this Behavior does.
      *
      * @details Longer explanation of the algorithm, inputs, and outputs.
      *
      * | Data Port Name | Port Type | Object Type |
      * | --------------- | --------- | --------------------------------- |
      * | input_pose | input | geometry_msgs::msg::PoseStamped |
      * | output_pose | output | geometry_msgs::msg::PoseStamped |
      */
  • Port Type column values: input, output, or bidirectional.
  • Document the constructor parameters with @param tags.

Source File (src/.../my_behavior.cpp)

Port Constants

Define port name constants as constexpr auto in an anonymous namespace. Use the kPortID prefix:

 namespace
 {
 constexpr auto kPortIDInputPose = "input_pose";
 constexpr auto kPortIDOutputPose = "output_pose";
 constexpr auto kPortIDMarkerSize = "marker_size";
 }

Port Definitions (providedPorts())

  • BT::InputPort<T> — Data flowing into the Behavior (read-only). This is the most common port type.
    • Required inputs use a blackboard placeholder: BT::InputPort<T>("name", "{placeholder}", "Description.")
    • Optional inputs with defaults: BT::InputPort<T>("name", default_value, "Description.")
  • BT::OutputPort<T> — Data written by the Behavior. Always use a blackboard placeholder: BT::OutputPort<T>("name", "{placeholder}", "Description.")
  • BT::BidirectionalPort<T> — Reads and writes the same port (e.g., accumulating into a vector). Use sparingly.
  • Most ports should be InputPort. Only use OutputPort for data the Behavior produces. Only use BidirectionalPort when the Behavior must modify an existing blackboard value in-place.

Port Descriptions

Every port must have a description string. Descriptions should:

  • Be a complete sentence ending with a period.
  • Explain the purpose, not just restate the type (e.g., "Distance in meters between adjacent place positions." not "A double.").
  • Mention units where applicable (meters, seconds, radians).
  • Note constraints or special values (e.g., "0 means forever.", "Negative indexes start backwards from the last element.").
  • For optional ports, state what happens when not provided (e.g., "If not provided, visualization is skipped.").

Port Naming Conventions

  • Use snake_case for port names: input_pose, camera_info, tray_apriltag_id.
  • Prefix input ports with context when ambiguous: input_image, input_pose, target_frame_id.
  • Suffix output ports descriptively: place_positions, detection_pose, annotated_image.
  • For topic name ports, use _topic suffix: visualization_topic.
  • Match existing conventions — check similar Behaviors before inventing new names.

Metadata

Provide both subcategory and description metadata:

 BT::KeyValueVector MyBehavior::metadata()
 {
  return { { kSubcategoryMetadataKey, "Vision" },
  { kDescriptionMetadataKey, kDescriptionMyBehavior } };
 }

Subcategory examples: "Vision", "Pose Handling", "Vector Handling", "Motion - Planning", "Perception - 2D Image".

The description is HTML wrapped in <p> tags, used in the Behavior Hub UI:

 inline constexpr auto kDescriptionMyBehavior = R"(
  <p>
  Clear, concise description. Explain inputs, outputs, and side effects.
  </p>
  )";

Input Validation

Use getRequiredInputs() for ports that must be set:

 const auto ports = getRequiredInputs(
  getInput<SomeMsg>(kPortIDDetections),
  getInput<CameraInfo>(kPortIDCameraInfo));
 if (!ports.has_value())
 {
  getBehaviorContext()->logger->publishFailureMessage(
  name(), "Failed to get required values from input data ports: " + ports.error());
  return BT::NodeStatus::FAILURE;
 }
 const auto& [detections, camera_info] = ports.value();

For optional ports with defaults, use .value() or .value_or():

 const double marker_size = getInput<double>(kPortIDMarkerSize).value(); // has default
 const bool visualize = getInput<bool>(kPortIDVisualize).value_or(false);

Registration

  1. Add #include in register_vision_behaviors.cpp (or the appropriate register_*_behaviors.cpp), keeping alphabetical order.
  2. Add registerBehavior<MyBehavior>(factory, "MyBehavior", shared_resources); in the registration function.
  3. Add "MyBehavior" to the corresponding kXxxBehaviorsScenario list in test/test_load_behavior_loader_plugin.cpp, alphabetically.

CMake

Add a static library block in the appropriate CMakeLists.txt:

 add_library(my_behavior STATIC my_behavior.cpp)
 target_link_libraries(my_behavior
  PUBLIC rsl::rsl core_behaviors_utils <other_public_deps>)
 target_link_libraries(my_behavior PRIVATE fmt::fmt) # if using fmt
 target_include_directories(my_behavior
  PUBLIC $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include>)

Then add it to the core_behaviors_vision (or core_behaviors) PRIVATE link list in the shared library target.

Testing

  • At minimum, every Behavior must be listed in test_load_behavior_loader_plugin.cpp — this verifies it can be loaded and instantiated.
  • For thorough testing, use the WithBehavior<T> test fixture from moveit_pro_behavior_interface:
    • Define a port setter map with BEGIN_BEHAVIOR_PORT_SETTER_MAP / DEFINE_BEHAVIOR_PORT_SETTER / DEFINE_OPTIONAL_BEHAVIOR_PORT_SETTER.
    • Use INSTANTIATE_SYNC_BEHAVIOR_PORT_NOT_SET_TESTS (or ASYNC variant) to auto-generate tests for each required port.
  • Always set tf2_spin_thread=false in BehaviorContext for tests.

Common Patterns

  • TF lookups: Use getBehaviorContext()->transform_buffer_ptr->canTransform() before lookupTransform(). Include <tf2_eigen/tf2_eigen.hpp> for tf2::transformToEigen().
  • Image annotation: Use cv_bridge::toCvCopy() + OpenCV drawing + ROSPublisherHandle to publish annotated images.
  • Camera intrinsics: Extract from CameraInfo.k[]fx=k[0], fy=k[4], cx=k[2], cy=k[5].
  • Pinhole projection: u = fx * (x/z) + cx, v = fy * (y/z) + cy.
  • Publishing one-shot data to the UI: use latched QoS — rclcpp::QoS(rclcpp::KeepLast(1)).reliable().transient_local(). foxglove_bridge subscribes on the UI's behalf and the frontend adapter requests no QoS, so durability must come from the publisher: latching lets DDS deliver the most recent message to a UI that subscribes or reconnects later. Continuous-stream topics (joint states, jog commands) don't need this. Where the publisher lives depends on how long the sample must outlive the publish:
    • If the latched sample only needs to reach a UI that connects during the same Objective run, a publisher held as a Behavior member (kept alive across ticks) is fine. See switch_ui_primary_view.cpp.
    • If it must survive past the run — e.g. a snapshot the UI re-fetches after a page reload — a Behavior-owned publisher is destroyed when the tree is torn down at the end of the Objective, retracting the latched sample. Store it on the long-lived BehaviorContext via persistent_publishers.getOrCreate(topic, factory) instead, and clear it when the snapshot is invalidated via persistent_publishers.clear(). See send_point_cloud_to_ui.cpp (stores) and clear_snapshot.cpp (clears).

Design Tips

  • .value() vs .value_or(): For ports with defaults defined in providedPorts(), use .value()getInput() is guaranteed to succeed because the port always has a value. Reserve .value_or() for truly optional ports that may not be wired in the behavior tree at all.
  • Keep tick() focused: If tick() grows beyond ~40 lines, extract logical blocks into private helper methods (e.g., publishVisualizationMarkers()). This makes tick() read like an outline and keeps each method testable.
  • Name utilities broadly: Name utility files/classes by what they do, not by the first use case. For example, marker_utils (handles lines, poses, text, delete-all) is better than pose_marker_utils.

Generated via doxygen2docusaurus 2.2.2 by Doxygen 1.9.8.