diff --git a/lib/common/output_xml.c b/lib/common/output_xml.c
index 34c68219b5..0594444e2e 100644
--- a/lib/common/output_xml.c
+++ b/lib/common/output_xml.c
@@ -1,563 +1,602 @@
 /*
  * Copyright 2019-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <ctype.h>
 #include <stdarg.h>
 #include <stdlib.h>
 #include <stdio.h>
 #include <crm/crm.h>
 #include <glib.h>
 
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/output.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>    // pcmk__xml2fd
 
 static gboolean legacy_xml = FALSE;
 static gboolean simple_list = FALSE;
 
 GOptionEntry pcmk__xml_output_entries[] = {
     { "xml-legacy", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &legacy_xml,
       NULL,
       NULL },
     { "xml-simple-list", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &simple_list,
       NULL,
       NULL },
 
     { NULL }
 };
 
 typedef struct subst_s {
     const char *from;
     const char *to;
 } subst_t;
 
 static const subst_t substitutions[] = {
     { "Active Resources",
       PCMK_XE_RESOURCES, },
     { "Assignment Scores",
       PCMK_XE_ALLOCATIONS, },
     { "Assignment Scores and Utilization Information",
       PCMK_XE_ALLOCATIONS_UTILIZATIONS, },
     { "Cluster Summary",
       PCMK_XE_SUMMARY, },
     { "Current cluster status",
       PCMK_XE_CLUSTER_STATUS, },
     { "Executing Cluster Transition",
       PCMK_XE_TRANSITION, },
     { "Failed Resource Actions",
       PCMK_XE_FAILURES, },
     { "Fencing History",
       PCMK_XE_FENCE_HISTORY, },
     { "Full List of Resources",
       PCMK_XE_RESOURCES, },
     { "Inactive Resources",
       PCMK_XE_RESOURCES, },
     { "Migration Summary",
       PCMK_XE_NODE_HISTORY, },
     { "Negative Location Constraints",
       PCMK_XE_BANS, },
     { "Node Attributes",
       PCMK_XE_NODE_ATTRIBUTES, },
     { "Operations",
       PCMK_XE_NODE_HISTORY, },
     { "Resource Config",
       PCMK_XE_RESOURCE_CONFIG, },
     { "Resource Operations",
       PCMK_XE_OPERATIONS, },
     { "Revised Cluster Status",
       PCMK_XE_REVISED_CLUSTER_STATUS, },
     { "Timings",
       PCMK_XE_TIMINGS, },
     { "Transition Summary",
       PCMK_XE_ACTIONS, },
     { "Utilization Information",
       PCMK_XE_UTILIZATIONS, },
 
     { NULL, NULL }
 };
 
 /* The first several elements of this struct must be the same as the first
  * several elements of private_data_s in lib/common/output_html.c.  That
  * struct gets passed to a bunch of the pcmk__output_xml_* functions which
  * assume an XML private_data_s.  Keeping them laid out the same means this
  * still works.
  */
 typedef struct private_data_s {
     /* Begin members that must match the HTML version */
     xmlNode *root;
     GQueue *parent_q;
     GSList *errors;
     /* End members that must match the HTML version */
     bool legacy_xml;
 } private_data_t;
 
+static bool
+has_root_node(pcmk__output_t *out)
+{
+    private_data_t *priv = NULL;
+
+    CRM_ASSERT(out != NULL);
+
+    priv = out->priv;
+    return priv != NULL && priv->root != NULL;
+}
+
+static void
+add_root_node(pcmk__output_t *out)
+{
+    private_data_t *priv = NULL;
+
+    /* has_root_node will assert if out is NULL, so no need to do it here */
+    if (has_root_node(out)) {
+        return;
+    }
+
+    priv = out->priv;
+
+    if (legacy_xml || priv->legacy_xml) {
+        priv->root = create_xml_node(NULL, PCMK_XE_CRM_MON);
+        crm_xml_add(priv->root, PCMK_XA_VERSION, PACEMAKER_VERSION);
+    } else {
+        priv->root = create_xml_node(NULL, PCMK_XE_PACEMAKER_RESULT);
+        crm_xml_add(priv->root, PCMK_XA_API_VERSION, PCMK__API_VERSION);
+        crm_xml_add(priv->root, PCMK_XA_REQUEST,
+                    pcmk__s(out->request, "libpacemaker"));
+    }
+
+    priv->parent_q = g_queue_new();
+    g_queue_push_tail(priv->parent_q, priv->root);
+}
+
 static void
 xml_free_priv(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     if (out == NULL || out->priv == NULL) {
         return;
     }
 
     priv = out->priv;
 
-    free_xml(priv->root);
-    g_queue_free(priv->parent_q);
+    if (has_root_node(out)) {
+        free_xml(priv->root);
+        g_queue_free(priv->parent_q);
+    }
+
     g_slist_free(priv->errors);
     free(priv);
     out->priv = NULL;
 }
 
 static bool
 xml_init(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL);
 
     /* If xml_init was previously called on this output struct, just return. */
     if (out->priv != NULL) {
         return true;
     } else {
         out->priv = calloc(1, sizeof(private_data_t));
         if (out->priv == NULL) {
             return false;
         }
 
         priv = out->priv;
     }
 
