diff --git a/ansible/baremetal-compute-register.yml b/ansible/baremetal-compute-register.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f233e09baf36da27d4d96fd71fc69132cb09a509
--- /dev/null
+++ b/ansible/baremetal-compute-register.yml
@@ -0,0 +1,78 @@
+---
+
+- name: Register baremetal compute nodes
+  hosts: controllers[0]
+  vars:
+    venv: "{{ virtualenv_path }}/openstack-cli"
+  tasks:
+    - name: Set up openstack cli virtualenv
+      pip:
+        virtualenv: "{{ venv }}"
+        name:
+          - python-openstackclient
+          - python-ironicclient
+        state: latest
+        virtualenv_command: "python3.{{ ansible_facts.python.version.minor }} -m venv"
+        extra_args: "{% if pip_upper_constraints_file %}-c {{ pip_upper_constraints_file }}{% endif %}"
+
+- name: Ensure baremetal compute nodes are registered in ironic
+  hosts: baremetal-compute
+  gather_facts: false
+  tags:
+    - baremetal
+  vars:
+    venv: "{{ virtualenv_path }}/openstack-cli"
+    controller_host: "{{ groups['controllers'][0] }}"
+  tasks:
+    - name: Check Ironic variables are defined
+      ansible.builtin.assert:
+        that:
+          - ironic_driver is defined
+          - ironic_driver_info is defined
+          - ironic_properties is defined
+          - ironic_resource_class is defined
+        fail_msg: One or more Ironic variables are undefined.
+
+    - block:
+      - name: Show baremetal node
+        ansible.builtin.command:
+          cmd: "{{ venv }}/bin/openstack baremetal node show {{ inventory_hostname }}"
+        register: node_show
+        failed_when:
+          - '"HTTP 404" not in node_show.stderr'
+          - node_show.rc != 0
+        changed_when: false
+
+      # NOTE: The openstack.cloud.baremetal_node module cannot be used in this
+      # script due to requiring a MAC address pre-defined, instead, this should
+      # be discovered by inpsection following this script.
+      #
+      # NOTE: IPMI address must be passed with Redfish address to ensure existing
+      # Ironic nodes match with new nodes during inspection.
+      - name: Create baremetal nodes
+        ansible.builtin.shell:
+          cmd: |
+            {{ venv }}/bin/openstack baremetal node create \
+            --name {{ inventory_hostname }} \
+            --driver {{ ironic_driver }} \
+            {% for key, value in ironic_driver_info.items() %}
+            --driver-info {{ key }}={{ value }} \
+            {% endfor %}
+            {% for key, value in ironic_properties.items() %}
+            --property {{ key }}={{ value }} \
+            {% endfor %}
+            --resource-class {{ ironic_resource_class }}
+        when:
+          - node_show.rc != 0
+
+      - name: Manage baremetal nodes
+        ansible.builtin.command:
+          cmd: "{{ venv }}/bin/openstack baremetal node manage {{ inventory_hostname }} --wait"
+        when:
+          - node_show.rc != 0
+      delegate_to: "{{ controller_host }}"
+      vars:
+        # NOTE: Without this, the controller's ansible_host variable will not
+        # be respected when using delegate_to.
+        ansible_host: "{{ hostvars[controller_host].ansible_host | default(controller_host) }}"
+      environment: "{{ openstack_auth_env }}"
diff --git a/doc/source/administration/bare-metal.rst b/doc/source/administration/bare-metal.rst
index fe8490064121af30899e09de749d0465ec073cf7..40cf52e3c546f9e02a4fc4cd94cc82318cafdc6b 100644
--- a/doc/source/administration/bare-metal.rst
+++ b/doc/source/administration/bare-metal.rst
@@ -13,6 +13,62 @@ By default these commands wait for the state transition to complete for each
 node. This behavior can be changed by overriding the variable
 ``baremetal_compute_wait`` via ``-e baremetal_compute_wait=False``
 
+Register
+--------
+
+This is an experimental workflow and acts as an alternative to enrolling nodes
+through inspection where nodes can be registered in Ironic via kayobe given these
+nodes are defined in the Kayobe inventory, an example hosts file for group r1 is below:
+
+.. code-block:: ini
+
+    [r1]
+    hv100 ipmi_address=1.2.3.4
+    ...
+
+    [baremetal-compute:children]
+    r1
+
+You should also define a group_vars file for this group containing the Ironic
+vars, this could be in ``etc/kayobe/inventory/group_vars/r1/ironic_vars`` or
+in the environment you are using.
+
+.. code-block:: yaml
+
+    ironic_driver: redfish
+
+    ironic_driver_info:
+        redfish_system_id: "{{ ironic_redfish_system_id }}"
+        redfish_address: "{{ ironic_redfish_address }}"
+        redfish_username: "{{ ironic_redfish_username }}"
+        redfish_password: "{{ ironic_redfish_password }}"
+        redfish_verify_ca: "{{ ironic_redfish_verify_ca }}"
+        ipmi_address: "{{ ipmi_address }}"
+
+    ironic_properties:
+        capabilities: "{{ ironic_capabilities }}"
+
+    ironic_resource_class: "example_resouce_class"
+    ironic_redfish_system_id: "/redfish/v1/Systems/System.Embedded.1"
+    ironic_redfish_verify_ca: "{{ inspector_rule_var_redfish_verify_ca }}"
+    ironic_redfish_address: "{{ ipmi_address }}"
+    ironic_redfish_username: "{{ inspector_redfish_username }}"
+    ironic_redfish_password: "{{ inspector_redfish_password }}"
+    ironic_capabilities: "boot_option:local,boot_mode:uefi"
+
+It's essential that the Ironic username and password match the BMC username
+and password for your nodes, if the username and password combination is
+not the same for the entire group you will need to adjust your configuration
+accordingly. The IPMI address should also match the BMC address for your node.
+
+Once this has been completed you can begin enrolling the Ironic nodes::
+
+    (kayobe) $ kayobe baremetal compute register
+
+Inspector is not used to discover nodes and no node inspection will take place on
+enrollment, nodes will automatically be placed into ``manageable`` state. To inspect,
+you should use ``kayobe baremetal compute inspect`` following enrollment.
+
 Manage
 ------
 
diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py
index 24c412722d8fc63690d5a52c6316fae5fccefd75..7b059b9b5344819c434d4a94fa9f533c94a311ee 100644
--- a/kayobe/cli/commands.py
+++ b/kayobe/cli/commands.py
@@ -1858,6 +1858,14 @@ class NetworkConnectivityCheck(KayobeAnsibleMixin, VaultMixin, Command):
         playbooks = _build_playbook_list("network-connectivity")
         self.run_kayobe_playbooks(parsed_args, playbooks)
 
+class BaremetalComputeRegister(KayobeAnsibleMixin, VaultMixin, Command):
+    """Register baremetal compute nodes in Ironic."""
+
+    def take_action(self, parsed_args):
+        self.app.LOG.debug("Register baremetal compute nodes in Ironic.")
+        playbooks = _build_playbook_list("baremetal-compute-register")
+        self.run_kayobe_playbooks(parsed_args, playbooks)
+
 
 class BaremetalComputeInspect(KayobeAnsibleMixin, VaultMixin, Command):
     """Perform hardware inspection on baremetal compute nodes."""
diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py
index d21227f11ea030d6723393352015d2f471d1faad..83739c678bb202f3fc6abc7eb7268e1230e9568c 100644
--- a/kayobe/tests/unit/cli/test_commands.py
+++ b/kayobe/tests/unit/cli/test_commands.py
@@ -2169,6 +2169,25 @@ class TestCase(unittest.TestCase):
         ]
         self.assertListEqual(expected_calls, mock_run.call_args_list)
 
+    @mock.patch.object(commands.KayobeAnsibleMixin,
+                       "run_kayobe_playbooks")
+    def test_baremetal_compute_register(self, mock_run):
+        command = commands.BaremetalComputeRegister(TestApp(), [])
+        parser = command.get_parser("test")
+        parsed_args = parser.parse_args([])
+        result = command.run(parsed_args)
+        self.assertEqual(0, result)
+        expected_calls = [
+            mock.call(
+                mock.ANY,
+                [
+                    utils.get_data_files_path(
+                        "ansible", "baremetal-compute-register.yml"),
+                ],
+            ),
+        ]
+        self.assertListEqual(expected_calls, mock_run.call_args_list)
+
     @mock.patch.object(commands.KayobeAnsibleMixin,
                        "run_kayobe_playbooks")
     def test_baremetal_compute_inspect(self, mock_run):
diff --git a/releasenotes/notes/baremetal-enroll-e01693210f95675c.yaml b/releasenotes/notes/baremetal-enroll-e01693210f95675c.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..88a3041468f2f95407f676e9520416137d06612c
--- /dev/null
+++ b/releasenotes/notes/baremetal-enroll-e01693210f95675c.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    This patch adds experimental functionallity to enroll baremetal nodes
+    into Ironic using Kayobe via a new playbook 'baremetal-compute-register.yml'
+    and adds 'kayobe baremetal compute register' into the Kayobe CLI.
diff --git a/setup.cfg b/setup.cfg
index e1a3ca85add2e2450fe1fd93e65050fb44218635..ce9dcb69ee085f02ea994037e4b9da53b654f1d6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -39,6 +39,7 @@ console_scripts=
     kayobe-vault-password-helper = kayobe.cmd.kayobe_vault_password_helper:main
 
 kayobe.cli=
+    baremetal_compute_register = kayobe.cli.commands:BaremetalComputeRegister
     baremetal_compute_inspect = kayobe.cli.commands:BaremetalComputeInspect
     baremetal_compute_introspection_data_save = kayobe.cli.commands:BaremetalComputeIntrospectionDataSave
     baremetal_compute_manage = kayobe.cli.commands:BaremetalComputeManage
@@ -106,6 +107,8 @@ kayobe.cli=
     infra_vm_host_package_update = kayobe.cli.commands:InfraVMHostPackageUpdate
     infra_vm_service_deploy = kayobe.cli.commands:InfraVMServiceDeploy
 
+kayobe.cli.baremetal_compute_register =
+    hooks = kayobe.cli.commands:HookDispatcher
 kayobe.cli.baremetal_compute_inspect =
     hooks = kayobe.cli.commands:HookDispatcher
 kayobe.cli.baremetal_compute_introspection_data_save =