This workflow corresponds to n8n.io template #15575 — 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 →
{
"id": "7ebD2fYHEFz7Qzku",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Custom Interactive Forms - Submission Handler",
"tags": [],
"nodes": [
{
"id": "06da9d06-b087-4cfd-a35f-ccaf59c9ca5b",
"name": "Read Form Config",
"type": "n8n-nodes-base.googleSheets",
"position": [
-208,
128
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1eAwCFWAICx3m9_ZCka0i0orrtMwcyRXbcunpgHFZc_k/edit#gid=0",
"cachedResultName": "Form_Config"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1eAwCFWAICx3m9_ZCka0i0orrtMwcyRXbcunpgHFZc_k",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1eAwCFWAICx3m9_ZCka0i0orrtMwcyRXbcunpgHFZc_k/edit?usp=drivesdk",
"cachedResultName": "Feedback Form"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4
},
{
"id": "1beea621-4227-42e8-beaf-8955a4d37c03",
"name": "Format Config",
"type": "n8n-nodes-base.code",
"position": [
16,
128
],
"parameters": {
"jsCode": "const items = $input.all();\nlet config = { title: '', subtitle: '', fields: [] };\nfor (const item of items) {\n const data = item.json;\n if (data.Setting_Type === 'Meta') {\n config[data.Key] = data.Value;\n }\n if (data.Setting_Type === 'Field') {\n config.fields.push({\n name: data.Key,\n type: data.Value,\n label: data.Label,\n options: data.Options ? data.Options.split(',').map(s => s.trim()) : [],\n required: data.Required === 'TRUE' || data.Required === 'true'\n });\n }\n}\nreturn [{ json: config }];"
},
"typeVersion": 2
},
{
"id": "990ee665-d30d-4717-a36a-8787a54f10ca",
"name": "Generate HTML",
"type": "n8n-nodes-base.code",
"position": [
288,
128
],
"parameters": {
"jsCode": "const inputData = $input.all()[0].json;\n\n// Handle both array and object inputs\nconst config = Array.isArray(inputData) ? inputData[0] : inputData;\n\n// Escape closing script tags to avoid breaking HTML\nconst safeConfig = JSON.stringify(config).replace(/<\\/script>/gi, '<\\\\/script>');\n\n// 1. Get your n8n instance's base URL from its environment variables\nconst n8nBaseUrl = $env.WEBHOOK_URL || \"http:localhost:5678\";\n// 2. Check if you clicked 'Execute workflow' (manual/test) or if it's running live\nconst isTestMode = n8nBaseUrl === \"http:localhost:5678\";\nconst webhookPrefix = isTestMode ? \"webhook-test\" : \"webhook\";\n// 3. Dynamically build the final URL\nconst URL = `${n8nBaseUrl}/${webhookPrefix}/dynamic-form-submit`;\n\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\n <script src=\"https://cdn.tailwindcss.com\"></script>\n <script defer src=\"https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js\"></script>\n\n <link\n href=\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap\"\n rel=\"stylesheet\"\n />\n\n <style>\n body {\n font-family: 'Plus Jakarta Sans', sans-serif;\n }\n\n [x-cloak] {\n display: none !important;\n }\n\n .aurora-bg {\n background: #070b14;\n position: fixed;\n inset: 0;\n z-index: 0;\n overflow: hidden;\n }\n\n .aurora-bg::before {\n content: '';\n position: absolute;\n top: -30%;\n left: -20%;\n width: 70%;\n height: 70%;\n background: radial-gradient(ellipse at center, rgba(99,102,241,0.25) 0%, transparent 70%);\n animation: drift1 18s ease-in-out infinite alternate;\n }\n\n .aurora-bg::after {\n content: '';\n position: absolute;\n bottom: -20%;\n right: -20%;\n width: 65%;\n height: 65%;\n background: radial-gradient(ellipse at center, rgba(168,85,247,0.2) 0%, transparent 70%);\n animation: drift2 22s ease-in-out infinite alternate;\n }\n\n .aurora-mid {\n position: absolute;\n top: 40%;\n left: 30%;\n width: 50%;\n height: 50%;\n background: radial-gradient(ellipse at center, rgba(56,189,248,0.1) 0%, transparent 65%);\n animation: drift3 26s ease-in-out infinite alternate;\n pointer-events: none;\n }\n\n @keyframes drift1 {\n from { transform: translate(0, 0) scale(1); }\n to { transform: translate(6%, 10%) scale(1.08); }\n }\n @keyframes drift2 {\n from { transform: translate(0, 0) scale(1); }\n to { transform: translate(-8%, -6%) scale(1.1); }\n }\n @keyframes drift3 {\n from { transform: translate(0, 0) scale(1); }\n to { transform: translate(5%, -8%) scale(0.95); }\n }\n\n .glass-card {\n background: rgba(255, 255, 255, 0.04);\n backdrop-filter: blur(28px);\n -webkit-backdrop-filter: blur(28px);\n border: 1px solid rgba(255, 255, 255, 0.1);\n }\n\n .field-input {\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid rgba(255, 255, 255, 0.12);\n color: #f1f5f9;\n transition: all 0.2s ease;\n }\n\n .field-input::placeholder {\n color: rgba(148, 163, 184, 0.5);\n }\n\n .field-input:focus {\n background: rgba(255, 255, 255, 0.09);\n border-color: rgba(99, 102, 241, 0.7);\n box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);\n outline: none;\n }\n\n .field-input option {\n background: #0f172a;\n color: #f1f5f9;\n }\n\n .submit-btn {\n background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);\n box-shadow: 0 8px 32px rgba(99, 102, 241, 0.35);\n transition: all 0.25s ease;\n }\n\n .submit-btn:hover:not(:disabled) {\n background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);\n box-shadow: 0 12px 40px rgba(99, 102, 241, 0.5);\n transform: translateY(-1px);\n }\n\n .submit-btn:active:not(:disabled) {\n transform: scale(0.98) translateY(0);\n }\n\n .label-text {\n color: rgba(148, 163, 184, 0.9);\n letter-spacing: 0.06em;\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n }\n\n .success-ring {\n box-shadow: 0 0 0 8px rgba(34, 197, 94, 0.1);\n }\n\n .icon-glow {\n box-shadow: 0 8px 32px rgba(99, 102, 241, 0.5);\n }\n\n .divider {\n border-color: rgba(255,255,255,0.08);\n }\n\n .select-arrow {\n color: rgba(148, 163, 184, 0.5);\n }\n </style>\n\n <title>${config.title || 'Dynamic Form Portal'}</title>\n</head>\n\n<body class=\"min-h-screen flex items-center justify-center p-4 md:p-8 overflow-x-hidden relative\" style=\"background:#070b14;\">\n\n <!-- Aurora Background -->\n <div class=\"aurora-bg\">\n <div class=\"aurora-mid\"></div>\n </div>\n\n <!-- Subtle grid overlay -->\n <div class=\"fixed inset-0 z-0 opacity-[0.03]\"\n style=\"background-image: linear-gradient(rgba(255,255,255,0.4) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.4) 1px, transparent 1px); background-size: 40px 40px;\">\n </div>\n\n <div\n x-data=\"formHandler()\"\n x-cloak\n class=\"max-w-lg w-full glass-card rounded-3xl p-8 md:p-10 z-10 relative\"\n >\n\n <!-- Header -->\n <div class=\"text-center mb-10\">\n\n <div class=\"inline-flex p-3.5 bg-indigo-600 rounded-2xl icon-glow mb-6\">\n <svg class=\"w-7 h-7 text-white\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n d=\"M13 10V3L4 14h7v7l9-11h-7z\"></path>\n </svg>\n </div>\n\n <h1 class=\"text-3xl md:text-[2.1rem] font-bold text-white tracking-tight mb-3 leading-tight\"\n x-text=\"config.title\"></h1>\n\n <p class=\"text-slate-400 text-base leading-relaxed\"\n x-text=\"config.description || config.subtitle\"></p>\n\n <hr class=\"divider border-t mt-8 mb-0\" />\n\n </div>\n\n <!-- Main Form -->\n <div x-show=\"!submitted\">\n\n <form @submit.prevent=\"submitForm\" class=\"space-y-5\">\n\n <template x-for=\"field in config.fields\" :key=\"field.name\">\n\n <div class=\"text-left\">\n\n <label class=\"block label-text mb-2 ml-0.5\">\n <span x-text=\"field.label\"></span>\n <span x-show=\"field.required\" class=\"text-indigo-400 ml-0.5\">*</span>\n </label>\n\n <!-- Select -->\n <template x-if=\"field.type === 'select'\">\n <div class=\"relative\">\n <select\n x-model=\"formData[field.name]\"\n :required=\"field.required\"\n class=\"field-input w-full px-5 py-3.5 rounded-xl appearance-none cursor-pointer pr-11\"\n >\n <option value=\"\" disabled>Choose an option\u2026</option>\n <template x-for=\"option in field.options\" :key=\"option\">\n <option :value=\"option\" x-text=\"option\"></option>\n </template>\n </select>\n <div class=\"select-arrow absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n </div>\n </div>\n </template>\n\n <!-- Textarea -->\n <template x-if=\"field.type === 'textarea'\">\n <textarea\n x-model=\"formData[field.name]\"\n :required=\"field.required\"\n rows=\"3\"\n placeholder=\"Enter details\u2026\"\n class=\"field-input w-full px-5 py-3.5 rounded-xl resize-none\"\n ></textarea>\n </template>\n\n <!-- Standard Input -->\n <template x-if=\"!['select', 'textarea'].includes(field.type)\">\n <input\n :type=\"field.type\"\n x-model=\"formData[field.name]\"\n :required=\"field.required\"\n placeholder=\"Enter value\u2026\"\n class=\"field-input w-full px-5 py-3.5 rounded-xl\"\n />\n </template>\n\n </div>\n\n </template>\n\n <!-- Submit Button -->\n <div class=\"pt-2\">\n <button\n type=\"submit\"\n :disabled=\"submitting\"\n class=\"submit-btn w-full disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold py-4 rounded-xl flex items-center justify-center gap-3\"\n >\n\n <span x-show=\"!submitting\" class=\"flex items-center gap-2\">\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 19l9 2-9-18-9 18 9-2zm0 0v-8\"></path>\n </svg>\n Submit Form\n </span>\n\n <span x-show=\"submitting\" class=\"flex items-center gap-2\">\n <svg class=\"animate-spin h-5 w-5 text-white\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n Processing\u2026\n </span>\n\n </button>\n </div>\n\n </form>\n\n </div>\n\n <!-- Success State -->\n <div x-show=\"submitted\" class=\"text-center py-8\">\n\n <div class=\"w-20 h-20 bg-green-500/10 success-ring rounded-full flex items-center justify-center mx-auto mb-7\">\n <svg class=\"w-10 h-10 text-green-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2.5\" d=\"M5 13l4 4L19 7\"></path>\n </svg>\n </div>\n\n <h2 class=\"text-2xl font-bold text-white mb-3\">All done!</h2>\n\n <p class=\"text-slate-400 text-base mb-8\">Your response has been submitted successfully.</p>\n\n <button\n @click=\"resetForm\"\n class=\"text-indigo-400 hover:text-indigo-300 font-semibold transition-colors flex items-center justify-center gap-2 mx-auto text-sm\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 19l-7-7m0 0l7-7m-7 7h18\"></path>\n </svg>\n Submit another response\n </button>\n\n </div>\n\n <!-- Error -->\n <div\n x-show=\"error\"\n class=\"mt-5 p-4 rounded-xl text-red-300 text-center text-sm font-medium\"\n style=\"background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2);\"\n x-text=\"error\"\n ></div>\n\n </div>\n\n <script>\n\n function formHandler() {\n\n const INJECTED_CONFIG = ${safeConfig};\n\n return {\n\n submitting: false,\n submitted: false,\n error: null,\n\n config: INJECTED_CONFIG,\n\n formData: {},\n\n init() {\n if (this.config.fields) {\n this.config.fields.forEach(field => {\n this.formData[field.name] = '';\n });\n }\n },\n\n async submitForm() {\n\n this.submitting = true;\n this.error = null;\n\n try {\n\n const response = await fetch('${URL}', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(this.formData)\n });\n\n if (!response.ok) throw new Error('Submission failed');\n\n this.submitted = true;\n\n } catch (err) {\n\n console.error(err);\n this.error = 'Submission failed. Please try again later.';\n\n } finally {\n\n this.submitting = false;\n\n }\n\n },\n\n resetForm() {\n\n this.submitted = false;\n\n Object.keys(this.formData).forEach(key => {\n this.formData[key] = '';\n });\n\n }\n\n };\n\n }\n\n </script>\n\n</body>\n</html>`;\n\nreturn [\n {\n json: {\n html\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "bb37a34e-3b27-408c-8237-2c1973bf77cb",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-480,
0
],
"parameters": {},
"typeVersion": 1
},
{
"id": "c6117291-f528-4e97-95a9-d30a11775055",
"name": "Upsert HTML Page",
"type": "@custom-js/n8n-nodes-pdf-toolkit-v2.pdfToolkit",
"position": [
448,
128
],
"parameters": {
"pageName": "={{ $('Format Config').item.json.title }}",
"resource": "page",
"operation": "upsert",
"htmlContent": "={{ $json.html }}"
},
"credentials": {
"customJsApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "0e1da581-715a-4b2b-8437-9e3c7bd0872e",
"name": "Webhook (POST)",
"type": "n8n-nodes-base.webhook",
"position": [
-368,
480
],
"parameters": {
"path": "dynamic-form-submit",
"options": {},
"httpMethod": "POST",
"responseMode": "lastNode"
},
"typeVersion": 1
},
{
"id": "7f44215b-4353-48c3-9f42-69ae936bbd28",
"name": "Add Timestamp",
"type": "n8n-nodes-base.code",
"position": [
96,
480
],
"parameters": {
"jsCode": "const body = $input.all()[0].json.body;\nbody.timestamp = new Date().toISOString();\nreturn [{ json: body }];"
},
"typeVersion": 2
},
{
"id": "c8467345-3c27-4e38-b587-34c138b83531",
"name": "Save Response",
"type": "n8n-nodes-base.googleSheets",
"position": [
576,
480
],
"parameters": {
"columns": {
"value": {
"name": "={{ $json.name }}",
"comments": "={{ $json.comments }}",
"timestamp": "={{ $json.timestamp }}",
"department": "={{ $json.department }}",
"feedback_date": "={{ $json.feedback_date }}"
},
"schema": [
{
"id": "name",
"type": "string",
"display": true,
"required": false,
"displayName": "name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "department",
"type": "string",
"display": true,
"required": false,
"displayName": "department",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "feedback_date",
"type": "string",
"display": true,
"required": false,
"displayName": "feedback_date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "comments",
"type": "string",
"display": true,
"required": false,
"displayName": "comments",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "timestamp",
"type": "string",
"display": true,
"required": false,
"displayName": "timestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 479587747,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1eAwCFWAICx3m9_ZCka0i0orrtMwcyRXbcunpgHFZc_k/edit#gid=479587747",
"cachedResultName": "Form_Responses"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1eAwCFWAICx3m9_ZCka0i0orrtMwcyRXbcunpgHFZc_k",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1eAwCFWAICx3m9_ZCka0i0orrtMwcyRXbcunpgHFZc_k/edit?usp=drivesdk",
"cachedResultName": "Feedback Form"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4
},
{
"id": "df5352fc-1a8a-45a3-9aba-8376ac1a912e",
"name": "Convert HTML to PDF",
"type": "@custom-js/n8n-nodes-pdf-toolkit-v2.pdfToolkit",
"position": [
736,
128
],
"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>{{$('Format Config').item.json.title}}</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": "5100eb03-e81a-4c73-b1e0-d75aa2cbcb59",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-544,
336
],
"parameters": {
"color": 5,
"width": 1504,
"height": 320,
"content": "## Handle Form responses\nReceives incoming form submissions via Webhook, adds a timestamp, and securely logs the data back into Google Sheets."
},
"typeVersion": 1
},
{
"id": "a4c762af-414f-4a5c-ba8d-2f26acd6cf98",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-544,
-128
],
"parameters": {
"color": 5,
"width": 720,
"height": 432,
"content": "## Feedback Form Configuration\nRetrieves the form field definitions from Google Sheets and parses them into a structured JSON format."
},
"typeVersion": 1
},
{
"id": "69f54556-3f4c-4352-8917-0c3489dad32f",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
208,
-128
],
"parameters": {
"color": 5,
"width": 400,
"height": 432,
"content": "## Generate & Host Form\nDynamically embeds the configuration into the HTML template and publishes the live page to CustomJs server."
},
"typeVersion": 1
},
{
"id": "773c2f78-50f6-481a-92c1-b27458298ef5",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
640,
-128
],
"parameters": {
"color": 5,
"width": 320,
"height": 432,
"content": "## Generate QR Code\nConverts the HTML or form link into a PDF/QR code to make sharing and access effortless for your end users."
},
"typeVersion": 1
},
{
"id": "a7a3a097-fd49-4d27-9bb3-8916c512e67d",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-480,
160
],
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"typeVersion": 1.3
},
{
"id": "70797e42-6bcb-4bcc-b56a-3259fb0acece",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-960,
-128
],
"parameters": {
"color": 5,
"width": 400,
"height": 784,
"content": "## Custom Interactive Forms\n\n### \ud83c\udf1f Overview\nThis workflow powers a fully dynamic, data-driven feedback system. \n\n### \ud83d\udcca Data Source & Integration\nIt automatically pulls form configuration settings (such as field types, dropdown options, and labels) directly from a Google Sheet and seamlessly injects them into a beautiful Tailwind CSS HTML template. \n\n### \u2699\ufe0f Core Processing\nThe workflow handles both generating the live form and listening for user submissions. \n\n### \ud83d\udd04 Triggers & Automation\nYou can trigger it manually whenever you update your form settings, or leave it on a schedule to ensure the live form is always perfectly synced with your spreadsheet.\n"
},
"typeVersion": 1
}
],
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "f3557e7d-734a-4a1e-ae31-e29768d40bad",
"connections": {
"Add Timestamp": {
"main": [
[
{
"node": "Save Response",
"type": "main",
"index": 0
}
]
]
},
"Format Config": {
"main": [
[
{
"node": "Generate HTML",
"type": "main",
"index": 0
}
]
]
},
"Generate HTML": {
"main": [
[
{
"node": "Upsert HTML Page",
"type": "main",
"index": 0
}
]
]
},
"Save Response": {
"main": [
[]
]
},
"Webhook (POST)": {
"main": [
[
{
"node": "Add Timestamp",
"type": "main",
"index": 0
}
]
]
},
"Read Form Config": {
"main": [
[
{
"node": "Format Config",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Read Form Config",
"type": "main",
"index": 0
}
]
]
},
"Upsert HTML Page": {
"main": [
[
{
"node": "Convert HTML to PDF",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Read Form Config",
"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.
customJsApigoogleSheetsOAuth2Api
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 and host dynamic interactive forms from Google Sheets using CustomJS.
Source: https://n8n.io/workflows/15575/ — 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 template is ideal for solo store owners, eCommerce marketers, automation beginners, or anyone using Shopify and Gmail who wants to recover lost revenue without coding.
PCN. Uses googleSheets, httpRequest, @n-octo-n/n8n-nodes-json-database, itemLists. Event-driven trigger; 60 nodes.
The workflow automates the process of gathering extensive keyword data for a "Main Keyword." It starts by reading initial parameters from a Google Sheets template, creates a new dedicated Google Sheet
🔥 March Sale – n8n Community Members Get ideoGener8r for Just $27! (Reg. $47) Use Coupon Code: (Valid until 3/31/2025 for n8n community members)
📄 Documentation: Notion Guide