{
"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\nconst nodeToWorkflowsMap = {};\n\n// Iterate over each workflow in the input\n$input.all().forEach(item => {\n const { wf_stats } = item.json;\n const { nodes_unique, wf_name, wf_url, wf_id } = wf_stats;\n\n // For each unique node in the workflow, update the mapping\n nodes_unique.forEach(node => {\n if (!nodeToWorkflowsMap[node]) {\n // If the node has not been added to the map, initialize it with the current workflow\n nodeToWorkflowsMap[node] = [{ wf_name, wf_url, wf_id }];\n } else {\n // If the node is already in the map, append the current workflow to its list\n nodeToWorkflowsMap[node].push({ wf_name, wf_url, wf_id });\n }\n });\n});\n\n// Convert the map into an array format suitable for n8n's output\nconst result = Object.keys(nodeToWorkflowsMap).map(node => ({\n json: {\n node,\n count: nodeToWorkflowsMap[node].length,\n workflows: nodeToWorkflowsMap[node]\n }\n}));\n\nreturn 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)],\n nodes_count_total:$json.nodes_array.length,\n nodes_count_uniq :[...new Set($json.nodes_array)].length,\n wf_created :DateTime.fromISO($json.createdAt).toFormat('yyyy-MM-dd HH:mm:ss'),\n wf_updated :DateTime.fromISO($json.updatedAt).toFormat('yyyy-MM-dd HH:mm:ss'),\n wf_name :$json.name,\n wf_id :`wf-${$json.id}`,\n wf_url :`${$json.instance_url}/workflow/${$json.id}` || \"\",\n wf_active :$json.active,\n wf_trigcount :$json.triggerCount,\n wf_tags :$json.tags_array,\n wf_whooks :$json.webhook_paths_array\n\n} }}"
}
]
}
},
"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\n* `globals` - general information (# of workflows, active workflows, total trigger count)\n* `wf_stats` - summary per workflow (number or nodes, unique nodes, list of nodes and tags)\n* `nodes-section` - summary per node (number of workflows that use a node and their URLs)\n* `tags-section` - summary per tag (number of workflows that use a node and their URLs)\n* `webhook-section` - lists all webhook endpoints of the instance and shows the workflow URLs\n\n### You can use this JSON in BI tools to create a custom dashboard\n\n## Learn JS tips & tricks\n### Instead of just using one Code node, the workflow contains several nodes with useful advanced tricks.\n\n### JMESPath\n* Make a simple array of strings out of a complex array: `$jmespath($json,'nodes[*].type')`\n* Extract values based on condition: `$jmespath($input.all(),'[?json.wf_stats.wf_active == `true`]')`\n\n### Map and arrow functions\n* Perform operation on each array element: `.map(item => (item.split('.').pop().toUpperCase() ))`\n* Calculate sum of values from an array: `.reduce((accumulator, currentValue) => accumulator + currentValue, 0)`\n\n### Create an array with only unique values\n* `[...new Set($json.nodes_array)]`\n\n### Date-time conversions with the Luxon library:\n* `DateTime.fromISO($json.createdAt).toFormat('yyyy-MM-dd HH:mm:ss')`\n\n### Template literals (Template strings) for creating strings in JS\n* `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\nconst tagToWorkflowsMap = {};\n\n// Iterate over each workflow in the input\n$input.all().forEach(item => {\n const { wf_stats } = item.json;\n // Destructure wf_url along with other properties\n const { wf_tags, wf_name, wf_id, wf_url } = wf_stats;\n\n // Check if the workflow has tags\n if (wf_tags && wf_tags.length > 0) {\n // For each tag in the workflow, update the mapping\n wf_tags.forEach(tag => {\n if (!tagToWorkflowsMap[tag]) {\n // If the tag has not been added to the map, initialize it with the current workflow including wf_url\n tagToWorkflowsMap[tag] = [{ wf_name, wf_id, wf_url }];\n } else {\n // If the tag is already in the map, append the current workflow to its list including wf_url\n tagToWorkflowsMap[tag].push({ wf_name, wf_id, wf_url });\n }\n });\n } else {\n // Handle workflows with no tags, categorizing them under a 'No Tags' category\n const noTagKey = 'No Tags'; // or any other placeholder you prefer\n if (!tagToWorkflowsMap[noTagKey]) {\n // Initialize with the current workflow including wf_url\n tagToWorkflowsMap[noTagKey] = [{ wf_name, wf_id, wf_url }];\n } else {\n // Append the current workflow to its list including wf_url\n tagToWorkflowsMap[noTagKey].push({ wf_name, wf_id, wf_url });\n }\n }\n});\n\n// Convert the map into an array format suitable for n8n's output\nconst result = Object.keys(tagToWorkflowsMap).map(tag => ({\n json: {\n tag,\n count: tagToWorkflowsMap[tag].length,\n workflows: tagToWorkflowsMap[tag] // This now contains objects with wf_name, wf_id, and wf_url\n }\n}));\n\nreturn 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,\n global_active : $jmespath($input.all(),'[?json.wf_stats.wf_active == `true`]').length,\n 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": "\n\n\n{{ $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\n## This webhook is needed to comply with the CORS policy of modern browsers.\n### It generates XML template and serves it using your n8n URL\n\nXSLT template is created with 2 Set nodes:\n1. `Template elements` node defines each section of the Dashboard\n2. `Final template` node puts everything together\n3. 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": "= 📊 ✅ ⚡ n8n Workflow Dashboard
\nOverview
\n Total Workflows
\n Active Workflows
\n Triggers Count
\n Workflows
\n \n \n
\n Nodes
\n \nWebhooks
\n \n
This dashboard was created using XML template language (XSLT) in n8n.
\n Read Article\nCreates a dynamic Docsify site with GPT-powered descriptions and Mermaid diagrams.
\n \nFeatures live editing, tag filtering, and automated documentation updates for your n8n instance.
\n View Template\nGenerates interactive workflow flowcharts using Mermaid.js and Bootstrap.
\n \nInstantly visualize structures with custom shapes and direct links to workflows, perfect for documentation.
\n View Template\n