This is just a quick example of how to make a custom ROS 2 launch substitution available to XML launch files.

TLDR: Add the @expose_substitution annotation to the class, implement the Class.parse() method.

You should already have a python class implementing a substitution, which might look something like this:

class RobotParameterFile(launch.Substitution):

    def __init__(self,         
        config_package: launch.SomeSubstitutionsType,
        robot_name: launch.SomeSubstitutionsType,
        param_file_path: launch.SomeSubstitutionsType,
    ) -> None:
        super().__init__()
        # ...

    def perform(self, context: launch.LaunchContext) -> Text:
        # ...
        return "test"

The goal is to use it from an XML launch file, like such:

<?xml version="1.0" encoding="UTF-8"?>
<launch version="0.1.1">
    <arg name="arg_a" />
    <arg name="arg_b" />

    <node pkg="diagnostic_remote_logging" exec="influx" name="diagnostics_influxdb_bridge">
        <param from="$(robot-param-file $(var arg_a) $(var arg_b) diagnostics.yaml)" />
    </node>
</launch>

To enable this, it is necessary to add the @expose_substitution("robot-param-file") annotation to the class, where the argument is the desired name/command in the XML launch file, as well as the parse(cls, data: Sequence[launch.SomeSubstitutionsType]) class method. The purpose of the parse method is to convert the list of arguments inside the $(...) into constructor arguments of the class:

from launch.frontend import expose_substitution

@expose_substitution("robot-param-file")
class RobotParameterFile(launch.Substitution):

    def __init__(self,         
        config_package: launch.SomeSubstitutionsType,
        robot_name: launch.SomeSubstitutionsType,
        param_file_path: launch.SomeSubstitutionsType,
    ) -> None:
        super().__init__()
        # ...

    def perform(self, context: launch.LaunchContext) -> Text:
        # ...
        return "test"

    @classmethod
    def parse(cls, data: Sequence[launch.SomeSubstitutionsType]):
        """Parse a RobotParameterFile substitution."""
        if not data or len(data) != 3:
            raise AttributeError(
                "robot-param-file substitution expects 3 arguments: config package, robot name, param file"
            )
        kwargs = {
            "config_package": data[0],
            "robot_name": data[1],
            "param_file_path": data[2],
        }
        return cls, kwargs

As shown, this also works when the arguments in data are themselves substitutions (which must be accounted for in __init__).

Is is then necessary to register the package containing this substitution as providing launch extensions, which is done by adding the following to setup.cfg (replace package_name with the actual package name):

[options.entry_points]
launch.frontend.launch_extension =
    package_name = package_name

I was building this package with cmake, so i also had to make sure to install setup.cfg:

ament_python_install_package(package_name
  PACKAGE_DIR package_name
  SETUP_CFG setup.cfg
)