ros2-lifecycle

ROS2 managed lifecycle node patterns for deterministic startup, activation, shutdown, reconfiguration, and recovery. Use when implementing LifecycleNode behavior, on_configure, on_activate, on_deactiv

SKILL.md

GitHub

ROS2 Lifecycle Nodes

When to Use This Skill

  • Bringing up a production robot where startup order and recovery matter
  • A node needs to load resources (models, calibration, hardware connections) before going live
  • Reconfiguring a running node without restarting (deactivate → cleanup → configure → activate)
  • Implementing a supervisor that orchestrates other nodes via lifecycle_msgs
  • Debugging "node started but is silently doing nothing" — likely stuck in unconfigured

If your node only needs __init__ and main, you don't need a lifecycle node. Reach for it when the transition between not-running and fully-operational is itself a thing your system needs to control.

The State Machine

                +-----------+   create()
                |  UNKNOWN  | -----------+
                +-----------+            v
                                 +--------------+
                                 | UNCONFIGURED |<------------+
                                 +--------------+             |
                                       |                      |
                                  configure()             cleanup()
                                       v                      |
                                 +-----------+                |
                                 |  INACTIVE | ---------------+
                                 +-----------+
                                       |
                                  activate()    deactivate()
                                       v             ^
                                 +-----------+       |
                                 |  ACTIVE   |-------+
                                 +-----------+
                                       |
                                   shutdown()
                                       v
                                 +-----------+
                                 |  FINALIZED|
                                 +-----------+
State Meaning
unconfigured Just created. No resources held.
inactive Configured (resources allocated), but not yet doing work.
active Doing its job — publishers publish, subscribers process.
finalized Shutting down. Terminal.

The Discipline: What Happens In Each Transition

Callback Do Don't
on_configure Allocate memory, load models, open hardware, create pubs/subs Start work, publish, subscribe to topics that will trigger work
on_activate Activate publishers, start timers, enable subscriptions Allocate resources (do that in configure)
on_deactivate Stop timers, disable subscriptions, deactivate publishers Free resources (you may activate again later)
on_cleanup Free everything on_configure allocated; back to unconfigured Leave resources held
on_shutdown Final cleanup before destruction Anything that depends on others — they may already be shutting down
on_error Try to recover; otherwise return FAILURE Hide errors; log + return appropriate state

Returning FAILURE from a callback aborts the transition; returning ERROR triggers on_error.

Python Implementation

import rclpy
from rclpy.lifecycle import Node as LifecycleNode
from rclpy.lifecycle import TransitionCallbackReturn
from rclpy.lifecycle import LifecyclePublisher
from sensor_msgs.msg import Image
from robot_interfaces.msg import DetectionArray


class ManagedPerception(LifecycleNode):
    def __init__(self):
        super().__init__('managed_perception')
        self.declare_parameter('model_path', '')
        self._model = None
        self._det_pub: LifecyclePublisher = None
        self._image_sub = None

    def on_configure(self, state) -> TransitionCallbackReturn:
        try:
            from my_pkg.detector import load_model
            self._model = load_model(self.get_parameter('model_path').value)
            self._det_pub = self.create_lifecycle_publisher(
                DetectionArray, 'detections', 10)
            self.get_logger().info('configured')
            return TransitionCallbackReturn.SUCCESS
        except Exception as e:
            self.get_logger().error(f'configure failed: {e}')
            return TransitionCallbackReturn.FAILURE

    def on_activate(self, state) -> TransitionCallbackReturn:
        self._image_sub = self.create_subscription(
            Image, 'camera/image_raw', self._on_image, 1)
        # LifecyclePublisher requires explicit activation
        super().on_activate(state)
        self.get_logger().info('active')
        return TransitionCallbackReturn.SUCCESS

    def on_deactivate(self, state) -> TransitionCallbackReturn:
        self.destroy_subscription(self._image_sub)
        self._image_sub = None
        super().on_deactivate(state)
        self.get_logger().info('inactive')
        return TransitionCallbackReturn.SUCCESS

    def on_cleanup(self, state) -> TransitionCallbackReturn:
        self.destroy_publisher(self._det_pub)
        self._det_pub = None
        self._model = None
        return TransitionCallbackReturn.SUCCESS

    def on_shutdown(self, state) -> TransitionCallbackReturn:
        return TransitionCallbackReturn.SUCCESS

    def on_error(self, state) -> TransitionCallbackReturn:
        self.get_logger().error(f'error in state {state.label}')
        return TransitionCallbackReturn.SUCCESS

    def _on_image(self, msg):
        if self._det_pub is None:
            return
        # Publishers ignore messages while inactive — but explicit guard is clearer
        detections = self._model.detect(msg)
        self._det_pub.publish(detections)


def main():
    rclpy.init()
    node = ManagedPerception()
    try:
        rclpy.spin(node)
    finally:
        node.destroy_node()
        rclpy.shutdown()

