Workflow stats

工作流概述

这是一个包含33个节点的复杂工作流,主要用于自动化处理各种任务。

工作流源代码

下载
{
  "id": "D0I76cew5KOnlem0",
  "meta": {
    "instanceId": "fb924c73af8f703905bc09c9ee8076f48c17b596ed05b18c0ff86915ef8a7c4a",
    "templateCredsSetupCompleted": true
  },
  "name": "Workflow stats",
  "tags": [],
  "nodes": [
    {
      "id": "b1a73981-db6a-4fd2-9cad-d02bfecc7d3d",
      "name": "When clicking \"Test workflow\"",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        1060,
        740
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "cbe2d1a8-51e9-4f3d-8ca5-321f3edf9a92",
      "name": "nodes-section",
      "type": "n8n-nodes-base.code",
      "position": [
        1900,
        800
      ],
      "parameters": {
        "jsCode": "// Initialize an empty object to hold the mapping between nodes and workflows
const nodeToWorkflowsMap = {};

// Iterate over each workflow in the input
$input.all().forEach(item => {
  const { wf_stats } = item.json;
  const { nodes_unique, wf_name, wf_url, wf_id } = wf_stats;

  // For each unique node in the workflow, update the mapping
  nodes_unique.forEach(node => {
    if (!nodeToWorkflowsMap[node]) {
      // If the node has not been added to the map, initialize it with the current workflow
      nodeToWorkflowsMap[node] = [{ wf_name, wf_url, wf_id }];
    } else {
      // If the node is already in the map, append the current workflow to its list
      nodeToWorkflowsMap[node].push({ wf_name, wf_url, wf_id });
    }
  });
});

// Convert the map into an array format suitable for n8n's output
const result = Object.keys(nodeToWorkflowsMap).map(node => ({
  json: {
    node,
    count: nodeToWorkflowsMap[node].length,
    workflows: nodeToWorkflowsMap[node]
  }
}));

return result;"
      },
      "typeVersion": 2
    },
    {
      "id": "49a10bf3-f2e6-4fe9-8390-2a266f1b52a9",
      "name": "workflows-section",
      "type": "n8n-nodes-base.set",
      "position": [
        1680,
        640
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "fd4aa80c-cd88-4a97-b943-dfcf1ab222ee",
              "name": "wf_stats",
              "type": "object",
              "value": "={{ { nodes_unique     :[...new Set($json.nodes_array)],
     nodes_count_total:$json.nodes_array.length,
     nodes_count_uniq :[...new Set($json.nodes_array)].length,
     wf_created       :DateTime.fromISO($json.createdAt).toFormat('yyyy-MM-dd HH:mm:ss'),
     wf_updated       :DateTime.fromISO($json.updatedAt).toFormat('yyyy-MM-dd HH:mm:ss'),
     wf_name          :$json.name,
     wf_id            :`wf-${$json.id}`,
     wf_url           :`${$json.instance_url}/workflow/${$json.id}` || \"\",
     wf_active        :$json.active,
     wf_trigcount     :$json.triggerCount,
     wf_tags          :$json.tags_array,
     wf_whooks        :$json.webhook_paths_array

} }}"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "afbbc6a0-dcb8-48e7-b2d1-ef00c769d3b7",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1240,
        -120
      ],
      "parameters": {
        "width": 1490,
        "height": 1375,
        "content": "## Create the main JSON object with the workflow statistics
* `globals` - general information (# of workflows, active workflows, total trigger count)
* `wf_stats` - summary per workflow (number or nodes, unique nodes, list of nodes and tags)
* `nodes-section` - summary per node (number of workflows that use a node and their URLs)
* `tags-section` - summary per tag (number of workflows that use a node and their URLs)
* `webhook-section` - lists all webhook endpoints of the instance and shows the workflow URLs

### You can use this JSON in BI tools to create a custom dashboard

## Learn JS tips & tricks
### Instead of just using one Code node, the workflow contains several nodes with useful advanced tricks.

### JMESPath
* Make a simple array of strings out of a complex array: `$jmespath($json,'nodes[*].type')`
* Extract values based on condition: `$jmespath($input.all(),'[?json.wf_stats.wf_active == `true`]')`

### Map and arrow functions
* Perform operation on each array element: `.map(item => (item.split('.').pop().toUpperCase() ))`
* Calculate sum of values from an array: `.reduce((accumulator, currentValue) => accumulator + currentValue, 0)`

### Create an array with only unique values
* `[...new Set($json.nodes_array)]`

### Date-time conversions with the Luxon library:
* `DateTime.fromISO($json.createdAt).toFormat('yyyy-MM-dd HH:mm:ss')`

### Template literals (Template strings) for creating strings in JS
* `wf-${$json.id}`"
      },
      "typeVersion": 1
    },
    {
      "id": "9dcb369b-fe22-45e1-906d-848a85b0c1e4",
      "name": "tags-section",
      "type": "n8n-nodes-base.code",
      "position": [
        1900,
        960
      ],
      "parameters": {
        "jsCode": "// Initialize an empty object to hold the mapping between tags and workflows
const tagToWorkflowsMap = {};

// Iterate over each workflow in the input
$input.all().forEach(item => {
  const { wf_stats } = item.json;
  // Destructure wf_url along with other properties
  const { wf_tags, wf_name, wf_id, wf_url } = wf_stats;

  // Check if the workflow has tags
  if (wf_tags && wf_tags.length > 0) {
    // For each tag in the workflow, update the mapping
    wf_tags.forEach(tag => {
      if (!tagToWorkflowsMap[tag]) {
        // If the tag has not been added to the map, initialize it with the current workflow including wf_url
        tagToWorkflowsMap[tag] = [{ wf_name, wf_id, wf_url }];
      } else {
        // If the tag is already in the map, append the current workflow to its list including wf_url
        tagToWorkflowsMap[tag].push({ wf_name, wf_id, wf_url });
      }
    });
  } else {
    // Handle workflows with no tags, categorizing them under a 'No Tags' category
    const noTagKey = 'No Tags'; // or any other placeholder you prefer
    if (!tagToWorkflowsMap[noTagKey]) {
      // Initialize with the current workflow including wf_url
      tagToWorkflowsMap[noTagKey] = [{ wf_name, wf_id, wf_url }];
    } else {
      // Append the current workflow to its list including wf_url
      tagToWorkflowsMap[noTagKey].push({ wf_name, wf_id, wf_url });
    }
  }
});

// Convert the map into an array format suitable for n8n's output
const result = Object.keys(tagToWorkflowsMap).map(tag => ({
  json: {
    tag,
    count: tagToWorkflowsMap[tag].length,
    workflows: tagToWorkflowsMap[tag] // This now contains objects with wf_name, wf_id, and wf_url
  }
}));

return result;"
      },
      "typeVersion": 2
    },
    {
      "id": "7509c96c-0907-4cf1-94cf-f9dfbc0d3f9d",
      "name": "globals-section",
      "type": "n8n-nodes-base.set",
      "position": [
        1900,
        520
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "9e1284bd-73c5-4d3d-bb5d-3437fca97780",
              "name": "globals",
              "type": "object",
              "value": "={{ { global_total : $input.all().length,
     global_active : $jmespath($input.all(),'[?json.wf_stats.wf_active == `true`]').length,
     global_trigger: $jmespath($input.all(),'[].json.wf_stats.wf_trigcount').reduce((accumulator, currentValue) => accumulator + currentValue, 0) }  }}"
            }
          ]
        }
      },
      "executeOnce": true,
      "typeVersion": 3.3
    },
    {
      "id": "2c0bc2dd-63d9-4b65-9e4e-2920892efaf7",
      "name": "Execute Workflow Trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        1060,
        540
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "8bceb3e9-e1d9-4ca0-af91-5377d4300346",
      "name": "Convert to XML",
      "type": "n8n-nodes-base.xml",
      "position": [
        1480,
        1600
      ],
      "parameters": {
        "mode": "jsonToxml",
        "options": {
          "headless": true
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6151d4b8-f592-418d-b099-17c71b1de0e4",
      "name": "Create HTML",
      "type": "n8n-nodes-base.html",
      "position": [
        1680,
        1600
      ],
      "parameters": {
        "html": "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>
<?xml-stylesheet type=\"text/xsl\" href=\"{{ $env.WEBHOOK_URL }}webhook/73a91e4d-143d-4168-9efb-6c56f2258aec/dashboard.xsl\"?>

{{ $json.data }}"
      },
      "typeVersion": 1
    },
    {
      "id": "e5ebc5c1-0fcc-4f9d-b8eb-df3a367cc097",
      "name": "Move Binary Data",
      "type": "n8n-nodes-base.moveBinaryData",
      "position": [
        1880,
        1600
      ],
      "parameters": {
        "mode": "jsonToBinary",
        "options": {
          "mimeType": "text/xml",
          "keepSource": false,
          "useRawData": true
        },
        "sourceKey": "html",
        "convertAllData": false
      },
      "typeVersion": 1
    },
    {
      "id": "5fdb74f7-6b2a-4042-91a2-c2088e8ea712",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2080,
        1600
      ],
      "parameters": {
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/xml"
              },
              {
                "name": "Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        },
        "respondWith": "binary"
      },
      "typeVersion": 1
    },
    {
      "id": "ed113e7c-c49f-4854-8fbf-5f7bf3591ede",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1000,
        1840
      ],
      "parameters": {
        "color": 7,
        "width": 909,
        "height": 426,
        "content": "# DO NOT RUN THIS
## This webhook is needed to comply with the CORS policy of modern browsers.
### It generates XML template and serves it using your n8n URL

XSLT template is created with 2 Set nodes:
1. `Template elements` node defines each section of the Dashboard
2. `Final template` node puts everything together
3. Bootstrap 5.3 styling is added. You can save the .css and .js files on your server. Right now a CDN version of the librarly is used."
      },
      "typeVersion": 1
    },
    {
      "id": "b6674f77-7797-4090-a4f9-56a9ddc0d4e0",
      "name": "Respond to Webhook2",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1700,
        2120
      ],
      "parameters": {
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/xsl"
              }
            ]
          }
        },
        "respondWith": "text",
        "responseBody": "={{ $json.xsl_template }}"
      },
      "typeVersion": 1
    },
    {
      "id": "c8c906da-0b61-46b0-be96-11da3c203e3f",
      "name": "Final template",
      "type": "n8n-nodes-base.set",
      "position": [
        1500,
        2120
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "2a42cfed-0451-41c2-9634-865cac2ea68d",
              "name": "xsl_template",
              "type": "string",
              "value": "=<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">
  <xsl:template match=\"/\">
    <html>
      <head>
        <title>n8n Workflows Dashboard</title>
        <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH\" crossorigin=\"anonymous\" />
        <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz\" crossorigin=\"anonymous\"></script>
        <style>
          body {
            position: relative;
          }
          
          section {
            scroll-margin-top: 20px;
          }

          .form-check-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            cursor: default;
            z-index: 1;
          }

          .badge-link {
            scroll-margin-top: 80px;
          }

          .sidebar {
            position: fixed;
            top: 0;
            bottom: 0;
            left: 0;
            z-index: 100;
            padding: 20px 0 0;
            box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
            overflow-y: auto;
          }

          .sidebar-sticky {
            position: relative;
            top: 0;
            height: calc(100vh - 20px);
            overflow-x: hidden;
            overflow-y: auto;
            padding-left: .25rem;
          }

          .nooverflow {
            overflow-x: hidden;
          }

          .sidebar .nav-link {
            font-weight: 500;
            color: var(--bs-gray-800);
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
          }

          .sidebar .nav-link.active {
            color: var(--bs-primary);
          }

          .sidebar .btn {
            padding: .25rem .5rem;
            font-weight: 600;
            color: var(--bs-gray-800);
          }

          .sidebar-a {
            padding: .25rem .5rem;
            margin-left: 1.25rem;
            color: var(--bs-gray-800);
            background-color: transparent;
          }

          .sidebar-bottom {
            padding: .25rem .5rem;
            margin-left: 1.25rem;
          }

          .btn-toggle::before {
            width: 1.25em;
            line-height: 0;
            content: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e\");
            transition: transform .35s ease;
            transform-origin: .5em 50%;
          }

          .btn-toggle[aria-expanded=\"true\"] {
            color: var(--bs-gray-800);
          }

          .btn-toggle[aria-expanded=\"true\"]::before {
            transform: rotate(90deg);
          }

          .btn-toggle-nav a {
            padding: .1rem .5rem;
            margin-top: .125rem;
          }

          .sidebar-a:hover,
          .sidebar-a:focus,
		  .btn-toggle:hover,
          .btn-toggle:focus {
            background-color: var(--bs-primary-bg-subtle);
          }

          .content {
            margin-left: 16.66%;
            padding: 20px;
          }

          .card-img-container {
            max-height: 150px;
            overflow: hidden;
            display: flex;
            align-items: center;
            justify-content: center;
          }
          
          .card-img-top {
            object-fit: cover;
            object-position: top;
            height: 100%;
            width: 100%;
          }

        </style>
      </head>
      <body data-bs-spy=\"scroll\" data-bs-target=\"#sidebar\" data-bs-offset=\"10\">
        <div class=\"container-fluid\">
          <div class=\"row\">
{{ $json.sidebar }}

            <main class=\"col-10 content\">

<!-- Overview section -->
{{ $json.overview }}
<!-- Workflows section -->
{{ $json.workflows }}
<!-- Nodes section -->
{{ $json.nodes }}
<!-- Tags section -->
{{ $json.tags }}
<!-- Webhooks section -->
{{ $json.webhooks }}
<!-- About section -->
{{ $json.about }}

            </main>
          </div>
        </div>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "173493c0-1f96-4416-a545-6d8c6034ac76",
      "name": "Template elements",
      "type": "n8n-nodes-base.set",
      "position": [
        1300,
        2120
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "afbcca70-2977-46a3-89c3-27a96f791d13",
              "name": "sidebar",
              "type": "string",
              "value": "=            <nav id=\"sidebar\" class=\"col-2 bg-light sidebar\">
              <div class=\"sidebar-sticky\">
                <ul class=\"list-unstyled ps-0\">
                  <li class=\"mb-1\">
                    <a href=\"#overview\" class=\"btn d-inline-flex align-items-center rounded border-0 sidebar-a\">Overview</a>
                  </li>
                  <!-- Workflows Section -->
                  <li class=\"mb-1\">
                    <button class=\"btn btn-toggle d-inline-flex align-items-center rounded border-0 collapsed\" data-bs-toggle=\"collapse\" data-bs-target=\"#workflows-collapse\" aria-expanded=\"false\">
                      Workflows
                    </button>
                    <div class=\"collapse\" id=\"workflows-collapse\">
                      <ul class=\"btn-toggle-nav list-unstyled fw-normal pb-1 small\">
                        <xsl:for-each select=\"root/wf_stats\">
                          <li><a href=\"#{wf_id}\" class=\"link-dark d-inline-flex text-decoration-none rounded sidebar-a\"><xsl:value-of select=\"wf_name\" /></a></li>
                        </xsl:for-each>
                      </ul>
                    </div>
                  </li>
                  <!-- Nodes Section (No Sanitization) -->
                  <li class=\"mb-1\">
                    <button class=\"btn btn-toggle d-inline-flex align-items-center rounded border-0 collapsed\" data-bs-toggle=\"collapse\" data-bs-target=\"#nodes-collapse\" aria-expanded=\"false\">
                      Nodes
                    </button>
                    <div class=\"collapse\" id=\"nodes-collapse\">
                      <ul class=\"btn-toggle-nav list-unstyled fw-normal pb-1 small\">
                        <xsl:for-each select=\"root/nodes-section\">
                          <li>
                            <a class=\"link-dark d-inline-flex text-decoration-none rounded sidebar-a\">
                              <xsl:attribute name=\"href\">
                                <!-- Use raw node name -->
                                <xsl:value-of select=\"concat('#node-', node)\" />
                              </xsl:attribute>
                              <xsl:value-of select=\"node\" />
                            </a>
                          </li>
                        </xsl:for-each>
                      </ul>
                    </div>
                  </li>
                  <!-- Tags Section (Keep Sanitization) -->
                  <li class=\"mb-1\">
                    <button class=\"btn btn-toggle d-inline-flex align-items-center rounded border-0 collapsed\" data-bs-toggle=\"collapse\" data-bs-target=\"#tags-collapse\" aria-expanded=\"false\">
                      Tags
                    </button>
                    <div class=\"collapse\" id=\"tags-collapse\">
                      <ul class=\"btn-toggle-nav list-unstyled fw-normal pb-1 small\">
                        <xsl:for-each select=\"root/tags-section\">
                           <!-- Sanitize tag name for href -->
                          <xsl:variable name=\"raw_tag\" select=\"tag\"/>
                          <xsl:variable name=\"lower_tag\" select=\"translate($raw_tag, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')\"/>
                          <xsl:variable name=\"spaced_tag\" select=\"translate($lower_tag, ' ', '-')\"/>
                          <xsl:variable name=\"invalid_chars\" select=\"&quot;'./?&amp;=:&quot;\"/>
                          <xsl:variable name=\"sanitized_tag_id\" select=\"translate($spaced_tag, $invalid_chars, '')\"/>
                          <li>
                            <a class=\"link-dark d-inline-flex text-decoration-none rounded sidebar-a\">
                              <xsl:attribute name=\"href\">
                                <xsl:value-of select=\"concat('#tag-', $sanitized_tag_id)\" />
                              </xsl:attribute>
                              <xsl:value-of select=\"tag\" />
                            </a>
                          </li>
                        </xsl:for-each>
                      </ul>
                    </div>
                  </li>
                  <!-- Webhooks Section (No Sanitization) -->
                  <li class=\"mb-1\">
                    <button class=\"btn btn-toggle d-inline-flex align-items-center rounded border-0 collapsed\" data-bs-toggle=\"collapse\" data-bs-target=\"#webhooks-collapse\" aria-expanded=\"false\">
                      Webhooks
                    </button>
                    <div class=\"collapse\" id=\"webhooks-collapse\">
                      <ul class=\"btn-toggle-nav list-unstyled fw-normal pb-1 small\">
                        <xsl:for-each select=\"root/whooks-section\">
                          <li>
                            <a class=\"link-dark d-inline-flex text-decoration-none rounded sidebar-a\">
                              <xsl:attribute name=\"href\">
                                <!-- Use raw hookpath -->
                                <xsl:value-of select=\"concat('#whook-', hookpath)\" />
                              </xsl:attribute>
                              <xsl:value-of select=\"hookpath\" />
                            </a>
                          </li>
                        </xsl:for-each>
                      </ul>
                    </div>
                  </li>
                  <!-- END: Webhooks Section -->
                  <li class=\"border-top my-3\"></li>
                  <li class=\"mb-1\">
                    <a href=\"#about\" class=\"btn d-inline-flex align-items-center rounded border-0 sidebar-a\">About</a>
                  </li>
                </ul>
                <div class=\"sidebar-bottom\">
                  <p>n8n Dashboard ver 0.8<br/> <!-- Updated version number -->
Contacts: <a class=\"link-offset-1 link-offset-1-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover\" href=\"https://www.linkedin.com/in/parsadanyan/\" target=\"_blank\">Eduard Parsadanyan</a></p>
                </div>
              </div>
            </nav>
"
            },
            {
              "id": "d6dc34a7-3c79-44ef-957c-63aec4b2d75a",
              "name": "overview",
              "type": "string",
              "value": "=<section  id=\"overview\" class=\"container\">
  <h1>n8n Workflow Dashboard</h1>
</section>

<section class=\"container mt-3\">
  <h2>Overview</h2>
  <div class=\"row\">
    <div class=\"col-md-4\">
      <div class=\"card bg-body-secondary mb-2 shadow-sm\">
        <div class=\"card-body text-center\">
          <h5 class=\"card-title\">Total Workflows</h5>
          <p class=\"card-text display-4\">📊 <xsl:value-of select=\"root/globals/global_total\" /></p>
        </div>
      </div>
    </div>
    <div class=\"col-md-4\">
      <div class=\"card bg-body-secondary mb-2 shadow-sm\">
        <div class=\"card-body text-center\">
          <h5 class=\"card-title\">Active Workflows</h5>
          <p class=\"card-text display-4\">✅ <xsl:value-of select=\"root/globals/global_active\" /></p>
        </div>
      </div>
    </div>
    <div class=\"col-md-4\">
      <div class=\"card bg-body-secondary mb-2 shadow-sm\">
        <div class=\"card-body text-center\">
          <h5 class=\"card-title\">Triggers Count</h5>
          <p class=\"card-text display-4\">⚡ <xsl:value-of select=\"root/globals/global_trigger\" /></p>
        </div>
      </div>
    </div>
  </div>
</section>"
            },
            {
              "id": "19ed123c-404b-4a68-a298-8f24c285f71c",
              "name": "workflows",
              "type": "string",
              "value": "=<section id=\"workflows\" class=\"container mt-3\">
  <h2>Workflows</h2>
  <xsl:for-each select=\"root/wf_stats\">
    <div class=\"card mb-3 shadow-sm nooverflow\">
      <div class=\"card-body\">
        <div class=\"d-flex align-items-center mb-2\">
          <div class=\"form-check form-switch me-3 position-relative\">
            <input class=\"form-check-input\" type=\"checkbox\" role=\"switch\">
              <xsl:attribute name=\"id\">
                <xsl:value-of select=\"concat('switch-', wf_id)\" />
              </xsl:attribute>
              <xsl:if test=\"wf_active = 'true'\">
                <xsl:attribute name=\"checked\">checked</xsl:attribute>
              </xsl:if>
            </input>
            <label class=\"form-check-label\">
              <xsl:attribute name=\"for\">
                <xsl:value-of select=\"concat('switch-', wf_id)\" />
              </xsl:attribute>
            </label>
            <div class=\"form-check-overlay\"></div>
          </div>
          <h5 class=\"card-title mb-0\">
            <a class=\"link-offset-1 link-offset-1-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover\" href=\"{wf_url}\" target=\"_blank\" title=\"Open workflow in a new window\">
              <xsl:attribute name=\"id\">
                <xsl:value-of select=\"wf_id\" />
              </xsl:attribute>
              <xsl:value-of select=\"wf_name\" />
            </a>
          </h5>
          <div class=\"ms-auto\">
            <span class=\"badge bg-light font-monospace text-dark me-2\">
              Updated At: <xsl:value-of select=\"wf_updated\" />
            </span>
            <span class=\"badge bg-light font-monospace text-dark me-2\">
              Created At: <xsl:value-of select=\"wf_created\" />
            </span>
            <span class=\"badge bg-light font-monospace text-dark me-2\">
              Nodes (Tot | Uniq | Trig): <xsl:value-of select=\"nodes_count_total\" /> | <xsl:value-of select=\"nodes_count_uniq\" /> | <xsl:value-of select=\"wf_trigcount\" />
            </span>
          </div>
        </div>
        <div class=\"row\">
          <div class=\"d-flex\">
            <div>
              <xsl:for-each select=\"nodes_unique\">
                <a href=\"#node-{.}\" title=\"Jump to this node\" class=\"badge-link\">
                  <span class=\"badge bg-info-subtle border border-info-subtle text-info-emphasis rounded-pill me-2 mb-2\">
                    <xsl:value-of select=\".\" />
                  </span>
                </a>
              </xsl:for-each>
            </div>
            <xsl:if test=\"wf_tags\">
              <div class=\"ms-auto\">
                <xsl:for-each select=\"wf_tags\">
                  <a href=\"#tag-{.}\" title=\"Jump to this tag\" class=\"badge-link\">
                    <span class=\"badge bg-light-subtle border border-light-subtle text-light-emphasis rounded-pill me-2 mb-2\">
                      <xsl:value-of select=\".\" />
                    </span>
                  </a>
                </xsl:for-each>
              </div>
            </xsl:if>
          </div>
        </div>
      </div>
    </div>
  </xsl:for-each>
</section>"
            },
            {
              "id": "9869134d-ee39-49a2-a978-eb3adaac482d",
              "name": "nodes",
              "type": "string",
              "value": "=<section id=\"nodes\" class=\"container mt-3\">
  <h2>Nodes</h2>
  <div class=\"accordion\" id=\"nodesAccordion\">
    <xsl:for-each select=\"root/nodes-section\">
      <div class=\"accordion-item shadow-sm\">
        <!-- Place the target ID directly on the H3 using the original node name -->
        <h3 class=\"accordion-header\">
            <xsl:attribute name=\"id\">
                <!-- Use raw node name -->
                <xsl:value-of select=\"concat('node-', node)\"/>
            </xsl:attribute>
          <button class=\"accordion-button collapsed\" type=\"button\" data-bs-toggle=\"collapse\">
            <xsl:attribute name=\"data-bs-target\">
              <!-- Use raw node name for targeting -->
              <xsl:value-of select=\"concat('#collapse-node-', node)\" />
            </xsl:attribute>
            <xsl:attribute name=\"aria-controls\">
              <xsl:value-of select=\"concat('collapse-node-', node)\" />
            </xsl:attribute>
            <!-- The <a> tag no longer needs an ID -->
            <a>
              <!-- Display the original node name -->
              <xsl:value-of select=\"node\" /> <span class=\"badge bg-info-subtle text-info-emphasis rounded-pill ms-2\"><xsl:value-of select=\"count\" /></span>
            </a>
          </button>
        </h3>
        <div class=\"accordion-collapse collapse\">
          <xsl:attribute name=\"id\">
            <!-- Use raw node name for collapse ID -->
            <xsl:value-of select=\"concat('collapse-node-', node)\" />
          </xsl:attribute>
          <!-- aria-labelledby should point to the h3's ID -->
          <xsl:attribute name=\"aria-labelledby\">
            <xsl:value-of select=\"concat('node-', node)\" />
          </xsl:attribute>
          <div class=\"accordion-body\">
            <xsl:for-each select=\"workflows\">
              <span class=\"badge bg-info-subtle border border-info-subtle text-info-emphasis rounded-pill me-2 mb-2\">
                <a href=\"#{wf_id}\" class=\"text-primary-emphasis text-decoration-none me-1 section-offset\" title=\"Jump to workflow details\">
                  <xsl:value-of select=\"wf_name\" />
                </a>
                <a href=\"{wf_url}\" target=\"_blank\" class=\"text-primary-emphasis text-decoration-none\" title=\"Open workflow in a new window\">
                  🔗
                </a>
              </span>
            </xsl:for-each>
          </div>
        </div>
      </div>
    </xsl:for-each>
  </div>
</section>
"
            },
            {
              "id": "f09bc0d1-017e-44f5-bc39-6bdfeffe22ec",
              "name": "tags",
              "type": "string",
              "value": "=<section id=\"tags\" class=\"container mt-3\">
  <h2>Tags</h2>
  <div class=\"accordion\" id=\"tagsAccordion\">
    <xsl:for-each select=\"root/tags-section\">
      <!-- Sanitize the tag name -->
      <xsl:variable name=\"raw_tag\" select=\"tag\"/>
      <xsl:variable name=\"lower_tag\" select=\"translate($raw_tag, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')\"/>
      <xsl:variable name=\"spaced_tag\" select=\"translate($lower_tag, ' ', '-')\"/>
      <xsl:variable name=\"invalid_chars\" select=\"&quot;'./?&amp;=:&quot;\"/> <!-- Add any other chars you find problematic -->
      <xsl:variable name=\"sanitized_tag_id\" select=\"translate($spaced_tag, $invalid_chars, '')\"/>

      <div class=\"accordion-item shadow-sm\">
        <h3 class=\"accordion-header\">
            <xsl:attribute name=\"id\">
                <xsl:value-of select=\"concat('heading-tag-', $sanitized_tag_id)\"/>
            </xsl:attribute>
          <button class=\"accordion-button collapsed\" type=\"button\" data-bs-toggle=\"collapse\">
            <xsl:attribute name=\"data-bs-target\">
              <xsl:value-of select=\"concat('#collapse-tag-', $sanitized_tag_id)\" />
            </xsl:attribute>
            <xsl:attribute name=\"aria-controls\">
              <xsl:value-of select=\"concat('collapse-tag-', $sanitized_tag_id)\" />
            </xsl:attribute>
            <a>
              <!-- Use the sanitized ID here -->
              <xsl:attribute name=\"id\">
                <xsl:value-of select=\"concat('tag-', $sanitized_tag_id)\" />
              </xsl:attribute>
              <!-- Display the original tag name -->
              <xsl:value-of select=\"tag\" /> <span class=\"badge bg-light-subtle text-light-emphasis rounded-pill ms-2\"><xsl:value-of select=\"count\" /></span>
            </a>
          </button>
        </h3>
        <div class=\"accordion-collapse collapse\">
          <xsl:attribute name=\"id\">
            <xsl:value-of select=\"concat('collapse-tag-', $sanitized_tag_id)\" />
          </xsl:attribute>
          <xsl:attribute name=\"aria-labelledby\">
            <xsl:value-of select=\"concat('heading-tag-', $sanitized_tag_id)\" />
          </xsl:attribute>
          <div class=\"accordion-body\">
            <xsl:for-each select=\"workflows\">
              <span class=\"badge bg-light-subtle border border-light-subtle text-light-emphasis rounded-pill me-2 mb-2\">
                <a href=\"#{wf_id}\" class=\"text-primary-emphasis text-decoration-none me-1 section-offset\" title=\"Jump to workflow details\">
                  <xsl:value-of select=\"wf_name\" />
                </a>
                <a href=\"{wf_url}\" target=\"_blank\" class=\"text-primary-emphasis text-decoration-none\" title=\"Open workflow in a new window\">
                  🔗
                </a>
              </span>
            </xsl:for-each>
          </div>
        </div>
      </div>
    </xsl:for-each>
  </div>
</section>
"
            },
            {
              "id": "2e1f449c-a59b-4eb7-a3b7-48bedff01812",
              "name": "webhooks",
              "type": "string",
              "value": "=<section id=\"webhooks\" class=\"container mt-3\">
  <h2>Webhooks</h2>
  <div class=\"accordion\" id=\"webhooksAccordion\">
    <xsl:for-each select=\"root/whooks-section\">
      <div class=\"accordion-item shadow-sm\">
        <!-- Place the target ID directly on the H3 using the original hookpath -->
        <h3 class=\"accordion-header\">
            <xsl:attribute name=\"id\">
                <!-- Use raw hookpath -->
                <xsl:value-of select=\"concat('whook-', hookpath)\"/>
            </xsl:attribute>
          <button class=\"accordion-button collapsed\" type=\"button\" data-bs-toggle=\"collapse\">
            <xsl:attribute name=\"data-bs-target\">
              <!-- Use raw hookpath for targeting -->
              <xsl:value-of select=\"concat('#collapse-whook-', hookpath)\" />
            </xsl:attribute>
            <xsl:attribute name=\"aria-controls\">
              <xsl:value-of select=\"concat('collapse-whook-', hookpath)\" />
            </xsl:attribute>
            <!-- The <a> tag no longer needs an ID -->
            <a>
              <!-- Display the original hookpath -->
              <xsl:value-of select=\"hookpath\" /> <span class=\"badge bg-secondary-subtle text-secondary-emphasis rounded-pill ms-2\"><xsl:value-of select=\"count\" /></span>
            </a>
          </button>
        </h3>
        <div class=\"accordion-collapse collapse\">
          <xsl:attribute name=\"id\">
            <!-- Use raw hookpath for collapse ID -->
            <xsl:value-of select=\"concat('collapse-whook-', hookpath)\" />
          </xsl:attribute>
          <!-- aria-labelledby should point to the h3's ID -->
          <xsl:attribute name=\"aria-labelledby\">
            <xsl:value-of select=\"concat('whook-', hookpath)\" />
          </xsl:attribute>
          <div class=\"accordion-body\">
            <xsl:for-each select=\"workflows\">
              <span class=\"badge bg-secondary-subtle border border-secondary-subtle text-secondary-emphasis rounded-pill me-2 mb-2\">
                <a href=\"#{wf_id}\" class=\"text-primary-emphasis text-decoration-none me-1 section-offset\" title=\"Jump to workflow details\">
                  <xsl:value-of select=\"wf_name\" />
                </a>
                <a href=\"{wf_url}\" target=\"_blank\" class=\"text-primary-emphasis text-decoration-none\" title=\"Open workflow in a new window\">
                  🔗
                </a>
              </span>
            </xsl:for-each>
          </div>
        </div>
      </div>
    </xsl:for-each>
  </div>
</section>
"
            },
            {
              "id": "2af68003-c9b9-4e60-8836-195da026ad2f",
              "name": "about",
              "type": "string",
              "value": "=<hr class=\"featurette-divider border-dark\" />
<section id=\"about\" class=\"container mt-3\">
  <h2 class=\"text-center mb-5\">About This Dashboard &amp; Related Templates</h2>
  <div class=\"row justify-content-center\">

    <!-- Eduard Section -->
    <div class=\"col-lg-3 text-center mb-4\">
      <img src=\"https://gravatar.com/avatar/a551e67c6fe7affd5f882a527dee154bb6c3ac90cf878326accb3fb3ec77c8a6?r=pg&amp;d=retro&amp;size=200\" alt=\"Eduard\" class=\"rounded-circle mb-3\" width=\"140\" height=\"140\" />
      <h3 class=\"fw-normal\">Eduard</h3>
      <p><a class=\"btn btn-warning\" href=\"https://n8n.io/creators/eduard/\" target=\"_blank\">More templates</a></p>
      <p><a class=\"btn btn-outline-primary\" href=\"https://www.linkedin.com/in/parsadanyan/\" target=\"_blank\">LinkedIn</a></p>
    </div>

    <!-- Original Article Card (Text Restored) -->
    <div class=\"col-lg-3 text-center mb-4\">
      <div class=\"card shadow-sm h-100\">
         <div class=\"card-img-container\">
            <img src=\"https://blog.n8n.io/content/images/size/w800/2023/09/gg.png\" class=\"card-img-top\" alt=\"How to work with XML and SQL using n8n\" />
         </div>
        <div class=\"card-body d-flex flex-column\">
          <!-- Restored original title -->
          <h5 class=\"card-title\">Read the article to find out more!</h5>
          <p class=\"card-text\">This dashboard was created using XML template language (XSLT) in n8n.</p>
          <a href=\"https://blog.n8n.io/sql-xml/#how-to-deliver-the-xml-file\" class=\"btn btn-primary mt-auto\" target=\"_blank\">Read Article</a>
        </div>
      </div>
    </div>

    <!-- New Card 1: Docsify Template (Text Expanded) -->
    <div class=\"col-lg-3 text-center mb-4\">
      <div class=\"card shadow-sm h-100\">
        <div class=\"card-body d-flex flex-column\">
          <h5 class=\"card-title\">📚 Auto-generate documentation for n8n workflows with GPT and Docsify</h5>
          <p class=\"card-subtitle mb-2 text-muted\">Creates a dynamic Docsify site with GPT-powered descriptions and Mermaid diagrams.</p>
          <!-- Added descriptive text -->
          <p class=\"card-text\">Features live editing, tag filtering, and automated documentation updates for your n8n instance.</p>
          <a href=\"https://n8n.io/workflows/2669-auto-generate-documentation-for-n8n-workflows-with-gpt-and-docsify/\" class=\"btn btn-primary mt-auto\" target=\"_blank\">View Template</a>
        </div>
      </div>
    </div>

    <!-- New Card 2: Mermaid Template (Text Expanded) -->
    <div class=\"col-lg-3 text-center mb-4\">
      <div class=\"card shadow-sm h-100\">
        <div class=\"card-body d-flex flex-column\">
          <h5 class=\"card-title\">🔍 Visualize Your n8n Workflows with Mermaid.js!</h5>
           <p class=\"card-subtitle mb-2 text-muted\">Generates interactive workflow flowcharts using Mermaid.js and Bootstrap.</p>
           <!-- Added descriptive text -->
           <p class=\"card-text\">Instantly visualize structures with custom shapes and direct links to workflows, perfect for documentation.</p>
          <a href=\"https://n8n.io/workflows/2378-visualize-your-n8n-workflows-with-mermaidjs/\" class=\"btn btn-primary mt-auto\" target=\"_blank\">View Template</a>
        </div>
      </div>
    </div>

  </div> <!-- End row -->
</section>
"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "3555218e-8df2-4ae8-9482-2c8ec99798c0",
      "name": "Sort-workflows",
      "type": "n8n-nodes-base.sort",
      "position": [
        2080,
        640
      ],
      "parameters": {
        "options": {},
        "sortFieldsUi": {
          "sortField": [
            {
              "order": "descending",
              "fieldName": "wf_stats.wf_updated"
            },
            {
              "fieldName": "wf_stats.wf_name"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2d893970-825e-4842-811f-7e7a24dd3bac",
      "name": "Sort-nodes",
      "type": "n8n-nodes-base.sort",
      "position": [
        2080,
        800
      ],
      "parameters": {
        "options": {},
        "sortFieldsUi": {
          "sortField": [
            {
              "order": "descending",
              "fieldName": "count"
            },
            {
              "fieldName": "node"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c197f00e-d147-45af-b121-a70d28912a7f",
      "name": "Sort-tags",
      "type": "n8n-nodes-base.sort",
      "position": [
        2080,
        960
      ],
      "parameters": {
        "options": {},
        "sortFieldsUi": {
          "sortField": [
            {
              "order": "descending",
              "fieldName": "count"
            },
            {
              "fieldName": "tag"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "4f28a9f6-b67e-42d8-8843-480803932c27",
      "name": "Aggregate-workflows",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        2260,
        640
      ],
      "parameters": {
        "options": {},
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "wf_stats"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "f4521a5c-8cc3-4831-90e2-1a1fda06fdac",
      "name": "Aggregate-nodes",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        2260,
        800
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "nodes-section"
      },
      "typeVersion": 1
    },
    {
      "id": "ae5040f7-4ae3-41e7-9afc-ebb625d303e7",
      "name": "Aggregate-tags",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        2260,
        960
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "tags-section"
      },
      "typeVersion": 1
    },
    {
      "id": "69a22d56-3b4e-4d5d-b351-3c787f23e9c9",
      "name": "n8n-get-workflows",
      "type": "n8n-nodes-base.n8n",
      "position": [
        1260,
        640
      ],
      "parameters": {
        "filters": {},
        "requestOptions": {}
      },
      "credentials": {
        "n8nApi": {
          "id": "45",
          "name": "n8n account 4"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "35564537-0053-4cdb-a05d-153ad4825393",
      "name": "Prepare JSON object",
      "type": "n8n-nodes-base.executeWorkflow",
      "position": [
        1260,
        1600
      ],
      "parameters": {
        "options": {},
        "workflowId": "={{ $workflow.id }}"
      },
      "typeVersion": 1
    },
    {
      "id": "9fd045f1-7126-4611-b26d-c45139429c6b",
      "name": "get-nodes-via-jmespath",
      "type": "n8n-nodes-base.set",
      "position": [
        1460,
        640
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "51f83719-066f-4231-a418-ba64a3b5b831",
              "name": "nodes_array",
              "type": "array",
              "value": "={{$jmespath($json,'nodes[*].type').map(item => (item.split('.').pop().toUpperCase() ))}}"
            },
            {
              "id": "bbc40849-66a7-4583-8c2c-ac590be59e38",
              "name": "tags_array",
              "type": "array",
              "value": "={{$jmespath($json,'tags[*].name')}}"
            },
            {
              "id": "08064cc3-f34e-4f05-9975-726378fe63ae",
              "name": "instance_url",
              "type": "string",
              "value": "={{$env[\"N8N_PROTOCOL\"]}}://{{$env[\"N8N_HOST\"]}}"
            },
            {
              "id": "1fdb9640-b628-4e13-9e4c-fef19cae7611",
              "name": "webhook_paths_array",
              "type": "array",
              "value": "={{ $jmespath($json, `nodes[?type=='n8n-nodes-base.webhook'].parameters.path | [?@]`) }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.3
    },
    {
      "id": "45723a66-03be-4be7-ae4a-978adb5b7e7b",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        960,
        1280
      ],
      "parameters": {
        "color": 6,
        "width": 1301.92628220859,
        "height": 1000.0640426993867,
        "content": "## Additional section to create a standalone dashboard via XLM templates
### This section is not required if you only need a JSON

### *IMPORTANT!*
### This webhook is not protected. Everyone who knows the URL endpoint can get access to the Dashboard. Please consider adding authentication.

1. `Request HTML dashboard` node runs that main section of the workflow
2. It converts the JSON into an XML structure
3. A final HTML page is created with the link to an XML stylesheet (this stylesheet controls the look of the dashboard)
4. The resulting page is returned via `Respond to Webhook` node"
      },
      "typeVersion": 1
    },
    {
      "id": "b17fbec5-03e2-4836-8704-6b31cdf92a5b",
      "name": "Request HTML dashboard",
      "type": "n8n-nodes-base.webhook",
      "position": [
        1060,
        1600
      ],
      "webhookId": "fb550a01-12f2-4709-ba2d-f71197b68340",
      "parameters": {
        "path": "fb550a01-12f2-4709-ba2d-f71197b68340",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "70fd1bbb-24e2-4fde-b054-6319120a7ac4",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        960,
        940
      ],
      "parameters": {
        "color": 3,
        "width": 663.915516288839,
        "height": 251.8866653838499,
        "content": "## IMPORTANT NOTE FOR CLOUD USERS
### Since the cloud version doesn't support environmental variables, please make the following changes:

1. **get-nodes-via-jmespath** node. Update the `instance_url` variable: enter your n8n URL instead of `{{$env[\"N8N_PROTOCOL\"]}}://{{$env[\"N8N_HOST\"]}}`
2. **Create HTML** node. Please provide the n8n instance URL instead of `{{ $env.WEBHOOK_URL }}`"
      },
      "typeVersion": 1
    },
    {
      "id": "36288776-5f67-40fd-872f-0eeac0dd03b0",
      "name": "Request xsl template",
      "type": "n8n-nodes-base.webhook",
      "position": [
        1100,
        2120
      ],
      "webhookId": "73a91e4d-143d-4168-9efb-6c56f2258aec",
      "parameters": {
        "path": "73a91e4d-143d-4168-9efb-6c56f2258aec/dashboard.xsl",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "cda6fce6-0b0a-4fdf-b50c-b5bd874e43a0",
      "name": "Final-json",
      "type": "n8n-nodes-base.merge",
      "position": [
        2560,
        540
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition",
        "numberInputs": 5
      },
      "typeVersion": 3.1
    },
    {
      "id": "1a7acbda-0eb4-4d1a-b458-02457ee82a9b",
      "name": "webhook-section",
      "type": "n8n-nodes-base.code",
      "position": [
        1900,
        1140
      ],
      "parameters": {
        "jsCode": "// Initialize an empty object to hold the mapping between webhook paths and workflows
const webhookMap = {};

// Iterate over each workflow item passed from the previous node
$input.all().forEach(item => {
  // --- Extract Data ---
  // Ensure wf_stats exists in the item's JSON payload
  if (!item.json || !item.json.wf_stats) {
    console.warn(\"Skipping item due to missing json or wf_stats:\", JSON.stringify(item));
    return; // Skip this item if wf_stats is missing
  }

  const { wf_stats } = item.json;
  // Destructure the necessary fields from wf_stats
  // Use default values for safety
  const { wf_whooks, wf_name = 'Unknown Workflow', wf_url = '', wf_id = 'unknown-' + Date.now() } = wf_stats;

  // --- Process Webhooks ---
  // Check if wf_whooks exists and is an array with items
  if (Array.isArray(wf_whooks) && wf_whooks.length > 0) {
    const workflowInfo = { wf_name, wf_url, wf_id }; // Prepare workflow details object

    // For each webhook path associated with this workflow
    wf_whooks.forEach(hookpath => {
      // Ensure hookpath is a non-empty string before processing
      if (typeof hookpath === 'string' && hookpath.trim() !== '') {
        const cleanHookpath = hookpath.trim(); // Use trimmed path

        // If this webhook path hasn't been seen before, initialize it in the map
        if (!webhookMap[cleanHookpath]) {
          webhookMap[cleanHookpath] = [workflowInfo];
        } else {
          // If the path exists, add this workflow's info to its list
          // (Avoid adding duplicates if the same workflow info is already there for this path)
          if (!webhookMap[cleanHookpath].some(wf => wf.wf_id === wf_id)) {
             webhookMap[cleanHookpath].push(workflowInfo);
          }
        }
      } else {
        // Optional: Log if a non-string or empty path was found in the array
         console.warn(`Invalid hookpath found in wf_whooks for workflow ${wf_id}:`, hookpath);
      }
    });
  }
  // Workflows without any webhooks (empty wf_whooks array) will be skipped naturally
});

// --- Format Output ---
// Convert the map ( { path: [workflows] } ) into an array of items for n8n output
const result = Object.keys(webhookMap).map(hookpath => ({
  json: {
    hookpath: hookpath, // The webhook path
    count: webhookMap[hookpath].length, // How many workflows use this path
    workflows: webhookMap[hookpath] // The list of { wf_name, wf_url, wf_id } objects
  }
}));

// Return the final array
return result;
"
      },
      "typeVersion": 2
    },
    {
      "id": "0cfcd940-f000-47ce-8e46-36dab4068acb",
      "name": "Sort-whooks",
      "type": "n8n-nodes-base.sort",
      "position": [
        2080,
        1140
      ],
      "parameters": {
        "options": {},
        "sortFieldsUi": {
          "sortField": [
            {
              "order": "descending",
              "fieldName": "count"
            },
            {
              "fieldName": "hookpath"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "099ecc9b-ca8d-4ccb-aa64-30a563f27aeb",
      "name": "Aggregate-whooks",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        2260,
        1140
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "whooks-section"
      },
      "typeVersion": 1
    },
    {
      "id": "a01a78e6-0957-4602-a558-430b17000452",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        600,
        1580
      ],
      "parameters": {
        "width": 620,
        "content": "## &#x200B;
# USE THIS WEBHOOK -->"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "pinData": {},
  "settings": {
    "callerPolicy": "workflowsFromSameOwner",
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "saveDataSuccessExecution": "all"
  },
  "versionId": "3fc1a529-eb6e-4f8a-9d7f-cb8e21e782a1",
  "connections": {
    "Sort-tags": {
      "main": [
        [
          {
            "node": "Aggregate-tags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sort-nodes": {
      "main": [
        [
          {
            "node": "Aggregate-nodes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create HTML": {
      "main": [
        [
          {
            "node": "Move Binary Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sort-whooks": {
      "main": [
        [
          {
            "node": "Aggregate-whooks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "tags-section": {
      "main": [
        [
          {
            "node": "Sort-tags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "nodes-section": {
      "main": [
        [
          {
            "node": "Sort-nodes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate-tags": {
      "main": [
        [
          {
            "node": "Final-json",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "Convert to XML": {
      "main": [
        [
          {
            "node": "Create HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Final template": {
      "main": [
        [
          {
            "node": "Respond to Webhook2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sort-workflows": {
      "main": [
        [
          {
            "node": "Aggregate-workflows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate-nodes": {
      "main": [
        [
          {
            "node": "Final-json",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "globals-section": {
      "main": [
        [
          {
            "node": "Final-json",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "webhook-section": {
      "main": [
        [
          {
            "node": "Sort-whooks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate-whooks": {
      "main": [
        [
          {
            "node": "Final-json",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "Move Binary Data": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Template elements": {
      "main": [
        [
          {
            "node": "Final template",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "n8n-get-workflows": {
      "main": [
        [
          {
            "node": "get-nodes-via-jmespath",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "workflows-section": {
      "main": [
        [
          {
            "node": "nodes-section",
            "type": "main",
            "index": 0
          },
          {
            "node": "tags-section",
            "type": "main",
            "index": 0
          },
          {
            "node": "globals-section",
            "type": "main",
            "index": 0
          },
          {
            "node": "Sort-workflows",
            "type": "main",
            "index": 0
          },
          {
            "node": "webhook-section",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate-workflows": {
      "main": [
        [
          {
            "node": "Final-json",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Prepare JSON object": {
      "main": [
        [
          {
            "node": "Convert to XML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request xsl template": {
      "main": [
        [
          {
            "node": "Template elements",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request HTML dashboard": {
      "main": [
        [
          {
            "node": "Prepare JSON object",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get-nodes-via-jmespath": {
      "main": [
        [
          {
            "node": "workflows-section",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute Workflow Trigger": {
      "main": [
        [
          {
            "node": "n8n-get-workflows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \"Test workflow\"": {
      "main": [
        [
          {
            "node": "n8n-get-workflows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

功能特点

  • 自动检测新邮件
  • AI智能内容分析
  • 自定义分类规则
  • 批量处理能力
  • 详细的处理日志

技术分析

节点类型及作用

  • Manualtrigger
  • Code
  • Set
  • Stickynote
  • Executeworkflowtrigger

复杂度评估

配置难度:
★★★★☆
维护难度:
★★☆☆☆
扩展性:
★★★★☆

实施指南

前置条件

  • 有效的Gmail账户
  • n8n平台访问权限
  • Google API凭证
  • AI分类服务订阅

配置步骤

  1. 在n8n中导入工作流JSON文件
  2. 配置Gmail节点的认证信息
  3. 设置AI分类器的API密钥
  4. 自定义分类规则和标签映射
  5. 测试工作流执行
  6. 配置定时触发器(可选)

关键参数

参数名称 默认值 说明
maxEmails 50 单次处理的最大邮件数量
confidenceThreshold 0.8 分类置信度阈值
autoLabel true 是否自动添加标签

最佳实践

优化建议

  • 定期更新AI分类模型以提高准确性
  • 根据邮件量调整处理批次大小
  • 设置合理的分类置信度阈值
  • 定期清理过期的分类规则

安全注意事项

  • 妥善保管API密钥和认证信息
  • 限制工作流的访问权限
  • 定期审查处理日志
  • 启用双因素认证保护Gmail账户

性能优化

  • 使用增量处理减少重复工作
  • 缓存频繁访问的数据
  • 并行处理多个邮件分类任务
  • 监控系统资源使用情况

故障排除

常见问题

邮件未被正确分类

检查AI分类器的置信度阈值设置,适当降低阈值或更新训练数据。

Gmail认证失败

确认Google API凭证有效且具有正确的权限范围,重新进行OAuth授权。

调试技巧

  • 启用详细日志记录查看每个步骤的执行情况
  • 使用测试邮件验证分类逻辑
  • 检查网络连接和API服务状态
  • 逐步执行工作流定位问题节点

错误处理

工作流包含以下错误处理机制:

  • 网络超时自动重试(最多3次)
  • API错误记录和告警
  • 处理失败邮件的隔离机制
  • 异常情况下的回滚操作