-    if (legacy_xml || priv->legacy_xml) {
-        priv->root = create_xml_node(NULL, PCMK_XE_CRM_MON);
-        crm_xml_add(priv->root, PCMK_XA_VERSION, PACEMAKER_VERSION);
-    } else {
-        priv->root = create_xml_node(NULL, PCMK_XE_PACEMAKER_RESULT);
-        crm_xml_add(priv->root, PCMK_XA_API_VERSION, PCMK__API_VERSION);
-        crm_xml_add(priv->root, PCMK_XA_REQUEST,
-                    pcmk__s(out->request, "libpacemaker"));
-    }
-
-    priv->parent_q = g_queue_new();
     priv->errors = NULL;
-    g_queue_push_tail(priv->parent_q, priv->root);
 
     /* Copy this from the file-level variable.  This means that it is only settable
      * as a command line option, and that pcmk__output_new must be called after all
      * command line processing is completed.
      */
     priv->legacy_xml = legacy_xml;
 
     return true;
 }
 
 static void
 add_error_node(gpointer data, gpointer user_data) {
     char *str = (char *) data;
     xmlNodePtr node = (xmlNodePtr) user_data;
     pcmk_create_xml_text_node(node, PCMK_XE_ERROR, str);
 }
 
 static void
 xml_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) {
     private_data_t *priv = NULL;
     xmlNodePtr node;
 
     CRM_ASSERT(out != NULL);
     priv = out->priv;
 
-    /* If root is NULL, xml_init failed and we are being called from pcmk__output_free
-     * in the pcmk__output_new path.
-     */
-    if (priv == NULL || priv->root == NULL) {
+    if (priv == NULL) {
         return;
     }
 
+    add_root_node(out);
+
     if (legacy_xml || priv->legacy_xml) {
         GSList *node = priv->errors;
 
         if (exit_status != CRM_EX_OK) {
             fprintf(stderr, "%s\n", crm_exit_str(exit_status));
         }
 
         while (node != NULL) {
             fprintf(stderr, "%s\n", (char *) node->data);
             node = node->next;
         }
     } else {
         char *rc_as_str = pcmk__itoa(exit_status);
 
         node = create_xml_node(priv->root, PCMK_XE_STATUS);
         pcmk__xe_set_props(node,
                            PCMK_XA_CODE, rc_as_str,
                            PCMK_XA_MESSAGE, crm_exit_str(exit_status),
                            NULL);
 
         if (g_slist_length(priv->errors) > 0) {
             xmlNodePtr errors_node = create_xml_node(node, PCMK_XE_ERRORS);
             g_slist_foreach(priv->errors, add_error_node, (gpointer) errors_node);
         }
 
         free(rc_as_str);
     }
 
     if (print) {
         pcmk__xml2fd(fileno(out->dest), priv->root);
     }
 
     if (copy_dest != NULL) {
         *copy_dest = copy_xml(priv->root);
     }
 }
 
 static void
 xml_reset(pcmk__output_t *out) {
     CRM_ASSERT(out != NULL);
 
     out->dest = freopen(NULL, "w", out->dest);
     CRM_ASSERT(out->dest != NULL);
 
     xml_free_priv(out);
     xml_init(out);
 }
 
 static void
 xml_subprocess_output(pcmk__output_t *out, int exit_status,
                       const char *proc_stdout, const char *proc_stderr) {
     xmlNodePtr node, child_node;
     char *rc_as_str = NULL;
 
     CRM_ASSERT(out != NULL);
 
     rc_as_str = pcmk__itoa(exit_status);
 
     node = pcmk__output_xml_create_parent(out, PCMK_XE_COMMAND,
                                           PCMK_XA_CODE, rc_as_str,
                                           NULL);
 
     if (proc_stdout != NULL) {
         child_node = pcmk_create_xml_text_node(node, PCMK_XE_OUTPUT,
                                                proc_stdout);
         crm_xml_add(child_node, PCMK_XA_SOURCE, "stdout");
     }
 
     if (proc_stderr != NULL) {
         child_node = pcmk_create_xml_text_node(node, PCMK_XE_OUTPUT,
                                                proc_stderr);
         crm_xml_add(child_node, PCMK_XA_SOURCE, "stderr");
     }
 
     free(rc_as_str);
 }
 
 static void
 xml_version(pcmk__output_t *out, bool extended) {
     const char *author = "Andrew Beekhof and the Pacemaker project "
                          "contributors";
     CRM_ASSERT(out != NULL);
 
     pcmk__output_create_xml_node(out, PCMK_XE_VERSION,
                                  PCMK_XA_PROGRAM, "Pacemaker",
                                  PCMK_XA_VERSION, PACEMAKER_VERSION,
                                  PCMK_XA_AUTHOR, author,
                                  PCMK_XA_BUILD, BUILD_VERSION,
                                  PCMK_XA_FEATURES, CRM_FEATURES,
                                  NULL);
 }
 
 G_GNUC_PRINTF(2, 3)
 static void
 xml_err(pcmk__output_t *out, const char *format, ...) {
     private_data_t *priv = NULL;
     int len = 0;
     char *buf = NULL;
     va_list ap;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
+    add_root_node(out);
+
     va_start(ap, format);
     len = vasprintf(&buf, format, ap);
     CRM_ASSERT(len > 0);
     va_end(ap);
 
     priv->errors = g_slist_append(priv->errors, buf);
 }
 
 G_GNUC_PRINTF(2, 3)
 static int
 xml_info(pcmk__output_t *out, const char *format, ...) {
     return pcmk_rc_no_output;
 }
 
 static void
 xml_output_xml(pcmk__output_t *out, const char *name, const char *buf) {
     xmlNodePtr parent = NULL;
     xmlNodePtr cdata_node = NULL;
 
     CRM_ASSERT(out != NULL);
 
     parent = pcmk__output_create_xml_node(out, name, NULL);
     if (parent == NULL) {
         return;
     }
     cdata_node = xmlNewCDataBlock(parent->doc, (pcmkXmlStr) buf, strlen(buf));
     xmlAddChild(parent, cdata_node);
 }
 
 G_GNUC_PRINTF(4, 5)
 static void
 xml_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun,
                const char *format, ...) {
     va_list ap;
     char *name = NULL;
     char *buf = NULL;
     int len;
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
     va_start(ap, format);
     len = vasprintf(&buf, format, ap);
     CRM_ASSERT(len >= 0);
     va_end(ap);
 
     for (const subst_t *s = substitutions; s->from != NULL; s++) {
         if (strcmp(s->from, buf) == 0) {
             name = g_strdup(s->to);
             break;
         }
     }
 
     if (name == NULL) {
         name = g_ascii_strdown(buf, -1);
     }
 
     if (legacy_xml || priv->legacy_xml || simple_list) {
         pcmk__output_xml_create_parent(out, name, NULL);
     } else {
         pcmk__output_xml_create_parent(out, PCMK_XE_LIST,
                                        PCMK_XA_NAME, name,
                                        NULL);
     }
 
     g_free(name);
     free(buf);
 }
 
 G_GNUC_PRINTF(3, 4)
 static void
 xml_list_item(pcmk__output_t *out, const char *name, const char *format, ...) {
     xmlNodePtr item_node = NULL;
     va_list ap;
     char *buf = NULL;
     int len;
 
     CRM_ASSERT(out != NULL);
 
     va_start(ap, format);
     len = vasprintf(&buf, format, ap);
     CRM_ASSERT(len >= 0);
     va_end(ap);
 
     item_node = pcmk__output_create_xml_text_node(out, PCMK_XE_ITEM, buf);
 
     if (name != NULL) {
         crm_xml_add(item_node, PCMK_XA_NAME, name);
     }
 
     free(buf);
 }
 
 static void
 xml_increment_list(pcmk__output_t *out) {
     /* This function intentially left blank */
 }
 
 static void
 xml_end_list(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
     if (priv->legacy_xml || simple_list) {
         g_queue_pop_tail(priv->parent_q);
     } else {
         char *buf = NULL;
         xmlNodePtr node;
 
         node = g_queue_pop_tail(priv->parent_q);
         buf = crm_strdup_printf("%lu", xmlChildElementCount(node));
         crm_xml_add(node, PCMK_XA_COUNT, buf);
         free(buf);
     }
 }
 
 static bool
 xml_is_quiet(pcmk__output_t *out) {
     return false;
 }
 
 static void
 xml_spacer(pcmk__output_t *out) {
     /* This function intentionally left blank */
 }
 
 static void
 xml_progress(pcmk__output_t *out, bool end) {
     /* This function intentionally left blank */
 }
 
 pcmk__output_t *
 pcmk__mk_xml_output(char **argv) {
     pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
 
     if (retval == NULL) {
         return NULL;
     }
 
     retval->fmt_name = "xml";
     retval->request = pcmk__quote_cmdline(argv);
 
     retval->init = xml_init;
     retval->free_priv = xml_free_priv;
     retval->finish = xml_finish;
     retval->reset = xml_reset;
 
     retval->register_message = pcmk__register_message;
     retval->message = pcmk__call_message;
 
     retval->subprocess_output = xml_subprocess_output;
     retval->version = xml_version;
     retval->info = xml_info;
     retval->transient = xml_info;
     retval->err = xml_err;
     retval->output_xml = xml_output_xml;
 
     retval->begin_list = xml_begin_list;
     retval->list_item = xml_list_item;
     retval->increment_list = xml_increment_list;
     retval->end_list = xml_end_list;
 
     retval->is_quiet = xml_is_quiet;
     retval->spacer = xml_spacer;
     retval->progress = xml_progress;
     retval->prompt = pcmk__text_prompt;
 
     return retval;
 }
 
 xmlNodePtr
 pcmk__output_xml_create_parent(pcmk__output_t *out, const char *name, ...) {
     va_list args;
     xmlNodePtr node = NULL;
 
     CRM_ASSERT(out != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL);
 
     node = pcmk__output_create_xml_node(out, name, NULL);
 
     va_start(args, name);
     pcmk__xe_set_propv(node, args);
     va_end(args);
 
     pcmk__output_xml_push_parent(out, node);
     return node;
 }
 
 void
 pcmk__output_xml_add_node_copy(pcmk__output_t *out, xmlNodePtr node) {
     private_data_t *priv = NULL;
     xmlNodePtr parent = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     CRM_ASSERT(node != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return);
 
+    add_root_node(out);
+
     priv = out->priv;
     parent = g_queue_peek_tail(priv->parent_q);
 
     // Shouldn't happen unless the caller popped priv->root
     CRM_CHECK(parent != NULL, return);
 
     add_node_copy(parent, node);
 }
 
 xmlNodePtr
 pcmk__output_create_xml_node(pcmk__output_t *out, const char *name, ...) {
     xmlNodePtr node = NULL;
     private_data_t *priv = NULL;
     va_list args;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL);
 