C++ Implementation

#include <rclcpp/rclcpp.hpp>
#include <rclcpp_lifecycle/lifecycle_node.hpp>
#include <rclcpp_lifecycle/lifecycle_publisher.hpp>
#include <std_msgs/msg/string.hpp>

using CallbackReturn =
    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn;

class ManagedNode : public rclcpp_lifecycle::LifecycleNode {
public:
  explicit ManagedNode(const rclcpp::NodeOptions& opts = rclcpp::NodeOptions())
      : LifecycleNode("managed_node", opts) {}

  CallbackReturn on_configure(const rclcpp_lifecycle::State&) override {
    RCLCPP_INFO(get_logger(), "configuring");
    pub_ = create_publisher<std_msgs::msg::String>("topic", 10);
    return CallbackReturn::SUCCESS;
  }

  CallbackReturn on_activate(const rclcpp_lifecycle::State& state) override {
    LifecycleNode::on_activate(state);   // activates lifecycle publishers
    RCLCPP_INFO(get_logger(), "activating");
    return CallbackReturn::SUCCESS;
  }

  CallbackReturn on_deactivate(const rclcpp_lifecycle::State& state) override {
    LifecycleNode::on_deactivate(state);
    RCLCPP_INFO(get_logger(), "deactivating");
    return CallbackReturn::SUCCESS;
  }

  CallbackReturn on_cleanup(const rclcpp_lifecycle::State&) override {
    pub_.reset();
    return CallbackReturn::SUCCESS;
  }

  CallbackReturn on_shutdown(const rclcpp_lifecycle::State&) override {
    return CallbackReturn::SUCCESS;
  }

private:
  rclcpp_lifecycle::LifecyclePublisher<std_msgs::msg::String>::SharedPtr pub_;
};

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = std::make_shared<ManagedNode>();
  rclcpp::spin(node->get_node_base_interface());
  rclcpp::shutdown();
  return 0;
}

Driving Lifecycle Transitions from the CLI

ros2 lifecycle list /managed_perception              # available transitions
ros2 lifecycle get  /managed_perception              # current state
ros2 lifecycle set  /managed_perception configure
ros2 lifecycle set  /managed_perception activate
ros2 lifecycle set  /managed_perception deactivate
ros2 lifecycle set  /managed_perception shutdown

Lifecycle Client (Programmatic Supervisor)

#include <lifecycle_msgs/srv/change_state.hpp>
#include <lifecycle_msgs/msg/transition.hpp>

class LifecycleClient {
public:
  LifecycleClient(rclcpp::Node::SharedPtr node, const std::string& target)
      : node_(node) {
    client_ = node_->create_client<lifecycle_msgs::srv::ChangeState>(
        target + "/change_state");
  }

  bool change(uint8_t transition_id) {
    if (!client_->wait_for_service(std::chrono::seconds(2))) return false;
    auto req = std::make_shared<lifecycle_msgs::srv::ChangeState::Request>();
    req->transition.id = transition_id;
    auto future = client_->async_send_request(req);
    if (rclcpp::spin_until_future_complete(node_, future) !=
        rclcpp::FutureReturnCode::SUCCESS) return false;
    return future.get()->success;
  }

private:
  rclcpp::Node::SharedPtr node_;
  rclcpp::Client<lifecycle_msgs::srv::ChangeState>::SharedPtr client_;
};

// usage:
client.change(lifecycle_msgs::msg::Transition::TRANSITION_CONFIGURE);
client.change(lifecycle_msgs::msg::Transition::TRANSITION_ACTIVATE);

Auto-Configure / Auto-Activate from Launch

See ros2-launch-config for the OnStateTransition event handler pattern that automatically advances a lifecycle node on startup.

Common Pitfalls

Pitfall What goes wrong Fix
Allocating in on_activate, freeing in on_deactivate Reactivating after deactivate re-allocates; defeats the configure pattern Allocate in on_configure, free in on_cleanup
Forgetting super().on_activate(state) in Python (or LifecycleNode::on_activate(state) in C++) Lifecycle publishers stay inactive; publish() is silently dropped Always call the base class transition before doing your work
Returning SUCCESS from a transition that actually failed Node ends up in wrong state; downstream supervisors think it's healthy Return FAILURE; let the supervisor decide whether to retry or give up
Doing real work in on_configure First reconfigure is slow; resource leaks if configure fails partway Configure = setup only; do work after on_activate
Subscribing in on_configure Subscriptions fire while inactive — undefined behavior Create subs in on_activate, destroy in on_deactivate
No on_error handler One transient failure tears the node down Implement on_error and return SUCCESS to fall back to unconfigured for retry

Related Skills

  • ros2-node-creation — base node patterns; lifecycle adds the state machine
  • ros2-launch-config — auto-configure/activate via launch event handlers
  • ros2-service-action — supervisors expose state changes as services
  • robot-bringup — system-level startup ordering with systemd