ROS 2 launch files explained: nodes, parameters and event handlers
What a launch file is for
A ROS 2 system is rarely one node. It is a driver, a state estimator, a planner, a controller, a bridge and a visualizer, spread over several processes and often several machines. The documentation puts the purpose plainly: “The launch system in ROS 2 is meant to automate the running of many nodes with a single command” (About launch).
A launch file describes the configuration of that system: what programs to run, where, which arguments to pass, and the ROS conventions such as namespaces, remappings and parameter files. The launch system then executes the description and monitors the processes it started (Creating a launch file).
If the framework itself is new to you, start with What is ROS 2?.
Two packages behind one command
There are two moving parts. launch is the framework that is not ROS specific: launch descriptions, actions, substitutions, conditions and event handlers. launch_ros is the ROS aware layer built on top of it, and the documentation states that the package providing this framework is launch_ros, which uses the non-ROS-specific launch framework underneath. The ros2 launch verb loads the file, builds the launch description and hands it to the launch service (launch architecture).
That split explains the API. Anything about ROS concepts, such as a node’s package and executable, a namespace or a remapping, comes from launch_ros. Anything about control flow, such as including another description, gating an action on a condition, or reacting to an event, comes from launch.
XML, YAML or Python
All three formats describe the same actions, and the docs say they are functionally equivalent. They differ in how much logic you can express:
- XML and YAML are declarative. They suit bring-up files that a hardware team edits: which nodes, which parameters, which namespaces.
- Python is a full program. Every launch file defines
generate_launch_description()and returns alaunch.LaunchDescription. It is the format to use when the description depends on computation, such as scanning for devices or building a node list in a loop.
Naming is a convention, not a requirement: _launch.xml and _launch.yaml are recommended suffixes, while a Python launch file needs to end in launch.py to be recognized and autocompleted by ros2 launch (Integrating launch files into packages).
Formats mix freely. The official different formats example includes a Python file, an XML file and a YAML file from a single XML launch file (different formats):
<include file="$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.py" />
<group>
<push_ros_namespace namespace="$(var chatter_xml_ns)" />
<include file="$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.xml" />
</group>
Running and inspecting a launch file
ros2 launch <package_name> <launch_file_name>
ros2 launch <path_to_launch_file>
ros2 launch launch_tutorial example_substitutions_launch.xml turtlesim_ns:='turtlesim3' use_provided_red:='True' new_background_r:=200
Arguments use key:=value, and the CLI documents that duplicates follow last one wins (ros2launch command). The command carries more inspection flags than most people use:
-s,--show-args, prints the arguments that may be given to the launch file with their defaults.-p,--print(alias--print-description), prints the launch description to the console without launching it.-d,--debug, puts the launch system in debug mode with more verbose output.--launch-prefix, prefixes every executable, for example--launch-prefix 'xterm -e gdb -ex run --args'to drop a node into a debugger.-a,--show-all-subprocesses-output, overrides each subprocess output configuration.-f,--log-file-name, names the log file, defaultlaunch.-n,--noninteractive, runs with no terminal attached and sends SIGINT to processes on shutdown. The CLI source notes this is the default when stdin is not a terminal, which matters in CI and containers.
For a launch file to be found by package name, it has to be installed. Python packages add glob('launch/*') to the data_files entry of setup.py; C++ packages add install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}/) to CMakeLists.txt before ament_package(). The documentation also recommends an exec_depend on ros2launch in package.xml, which keeps the ros2 launch command available and ensures all launch file formats are recognized.
Actions, includes and conditions
A launch description is a list of actions. The core set includes ExecuteProcess, IncludeLaunchDescription, DeclareLaunchArgument, SetLaunchConfiguration, GroupAction, TimerAction, RegisterEventHandler, EmitEvent, LogInfo, RaiseError and OpaqueFunction, the last of which runs a Python function and treats its return value as more launch entities, which is how descriptions get built dynamically at run time (launch architecture).
An include can pass values down. In XML, <let> sets a launch configuration that the included file sees, and the docs note that older launch versions needed <arg> instead:
<launch>
<let name="background_r" value="200" />
<include file="$(find-pkg-share launch_tutorial)/launch/example_substitutions_launch.xml">
<let name="turtlesim_ns" value="turtlesim2" />
<let name="new_background_r" value="$(var background_r)" />
</include>
</launch>
Any action can carry a condition, if or unless in XML and YAML, IfCondition or UnlessCondition in Python. That is the clean way to express one robot with two configurations, for example simulation versus hardware, without forking the file:
<timer period="2.0">
<executable cmd="ros2 param set /turtlesim background_r $(var new_background_r)"
if="$(eval '$(var new_background_r) == 200 and $(var use_provided_red)')" />
</timer>
The same predicate can be written without a Python expression, using the boolean substitutions, which is easier to read for people who do not write Python:
<executable cmd="ros2 param set /turtlesim background_r $(var new_background_r)"
if="$(and $(equals $(var new_background_r) 200) $(var use_provided_red))" />
Substitutions resolve values at launch time
A substitution is evaluated when the launch description executes, not when the file is written. That is what makes a launch file reusable across robots and machines (Using substitutions). The common ones:
$(var name)for a launch configuration,LaunchConfiguration('name')in Python.$(eval 'python expression')andPythonExpressionfor arbitrary evaluation.$(find-pkg-share package)andFindPackageSharefor a package’s share directory, usually combined withPathJoinSubstitution.FindExecutable, which locates the full path to an executable on the PATH, and theEnvironmentVariablesubstitution for environment values.- Dedicated boolean substitutions:
$(equals A B),$(not-equals A B),$(and A B),$(or A B),$(any ...)and$(all ...).
One detail that causes quiet bugs: comparison is performed on the string representation of each argument, so a value passed on the command line is compared as text, not as a number or a boolean.
Nodes, parameters, remappings, namespaces
A ROS node in a launch file is a package, an executable, a name and a namespace. The official example shows two ways of passing arguments, and the difference matters:
<node pkg="turtlesim" exec="turtlesim_node" name="sim" namespace="turtlesim1"
args="--ros-args --log-level info" />
<node pkg="turtlesim" exec="turtlesim_node" name="sim" namespace="turtlesim2"
ros_args="--log-level warn">
<param name="background_r" value="$(var background_r)" />
<param name="background_g" value="$(var background_g)" />
</node>
<node pkg="turtlesim" exec="mimic" name="mimic">
<remap from="/input/pose" to="/turtlesim1/turtle1/pose" />
</node>
args passes everything to the process, so ROS specific flags need the --ros-args prefix, while ros_args takes only ROS arguments. Use args when mixing ROS and non-ROS arguments, or ros_args for cleaner syntax.
Instead of inline parameters, a node can load a parameter file, and the docs are explicit that this YAML file is a node configuration file, not another launch file:
<node pkg="turtlesim" exec="turtlesim_node" namespace="turtlesim2" name="sim">
<param from="$(find-pkg-share launch_tutorial)/config/turtlesim.yaml" />
</node>
The file is keyed by fully qualified node name and carries a ros__parameters block, for example /turtlesim2/sim: ros__parameters: background_r: 150. Wildcards extend the same block to many nodes: /** assigns the parameters to every node regardless of name or namespace (Managing large projects).
Namespaces are normally set per node, but a group can set one for everything inside it. PushROSNamespace inherits to nested nodes, with one hard rule: it has to be the first action in the list for the following actions to inherit the namespace.
What a launch file does not do is configure Quality of Service. QoS lives in the node’s own code or in QoS override files, and a mismatch there is the classic reason a topic shows no data even though the launch file is correct. See ROS 2 Quality of Service policies explained and, for the interface types a launch file wires together, ROS 2 topics vs services vs actions.
Composable nodes
Composition puts several nodes in one process, which removes serialization and transport cost on the hot path. Launch is the usual way to bring a component container up. In Python it is ComposableNodeContainer holding ComposableNode descriptions, and in XML it is a node_container with composable_node children:
<node_container pkg="rclcpp_components" exec="component_container" name="image_container" namespace="">
<composable_node pkg="image_tools" plugin="image_tools::Cam2Image" name="cam2image">
<remap from="/image" to="/burgerimage" />
<param name="width" value="320" />
<extra_arg name="use_intra_process_comms" value="true" />
</composable_node>
</node_container>
use_intra_process_comms is passed as an extra argument rather than a parameter, which trips people who copy the block and expect a <param>. Where those components execute, and how many threads they run on, is executor territory: ROS 2 executors and callback groups explained.
Event handlers and lifecycle
Launch does not only start processes, it watches them. Handlers are registered for events: OnProcessStart, OnProcessIO, OnExecutionComplete, OnProcessExit and OnShutdown are the ones covered by the official event handler tutorial. The practical pattern is an OnProcessExit handler for the main node that emits a Shutdown event, so closing the robot’s main window or killing its main process takes the whole launch description down with it. OnShutdown then logs why (Using event handlers).
launch_ros extends this for managed nodes. It ships LifecycleNode and LifecycleTransition actions and the lifecycle events ChangeState and StateTransition, so a launch file can drive a node through configure, activate and deactivate in a defined order (launch_ros actions). The same module list shows what the ROS layer adds beyond a plain process launch: SetParameter, SetParametersFromFile, SetRemap, SetUseSimTime, SetROSLogDir, PushROSNamespace, ComposableNodeContainer and LoadComposableNodes.
Structuring a large project
The official guidance for multi-component systems is to keep the top level thin. The docs state the design tip directly: top-level launch files should be short, consist of includes to other files corresponding to subcomponents of the application, and commonly changed parameters.
In practice that means one file per subsystem, such as perception, navigation or the arm, one top level file per configuration, and launch arguments plus parameter files for everything that differs between robots or between simulation and hardware. Swapping one subsystem then means changing one include, not editing a monolithic file.
Pitfalls worth knowing before you debug
Closing a node does not stop the launch file. Processes are monitored, but the description keeps running until an action or handler shuts it down, which is why the OnProcessExit plus Shutdown pattern exists.
Launch files inside a package are invisible to ros2 launch <package_name> until they are installed, so a file missing from the setup.py data files or the CMakeLists.txt install rule will not be found by package name.
Argument values are strings. Boolean substitutions compare text, so declaring a default of False and comparing against True is a string comparison, not a boolean one.
Finally, ros2 launch printed output is not the only diagnostic. --print shows what launch thinks the description contains, --show-args shows what can be overridden, and --debug raises verbosity.
When launch is the wrong tool
A launch file earns its keep when a system has multiple processes with shared conventions. For a single node, or a control loop that is one process, it adds a layer of indirection with no payoff.
For long-running deployments, keep the distinction clear: launch is a bring-up and development tool that describes and supervises a system of processes, and teams typically layer it under a service manager or a container rather than treating it as the supervisor. The strongest use of launch files is a bring-up a new engineer can run with one command and override with a handful of arguments. The weakest, on this reading of the documentation, is a thicket of nested includes that only the author can follow.
Sources
- ROS 2 docs: About launch
- ROS 2 docs: Creating a launch file
- ROS 2 docs: Integrating launch files into packages
- ROS 2 docs: Using substitutions
- ROS 2 docs: Using event handlers
- ROS 2 docs: Managing large projects
- ROS 2 docs: Using XML, YAML and Python for launch files
- ROS 2 docs: Composition
- design.ros2.org: roslaunch design
- launch architecture
- launch_ros actions
- ros2launch command line source
Frequently asked questions
- What is the difference between the launch and launch_ros packages?
- launch is the non-ROS-specific framework that provides launch descriptions, actions, substitutions, conditions and event handlers. launch_ros builds on it with ROS aware actions such as Node, LifecycleNode, ComposableNodeContainer, PushROSNamespace, SetParameter, SetRemap and SetUseSimTime, plus the substitutions that resolve package share directories and executables. ros2 launch is the command line entry point that loads a launch file and hands it to the launch service.
- Can a launch file include another launch file written in a different format?
- Yes. The formats are functionally equivalent and can be mixed. The official different formats guide includes Python, XML and YAML launch files from one XML launch file, and the launch_ros examples wrap each include in a group with push_ros_namespace so the included nodes land in their own namespace.
- How do I pass arguments to a launch file from the command line?
- Use key:=value after the package and file name, for example ros2 launch launch_tutorial example_substitutions_launch.py turtlesim_ns:=turtlesim3. Arguments have to be declared first, with an arg tag in XML or YAML or DeclareLaunchArgument in Python, otherwise launch reports that the argument is not declared. Duplicate arguments follow last one wins, and --show-args prints every argument with its default.
- Why does ros2 launch say my launch file does not exist even though it is in the package?
- Launch files inside a package are only visible after they are installed into the package share directory. Python packages need the launch directory listed in the data_files entry of setup.py, C++ packages need install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}/) in CMakeLists.txt, and the package should declare an exec_depend on ros2launch so the ros2 launch verb and all launch file formats are available.
- Does closing a node stop the launch file?
- Not by default. The launch system starts processes and monitors their state, but the launch description keeps running unless an action or an event handler shuts it down. The tutorial example registers an OnProcessExit handler for the turtlesim window and emits a Shutdown event from it, which is the usual pattern for stopping everything when the main process dies.
Related reading
ROS 2 lifecycle nodes explained: states, transitions and launch
ROS 2 lifecycle nodes add a managed state machine to drivers and servers: configure, activate, deactivate and clean up in a deliberate order.
ROS 2 topics vs services vs actions
Topics stream continuous data, services answer a request, actions run long tasks with feedback. How the three ROS 2 interfaces work, and how to choose.
Fleet APIs, in-orbit assembly and zero-copy rosidl
Serve opens its delivery fleet to any developer, Caltech assembles a truss in orbit, and ROS 2 debates moving zero-copy into rosidl itself.