# lifecore\_ros2: composing reusable ROS 2 Python lifecycle components

**URL:** <https://discourse.openrobotics.org/t/lifecore-ros2-composing-reusable-ros-2-python-lifecycle-components/54592>\
**Category:** Projects\
**Tags:** ros2, lifecycle, python, jazzy\
**Created:** [May 7, 2026, 1:02pm UTC](https://discourse.openrobotics.org/t/lifecore-ros2-composing-reusable-ros-2-python-lifecycle-components/54592 "2026-05-07T13:02:15Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![apajon](https://sea2.discourse-cdn.com/flex022/user_avatar/discourse.openrobotics.org/apajon/32/33728_2.png) [@apajon](https://discourse.openrobotics.org/u/apajon)\
**Post date:** [May 7, 2026, 1:02pm UTC](https://discourse.openrobotics.org/t/lifecore-ros2-composing-reusable-ros-2-python-lifecycle-components/54592/1 "2026-05-07T13:02:15Z")

</div>

Hi ROS community,

I would like to share a small ROS 2 project I have been building around lifecycle nodes in Python:

- Core library: [GitHub - apajon/lifecore\_ros2: lifecore\_ros2 is a modular ROS 2 framework designed to structure robotic applications around lifecycle-aware components. It provides a clean separation between nodes and components, enforces strict lifecycle management, and enables scalable, maintainable architectures for complex robotic systems. · GitHub](https://github.com/apajon/lifecore_ros2)
- Examples: [GitHub - apajon/lifecore\_ros2\_examples · GitHub](https://github.com/apajon/lifecore_ros2_examples)

The project is called `lifecore_ros2`.

It is an experimental Python library for ROS 2 Jazzy that helps structure lifecycle nodes as a set of explicit, reusable, lifecycle-aware components.

## Why I built this

ROS 2 lifecycle nodes are very useful when a node needs a clear managed state:

```plaintext
unconfigured -> inactive -> active -> finalized

```

In practice, though, when building Python nodes with `rclpy`, I often ended up with one large lifecycle node class containing everything:

```plaintext
SensorWatchdogLifecycleNode
├── parameters
├── publishers
├── subscribers
├── timers
├── service clients
├── lifecycle callbacks
└── runtime logic

```

This works, but as the node grows, the lifecycle logic and the application logic can become tightly mixed.

The recurring questions are usually:

- where should this publisher be created?
- should this timer exist while inactive?
- should this subscription callback run while inactive?
- where should cleanup happen?
- how do I reuse this piece of lifecycle-aware logic in another node?

`lifecore_ros2` tries to make those boundaries explicit.

## The idea

Instead of putting all lifecycle behavior directly into one large node class, the node owns a set of components.

```plaintext
LifecycleComponentNode
├── SensorSubscriberComponent
├── StatusPublisherComponent
├── WatchdogTimerComponent
└── DiagnosticsComponent

```

The node still owns the ROS 2 lifecycle.

Components do not introduce a second state machine. They simply receive lifecycle hooks from the node:

```plaintext
configure -> create ROS resources
activate -> enable runtime behavior
deactivate -> stop/gate runtime behavior
cleanup -> release ROS resources

```

So the goal is not to replace ROS 2 lifecycle nodes.

The goal is to make lifecycle nodes easier to compose, test, reason about, and reuse across several nodes when they share similar ROS interfaces.

## Reusable lifecycle-aware building blocks

Another motivation behind this project is reuse.

In many ROS 2 projects, the same topic-level logic appears in several nodes:

- subscribing to the same sensor topic
- publishing the same command or status topic
- exposing the same diagnostic publisher
- wrapping the same service client
- applying the same lifecycle guard around callbacks

Without a component boundary, this logic often ends up being copied from one node class to another.

`lifecore_ros2` is designed to make those pieces reusable and easy to move between nodes.

For example, instead of rewriting the same subscription setup in several lifecycle nodes, you can create a component once:

```python
class BatteryStateSubscriberComponent(LifecycleSubscriberComponent):
    ...

```

Then reuse it wherever it is needed:

```python
node.add_component(BatteryStateSubscriberComponent(...))

```

The same idea applies to publishers, timers, service clients, and small domain-specific pieces of node behavior.

The goal is not only to make one node cleaner.

The goal is to make lifecycle-aware ROS 2 logic portable:

```plaintext
write once -> import in another node -> attach to lifecycle -> reuse

```

This is useful when several nodes share the same communication contracts but have different responsibilities.

## What the library provides

The core package currently includes:

- `LifecycleComponentNode`
- `LifecycleComponent`
- lifecycle-aware publisher components
- lifecycle-aware subscriber components
- lifecycle-aware timer components
- lifecycle-aware service server/client components
- a `when_active` guard for callbacks
- typed lifecycle/composition errors
- test helpers for lifecycle-oriented unit tests

The pattern is intentionally small.

It is not:

- a replacement for native ROS 2 lifecycle semantics
- a second lifecycle state machine
- a behavior tree system
- a component container replacement
- a launch/orchestration framework
- a plugin framework

It is just a composition layer inside a Python lifecycle node.

## Example: sensor watchdog

The examples repository contains a direct comparison of the same node implemented three ways:

1. plain ROS 2 node
2. classic ROS 2 lifecycle node
3. `lifecore_ros2` component-based lifecycle node

The example is a small sensor watchdog:

```plaintext
/sensor/value -> watchdog node -> /sensor/status

```

The watchdog receives sensor values, checks whether the latest value is fresh, and publishes a status.

The point of the example is not that the node is complex.

The point is that the lifecycle behavior becomes visible:

- with a plain ROS 2 node, everything starts immediately
- with a classic lifecycle node, the lifecycle exists, but guards and resource handling are manual
- with `lifecore_ros2`, publishers, subscribers, timers, and runtime behavior are grouped into explicit lifecycle-aware components

## Quick start

Core library:

```bash
git clone https://github.com/apajon/lifecore_ros2.git
cd lifecore_ros2

source /opt/ros/jazzy/setup.bash
uv sync --extra dev

uv run python examples/minimal_node.py

```

In another terminal:

```bash
source /opt/ros/jazzy/setup.bash

ros2 lifecycle set /minimal_lifecore_node configure
ros2 lifecycle set /minimal_lifecore_node activate

```

Examples repository:

```bash
git clone https://github.com/apajon/lifecore_ros2_examples.git
cd lifecore_ros2_examples

source /opt/ros/jazzy/setup.bash
uv sync --dev

```

Run the fake sensor publisher:

```bash
uv run python examples/lifecycle_comparison/sensor_value_publisher_node.py

```

Run the `lifecore_ros2` watchdog:

```bash
uv run python examples/lifecycle_comparison/lifecore_ros2/sensor_watchdog_lifecore_node.py

```

Then activate it:

```bash
ros2 lifecycle set /sensor_watchdog_lifecore configure
ros2 lifecycle set /sensor_watchdog_lifecore activate
ros2 topic echo /sensor/status

```

## Current status

The project currently targets:

- ROS 2 Jazzy
- Python 3.12+
- `rclpy`
- `uv` for local development

The API is still in the `0.x` series, so I consider it experimental.

I am sharing it now because I would like feedback before freezing too many design decisions.

## Feedback wanted

I would be very interested in feedback from people using lifecycle nodes in real ROS 2 Python projects.

In particular:

- does this component-based lifecycle model make sense to you?
- are lifecycle-aware publishers, subscribers, timers, and services useful primitives?
- would reusable topic-level components be useful in your ROS 2 codebase?
- do you already copy/paste publisher, subscriber, timer, or service client logic between nodes?
- is the ownership model clear?
- does this solve a real pain point, or does it add too much abstraction?
- where does it overlap too much with existing ROS 2 patterns?
- what would you expect before using something like this in a production robot?

Any criticism is welcome.

The goal is to keep the library small and aligned with ROS 2 lifecycle semantics, not to build a new framework on top of ROS 2.

Thanks for reading.