+    add_root_node(out);
+
     priv = out->priv;
 
     node = create_xml_node(g_queue_peek_tail(priv->parent_q), name);
     va_start(args, name);
     pcmk__xe_set_propv(node, args);
     va_end(args);
 
     return node;
 }
 
 xmlNodePtr
 pcmk__output_create_xml_text_node(pcmk__output_t *out, const char *name, const char *content) {
     xmlNodePtr node = NULL;
 
     CRM_ASSERT(out != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL);
 
     node = pcmk__output_create_xml_node(out, name, NULL);
     pcmk__xe_set_content(node, content);
     return node;
 }
 
 void
 pcmk__output_xml_push_parent(pcmk__output_t *out, xmlNodePtr parent) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     CRM_ASSERT(parent != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return);
 
+    add_root_node(out);
+
     priv = out->priv;
 
     g_queue_push_tail(priv->parent_q, parent);
 }
 
 void
 pcmk__output_xml_pop_parent(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return);
 
+    add_root_node(out);
+
     priv = out->priv;
 
     CRM_ASSERT(g_queue_get_length(priv->parent_q) > 0);
     g_queue_pop_tail(priv->parent_q);
 }
 
 xmlNodePtr
 pcmk__output_xml_peek_parent(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL);
 
+    add_root_node(out);
+
     priv = out->priv;
 
     /* If queue is empty NULL will be returned */
     return g_queue_peek_tail(priv->parent_q);
 }