Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Scaffold a Behavior Tree leaf node — plain BehaviorTree.CPP (SyncActionNode / StatefulActionNode / ConditionNode) or a BehaviorTree.ROS2 wrapper (RosActionNode / RosServiceNode / RosTopicPubNode / RosTopicSubNode) — with ports, factory/plugin registration, and XML v4 usage. Trigger when the user asks to write a behavior-tree node (not Nav 2-specific).
.claude/skills/harunkurtdev-behaviortree-node-creation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 37% | 0% |
| case-08 | ✗→✓ | ▲ Improved | -29% | 0% |
| case-14 | ✗→✓ | ▲ Improved | -2% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 102% | 0% |
| case-23 | ✗→✓ | ▲ Improved | 2% | 0% |
How to write, register, and use a custom behavior-tree leaf node — both plain BehaviorTree.CPP nodes and the ROS 2 wrappers from BehaviorTree.ROS2.
rules/behaviortree_cpp.md.rules/behaviortree_ros2.md.~/nav2_ws/src/BehaviorTree.CPP/sample_nodes/(dummy_nodes, movebase_node)
~/nav2_ws/src/BehaviorTree.ROS2/btcpp_ros2_samples/src/(sleep_action, set_bool_node)
nav2_behavior_tree instead.| Your node… | Base class | Library | |------------|-----------|---------| | does quick work, finishes in one tick | BT::SyncActionNode | BT.CPP | | is async / long-running (no ROS) | BT::StatefulActionNode | BT.CPP | | is a boolean check | BT::ConditionNode | BT.CPP | | calls a ROS 2 action | BT::RosActionNode<ActionT> | BT.ROS2 | | calls a ROS 2 service | BT::RosServiceNode<ServiceT> | BT.ROS2 | | publishes to a topic | BT::RosTopicPubNode<MsgT> | BT.ROS2 | | subscribes to a topic | BT::RosTopicSubNode<MsgT> | BT.ROS2 |
Golden rule: never block in a tick. Sync nodes return immediately; async/ROS nodes return RUNNING and resume on the next tick.
cpp#include "behaviortree_cpp/bt_factory.h" class SaySomething : public BT::SyncActionNode { public: SaySomething(const std::string& name, const BT::NodeConfig& config) : BT::SyncActionNode(name, config) {} static BT::PortsList providedPorts() { // omit if no ports return { BT::InputPort<std::string>("message") }; } BT::NodeStatus tick() override { auto msg = getInput<std::string>("message"); if (!msg) throw BT::RuntimeError("missing 'message': ", msg.error()); std::cout << msg.value() << "\n"; return BT::NodeStatus::SUCCESS; } };
Async work → StatefulActionNode:
cppclass MoveBase : public BT::StatefulActionNode { public: static BT::PortsList providedPorts() { return { BT::InputPort<Pose2D>("goal") }; } BT::NodeStatus onStart() override; // kick off; return RUNNING (or SUCCESS if instant) BT::NodeStatus onRunning() override; // poll; return RUNNING until done, then SUCCESS/FAILURE void onHalted() override; // cancellation cleanup };
cpp#include "behaviortree_ros2/bt_action_node.hpp" #include "btcpp_ros2_interfaces/action/sleep.hpp" using namespace BT; class SleepAction : public RosActionNode<btcpp_ros2_interfaces::action::Sleep> { public: SleepAction(const std::string& name, const NodeConfig& conf, const RosNodeParams& params) : RosActionNode<btcpp_ros2_interfaces::action::Sleep>(name, conf, params) {} static PortsList providedPorts() { return providedBasicPorts({ InputPort<unsigned>("msec") }); // + the action-name port } bool setGoal(Goal& goal) override { // BT input → goal; false ⇒ INVALID_GOAL goal.msec_timeout = getInput<unsigned>("msec").value(); return true; } NodeStatus onResultReceived(const WrappedResult& wr) override { return wr.result->done ? NodeStatus::SUCCESS : NodeStatus::FAILURE; } NodeStatus onFeedback(const std::shared_ptr<const Feedback> fb) override { return NodeStatus::RUNNING; // must NOT return IDLE } NodeStatus onFailure(ActionNodeErrorCode error) override { RCLCPP_ERROR(logger(), "Sleep failed: %d", error); return NodeStatus::FAILURE; } };
Service nodes override setRequest() + onResponseReceived(); pub nodes override setMessage(); sub nodes handle the received message.
Plain BT.CPP:
cppfactory.registerNodeType<SaySomething>("SaySomething"); // free function → factory.registerSimpleAction("CheckBattery", std::bind(CheckBattery)); // plugin: BT_REGISTER_NODES(factory){ factory.registerNodeType<MyNode>("MyNode"); } // → factory.registerFromPlugin("libmy_nodes.so");
ROS 2 (needs RosNodeParams to carry the node handle / timeouts):
cppBT::RosNodeParams params; params.nh = node; // std::shared_ptr<rclcpp::Node> params.default_port_value = "sleep_action"; // default action/service/topic name factory.registerNodeType<SleepAction>("SleepAction", params); // or as a plugin: CreateRosNodePlugin(SleepAction, "SleepAction"); // RegisterRosNode(factory, "libsleep_action.so", params);
xml<root BTCPP_format="4"> <BehaviorTree ID="MainTree"> <Sequence> <SaySomething message="starting"/> <SleepAction msec="2000"/> </Sequence> </BehaviorTree> </root>
cppfactory.registerBehaviorTreeFromFile("main.xml"); auto tree = factory.createTree("MainTree"); tree.tickWhileRunning(); // ROS nodes: spin the node in parallel / use the executor
To expose a whole tree as a ROS 2 action, subclass BT::TreeExecutionServer and override registerNodesIntoFactory() (see behaviortree_ros2.md).
getInput<T>("name") returns BT::Expected<T> — always check it.setOutput("name", value) writes to the blackboard.{key} in XML binds a port to a blackboard entry.BT::convertFromString<T>(StringView).name, ID, anything starting with _.tick() / onRunning() — defeats the tree; returnRUNNING and resume next tick.
onFeedback returning IDLE — not allowed (throws); returnRUNNING (or SUCCESS/FAILURE to stop early).
providedPorts() when the XML uses ports, or a namemismatch between providedPorts and the XML attribute.
RosNodeParams — it needs thenode handle; use the (factory, params) overload / RegisterRosNode.
BTCPP_format="4"; migrate withconvert_v3_to_v4.py.
its type locks; a std::string output can convert, others can't.
the node actually talks to a ROS 2 interface.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 19,474 | 19,685 | +1% | 1 | 1 | 0% | 3,805 | 6,041 | +59% | 0 | 0 | — |
case-02 | pass→pass | 10,518 | 6,767 | -36% | 1 | 1 | 0% | 2,061 | 3,275 | +59% | 0 | 0 | — |
case-03 | pass→pass | 17,457 | 11,412 | -35% | 1 | 1 | 0% | 3,440 | 4,089 | +19% | 0 | 0 | — |
case-04 | pass→pass | 11,860 | 7,615 | -36% | 1 | 1 | 0% | 2,348 | 3,436 | +46% | 0 | 0 | — |
case-05 | fail→pass | 14,559 | 9,702 | -33% | 1 | 1 | 0% | 2,827 | 3,861 | +37% | 0 | 0 | — |
case-06 | pass→pass | 6,589 | 4,381 | -34% | 1 | 1 | 0% | 1,223 | 2,703 | +121% | 0 | 0 | — |
case-07 | pass→pass | 10,189 | 4,710 | -54% | 1 | 1 | 0% | 1,843 | 2,758 | +50% | 0 | 0 | — |
case-08 | fail→pass | 23,077 | 6,691 | -71% | 1 | 1 | 0% | 4,360 | 3,106 | -29% | 0 | 0 | — |
case-09 | pass→pass | 12,121 | 6,270 | -48% | 1 | 1 | 0% | 2,118 | 3,066 | +45% | 0 | 0 | — |
case-10 | pass→pass | 7,479 | 4,054 | -46% | 1 | 1 | 0% | 1,308 | 2,531 | +94% | 0 | 0 | — |
case-11 | pass→pass | 4,028 | 3,062 | -24% | 1 | 1 | 0% | 526 | 2,347 | +346% | 0 | 0 | — |
case-12 | pass→pass | 12,712 | 9,044 | -29% | 1 | 1 | 0% | 2,387 | 3,594 | +51% | 0 | 0 | — |
case-13 | pass→pass | 6,578 | 4,482 | -32% | 1 | 1 | 0% | 1,110 | 2,695 | +143% | 0 | 0 | — |
case-18 | pass→pass | 13,796 | 12,766 | -7% | 1 | 1 | 0% | 2,421 | 4,128 | +71% | 0 | 0 | — |
case-14 | fail→pass | 19,204 | 7,239 | -62% | 1 | 1 | 0% | 3,250 | 3,194 | -2% | 0 | 0 | — |
case-15 | fail→pass | 7,395 | 3,877 | -48% | 1 | 1 | 0% | 1,294 | 2,620 | +102% | 0 | 0 | — |
case-16 | pass→pass | 11,556 | 7,450 | -36% | 1 | 1 | 0% | 2,123 | 3,276 | +54% | 0 | 0 | — |
case-17 | pass→pass | 14,523 | 5,528 | -62% | 1 | 1 | 0% | 2,886 | 2,955 | +2% | 0 | 0 | — |
case-19 | pass→pass | 12,747 | 6,551 | -49% | 1 | 1 | 0% | 2,205 | 3,006 | +36% | 0 | 0 | — |
case-20 | pass→pass | 18,902 | 18,477 | -2% | 1 | 1 | 0% | 3,730 | 5,694 | +53% | 0 | 0 | — |
case-21 | pass→pass | 13,685 | 11,372 | -17% | 1 | 1 | 0% | 2,924 | 4,333 | +48% | 0 | 0 | — |
case-22 | pass→pass | 13,718 | 16,102 | +17% | 1 | 1 | 0% | 2,802 | 5,352 | +91% | 0 | 0 | — |
case-23 | fail→pass | 16,004 | 5,545 | -65% | 1 | 1 | 0% | 2,808 | 2,861 | +2% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +22 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.