This workflow corresponds to n8n.io template #13545 — we link there as the canonical source.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "c430dbe3-a6b8-4e18-8cf8-a5e42a495f46",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
5856,
1920
],
"parameters": {},
"typeVersion": 1
},
{
"id": "a63f4cbf-2477-4662-b651-af1c14332d32",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
4528,
1408
],
"parameters": {
"width": 1200,
"height": 1184,
"content": "# Hosting Static HTML KPI Dashboard with CustomJS\n\n[KPI Google Sheet](https://docs.google.com/spreadsheets/d/10mj6hngkPTg1_A6bVNn8QfKujap6j-Lrt_LwAkY2d7Q/edit?usp=sharing) \n\nThis workflow automatically generates a weekly KPI dashboard from Google Sheets data and hosts it as a live static HTML page using [CustomJS](https://www.customjs.space).\n\n---\n\n## Setup\n- Requires a **self-hosted n8n instance** and a **CustomJS API key**.\n- Google Sheets should be **publicly accessible** or shared with a service account for n8n access.\n- Optional: Additional JSON/CSV sources can be integrated.\n\n---\n\n## How it works\n1 **Manual Trigger** \n- Run the workflow on demand or schedule weekly.\n\n\n2 **Load Data from Google Sheet** \n- Pull KPI metrics such as Visitors, Leads, Demo Booked, Proposal Sent, Won. \n- Aggregate weekly or per channel as needed.\n\n\n3 **Prepare Structured JSON** \n- Transform sheet rows into a clean JSON structure ready for the dashboard. \n- Example structure: \n```json\n[\n {\"Date\":\"2026-02-01\",\"Channel\":\"Google Ads\",\"Visitors\":1200,\"Leads\":95,\"Demo Booked\":40,\"Proposal Sent\":22,\"Won\":9},\n {\"Date\":\"2026-02-01\",\"Channel\":\"LinkedIn\",\"Visitors\":800,\"Leads\":70,\"Demo Booked\":28,\"Proposal Sent\":16,\"Won\":7}\n]\n```\n\n4 **Generate HTML Dashboard** \n- Feed JSON into a CustomJS HTML template node.\n- Automatically builds KPI cards, tables, and charts using Chart.js.\n\n\n5 **Host Static HTML**\n- CustomJS deploys the HTML page instantly.\n- Optional: Connect a custom domain in one click.\n- Each weekly update overwrites the previous dashboard.\n\n\n6 **Optional Enhancements**\n- Generate QR codes for the live dashboard link.\n- Include multiple charts, multiple sheets, or historical trends."
},
"typeVersion": 1
},
{
"id": "63aad3dc-26fd-4427-ab40-762761c6b1fe",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
5776,
1824
],
"parameters": {
"color": 7,
"width": 960,
"height": 416,
"content": "## Collect sheet data and convert to json"
},
"typeVersion": 1
},
{
"id": "dd1a3ad4-505f-4222-a05c-e66bd9332c99",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
6848,
1824
],
"parameters": {
"color": 7,
"width": 544,
"height": 416,
"content": "## Build and publish HTML"
},
"typeVersion": 1
},
{
"id": "63ab98d5-413e-4c9f-9334-742c5a87c3b4",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
7504,
1840
],
"parameters": {
"color": 7,
"width": 432,
"height": 400,
"content": "## Generate QR code"
},
"typeVersion": 1
},
{
"id": "3ceea1d1-37dd-48f3-a571-4a9bbbe02479",
"name": "HTML",
"type": "n8n-nodes-base.html",
"position": [
6960,
1968
],
"parameters": {
"html": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>KPI Dashboard</title>\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n\n<style>\nbody{\n font-family: system-ui, -apple-system, sans-serif;\n background:#f6f7fb;\n margin:0;\n padding:24px;\n color:#111;\n}\n.container{\n max-width:1100px;\n margin:auto;\n}\nh1{margin-bottom:4px;}\n.subtitle{color:#666;margin-bottom:30px;}\n\n.cards{\n display:grid;\n grid-template-columns:repeat(auto-fit,minmax(180px,1fr));\n gap:16px;\n margin-bottom:30px;\n}\n.card{\n background:white;\n padding:18px;\n border-radius:12px;\n box-shadow:0 2px 6px rgba(0,0,0,0.06);\n}\n.card .label{font-size:13px;color:#666}\n.card .value{font-size:24px;font-weight:700;margin-top:6px}\n\n.charts{\n display:grid;\n grid-template-columns:repeat(auto-fit, minmax(300px,1fr));\n gap:24px;\n margin-bottom:24px;\n}\n@media(max-width:900px){\n .charts{\n grid-template-columns:1fr;\n }\n}\ncanvas{\n max-width:100%;\n width:100%;\n height:280px;\n box-sizing:border-box;\n}\n\n.section{\n background:white;\n padding:18px;\n border-radius:12px;\n margin-bottom:24px;\n box-shadow:0 2px 6px rgba(0,0,0,0.06);\n}\n\ntable{\n width:100%;\n border-collapse:collapse;\n}\nth,td{\n padding:10px;\n border-bottom:1px solid #eee;\n text-align:left;\n font-size:14px;\n}\nth{color:#666;font-weight:600;}\n\n.footer{\n text-align:center;\n margin-top:30px;\n color:#999;\n font-size:12px;\n}\n</style>\n</head>\n\n<body>\n<div class=\"container\">\n\n<h1 id=\"title\"></h1>\n<div class=\"subtitle\" id=\"period\"></div>\n\n<div class=\"cards\" id=\"kpiCards\"></div>\n\n<div class=\"charts\">\n\n <div class=\"section\">\n <h3>Visitors by Channel</h3>\n <canvas id=\"visitorsChart\"></canvas>\n </div>\n\n <div class=\"section\">\n <h3>Funnel Conversion</h3>\n <canvas id=\"funnelChart\"></canvas>\n </div>\n\n</div>\n\n<div class=\"section\">\n <h3>Channel Breakdown</h3>\n <table id=\"table\"></table>\n</div>\n\n<div class=\"footer\">\nGenerated automatically \u2022 KPI Dashboard\n</div>\n\n</div>\n\n<script>\n/* ===== JSON Daten ===== */\nconst report = {\n reportTitle: \"Weekly Sales Funnel Report\",\n period: \"Week ending 2026-02-01\",\n data: {{ JSON.stringify($json.data) }}\n};\n\n/* ===== Totals berechnen ===== */\nconst totals = report.data.reduce((a,c)=>({\n visitors: a.visitors + (Number(c[\"Visitors\"]) || 0),\n leads: a.leads + (Number(c[\"Leads\"]) || 0),\n demo: a.demo + (Number(c[\"Demo Booked\"]) || 0),\n proposal: a.proposal + (Number(c[\"Proposal Sent\"]) || 0),\n won: a.won + (Number(c[\"Won\"]) || 0)\n}), {visitors:0, leads:0, demo:0, proposal:0, won:0});\n\n/* ===== KPIs ===== */\nconst leadRate = totals.visitors\n ? ((totals.leads / totals.visitors)*100).toFixed(1)\n : \"0.0\";\n\nconst closeRate = totals.leads\n ? ((totals.won / totals.leads)*100).toFixed(1)\n : \"0.0\";\n\n/* ===== HEADER ===== */\ndocument.getElementById(\"title\").textContent = report.reportTitle;\ndocument.getElementById(\"period\").textContent = report.period;\n\n/* ===== KPI CARDS ===== */\nconst cards = [\n [\"Visitors\", totals.visitors],\n [\"Leads\", totals.leads],\n [\"Deals Won\", totals.won],\n [\"Lead Rate\", leadRate+\"%\"],\n [\"Close Rate\", closeRate+\"%\"]\n];\ndocument.getElementById(\"kpiCards\").innerHTML = cards.map(c=>`\n<div class=\"card\">\n <div class=\"label\">${c[0]}</div>\n <div class=\"value\">${c[1]}</div>\n</div>`).join(\"\");\n\n/* ===== TABLE ===== */\ndocument.getElementById(\"table\").innerHTML = `\n<tr>\n <th>Channel</th>\n <th>Visitors</th>\n <th>Leads</th>\n <th>Demo</th>\n <th>Proposal</th>\n <th>Won</th>\n</tr>` + report.data.map(r=>`\n<tr>\n <td>${r[\"Channel\"]}</td>\n <td>${r[\"Visitors\"]}</td>\n <td>${r[\"Leads\"]}</td>\n <td>${r[\"Demo Booked\"]}</td>\n <td>${r[\"Proposal Sent\"]}</td>\n <td>${r[\"Won\"]}</td>\n</tr>`).join(\"\");\n\n/* ===== VISITORS CHART ===== */\nnew Chart(document.getElementById('visitorsChart'),{\n type:'bar',\n data:{\n labels: report.data.map(r=>r[\"Channel\"]),\n datasets:[{\n label:'Visitors',\n data: report.data.map(r=>Number(r[\"Visitors\"]) || 0),\n backgroundColor:['#4f46e5','#10b981','#f59e0b']\n }]\n },\n options:{\n responsive:true,\n plugins:{legend:{display:false}},\n layout:{padding:8}\n }\n});\n\n/* ===== FUNNEL CHART ===== */\nnew Chart(document.getElementById('funnelChart'),{\n type:'line',\n data:{\n labels:[\"Visitors\",\"Leads\",\"Demo\",\"Proposal\",\"Won\"],\n datasets:[{\n label:'Total Funnel',\n data:[\n totals.visitors,\n totals.leads,\n totals.demo,\n totals.proposal,\n totals.won\n ],\n borderColor:'#4f46e5',\n backgroundColor:'rgba(79,70,229,0.2)',\n tension:0.4,\n fill:true\n }]\n },\n options:{\n responsive:true,\n plugins:{legend:{display:false}},\n layout:{padding:8}\n }\n});\n</script>\n</body>\n</html>"
},
"typeVersion": 1.2
},
{
"id": "af3d762b-f5c4-4b39-8f0a-8bf275e53dc7",
"name": "Get Data From Sheet",
"type": "n8n-nodes-base.httpRequest",
"position": [
6096,
1968
],
"parameters": {
"url": "https://docs.google.com/spreadsheets/d/10mj6hngkPTg1_A6bVNn8QfKujap6j-Lrt_LwAkY2d7Q/gviz/tq?tqx=out:csv&sheet=Sheet1",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
}
},
"typeVersion": 4.4
},
{
"id": "effc82e4-f01b-4984-8b4b-5353d203f4f7",
"name": "Convert HTML to PDF",
"type": "@custom-js/n8n-nodes-pdf-toolkit-v2.pdfToolkit",
"position": [
7632,
1968
],
"parameters": {
"html": "=<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>KPI Dashbaord</title>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js\"></script>\n<style>\nbody {\n font-family: system-ui, -apple-system, sans-serif;\n background: #f6f7fb;\n margin: 0;\n padding: 24px;\n color: #111;\n}\n.container {\n max-width: 400px;\n margin: auto;\n background: #fff;\n padding: 24px;\n border-radius: 12px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n}\nh1 {\n margin-bottom: 24px;\n}\n#qrcode {\n margin-top: 24px;\n}\n</style>\n</head>\n<body>\n\n<div class=\"container\">\n <h1>KPI Dashboard</h1>\n <div id=\"qrcode\"></div>\n</div>\n\n<script>\nconst url = \"{{ $json.htmlFileUrl }}\";\n\nnew QRCode(document.getElementById(\"qrcode\"), {\n text: url,\n width: 200,\n height: 200,\n colorDark: \"#4f46e5\",\n colorLight: \"#ffffff\",\n correctLevel: QRCode.CorrectLevel.H\n});\n</script>\n\n</body>\n</html> ",
"operation": "htmlToPdf",
"pdfHeightMm": 120
},
"credentials": {
"customJsApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "9ec01a64-4ba6-4df0-a3fc-8d4dc711af49",
"name": "Extract from File",
"type": "n8n-nodes-base.extractFromFile",
"position": [
6320,
1968
],
"parameters": {
"options": {}
},
"typeVersion": 1.1
},
{
"id": "c05037bf-f6cb-4238-b45d-17f7bd3c3304",
"name": "Aggregate",
"type": "n8n-nodes-base.aggregate",
"position": [
6512,
1968
],
"parameters": {
"options": {},
"aggregate": "aggregateAllItemData"
},
"typeVersion": 1
},
{
"id": "64a36470-99ac-45a7-86c5-6d40ca508615",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
5856,
2096
],
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"typeVersion": 1.3
},
{
"id": "95630953-76c3-4af6-80d9-4714c17715df",
"name": "Upsert HTML Page",
"type": "@custom-js/n8n-nodes-pdf-toolkit-v2.pdfToolkit",
"position": [
7200,
1968
],
"parameters": {
"pageName": "KPI Dashboard",
"resource": "page",
"operation": "upsert",
"htmlContent": "={{ $('HTML').item.json.html }}"
},
"credentials": {
"customJsApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
}
],
"connections": {
"HTML": {
"main": [
[
{
"node": "Upsert HTML Page",
"type": "main",
"index": 0
}
]
]
},
"Aggregate": {
"main": [
[
{
"node": "HTML",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Get Data From Sheet",
"type": "main",
"index": 0
}
]
]
},
"Upsert HTML Page": {
"main": [
[
{
"node": "Convert HTML to PDF",
"type": "main",
"index": 0
}
]
]
},
"Extract from File": {
"main": [
[
{
"node": "Aggregate",
"type": "main",
"index": 0
}
]
]
},
"Get Data From Sheet": {
"main": [
[
{
"node": "Extract from File",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Get Data From Sheet",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
customJsApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow demonstrates how to automatically generate a weekly KPI dashboard from Google Sheets and host it as a live static HTML page using CustomJS.
Source: https://n8n.io/workflows/13545/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This n8n template demonstrates how to download multiple PDF files from public URLs and merge them into a single PDF using the PDF Toolkit from www.customjs.space.
This workflow allows you to import any workflow from a file or another n8n instance and map the credentials easily. A multi-form setup guides you through the entire process At the beginning you have t
[n8n] Advanced URL Parsing and Shortening Workflow - Switchy.io Integration. Uses splitInBatches, stickyNote, httpRequest, html. Event-driven trigger; 56 nodes.
[](https://youtu.be/c7yCZhmMjtI)
This automation organizes your n8n workflows files into categorizes (Active, Template, Done, Archived) and uploads them directly to a categorized Google Drive folders. It is designed to help users man