This workflow corresponds to n8n.io template #11467 — we link there as the canonical source.
This workflow follows the Chainllm → Google Gemini Chat recipe pattern — see all workflows that pair these two integrations.
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": "369ae0a2-01a3-4246-8d68-887ac4e0d603",
"name": "Main Sticky1",
"type": "n8n-nodes-base.stickyNote",
"position": [
816,
1184
],
"parameters": {
"color": 2,
"width": 500,
"height": 632,
"content": "# AI Workflow Description and Template Generator\nThis workflow automates the creation of professional documentation and template-ready sticky notes for any n8n workflow using AI.\n## How it works\n1. Receives an n8n workflow JSON file via Telegram\n2. Validates the input file type and extracts workflow data\n3. Scrubs sensitive information and analyzes workflow structure\n4. Uses Google Gemini AI to generate comprehensive documentation\n5. Assembles a complete template with main workflow sticky note and logical section stickies\n6. Sends back the documented workflow file, usage checklist, and setup guide via Telegram\n\n## Setup\n1. Configure Telegram Trigger credentials for receiving files\n2. Configure Telegram API credentials for sending messages\n3. Configure Google Gemini Chat Model (Google PaLM API) credentials\n\n## Customization\nAdjust the prompt in the \"AI Template Generator\" node to modify documentation style, detail level, or specific requirements for your use case."
},
"typeVersion": 1
},
{
"id": "08d7799b-5dfb-47e4-a345-4a80c130f246",
"name": "Telegram Trigger1",
"type": "n8n-nodes-base.telegramTrigger",
"position": [
1392,
1344
],
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"typeVersion": 1.1,
"alwaysOutputData": true
},
{
"id": "4ed51649-fff8-48b3-a7ca-23db822eac8d",
"name": "Get a file1",
"type": "n8n-nodes-base.telegram",
"position": [
1808,
1264
],
"parameters": {
"fileId": "={{ $json.message.document.file_id }}",
"resource": "file",
"additionalFields": {
"mimeType": "={{ $json.message.document.mime_type }}"
}
},
"typeVersion": 1.2,
"alwaysOutputData": true
},
{
"id": "7ada2d88-b18e-445d-b675-3876365d3233",
"name": "Scrub & Analyze Workflow",
"type": "n8n-nodes-base.code",
"position": [
2368,
1264
],
"parameters": {
"jsCode": "// COMPREHENSIVE WORKFLOW SCRUBBER & ANALYZER - ROBUST VERSION\n// Handles any workflow structure with defensive programming\n\n// ============ HELPER FUNCTIONS ============\n\nfunction safeGet(obj, path, defaultValue = null) {\n try {\n return path.split('.').reduce((acc, part) => acc?.[part], obj) ?? defaultValue;\n } catch (e) {\n return defaultValue;\n }\n}\n\nfunction isValidArray(arr) {\n return Array.isArray(arr) && arr.length > 0;\n}\n\nfunction isValidObject(obj) {\n return obj && typeof obj === 'object' && !Array.isArray(obj);\n}\n\n// ============ INPUT HANDLING ============\n\ntry {\n // 1. GET INPUT WITH MULTIPLE FALLBACKS\n let rawInput = $input?.item?.json || $input?.first()?.json || {};\n let w = rawInput.data || rawInput;\n \n // 2. DETECT AND NORMALIZE STRUCTURE\n if (w.data) {\n if (typeof w.data === 'string') {\n try { w = JSON.parse(w.data); } catch(e) { w = w.data; }\n } else if (w.data.nodes) {\n w = w.data;\n }\n }\n \n // 3. VALIDATE WORKFLOW STRUCTURE\n if (!isValidObject(w)) {\n throw new Error('Invalid workflow: Input is not an object');\n }\n \n if (!isValidArray(w.nodes)) {\n throw new Error('Invalid workflow: Missing or empty nodes array');\n }\n \n // ============ TRACKING & PATTERNS ============\n \n const scrubbed = {\n credentials: 0,\n webhooks: 0,\n urls: 0,\n emails: 0,\n apiKeys: 0,\n documentIds: 0,\n phoneNumbers: 0,\n sensitiveFields: 0\n };\n \n const patterns = {\n email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g,\n phone: /\\+?[1-9]\\d{1,14}/g,\n apiKey: /(api[_-]?key|token|secret|password|bearer|auth)\\s*[:=]\\s*['\"]?([a-zA-Z0-9_\\-\\.]+)['\"]?/gi,\n url: /https?:\\/\\/[^\\s\"'<>]+/g,\n uuid: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi\n };\n \n const sensitiveKeys = [\n 'webhookId', 'cachedResultUrl', 'cachedResultName', 'instanceId',\n 'apiKey', 'token', 'secret', 'password', 'accessToken', 'refreshToken',\n 'privateKey', 'clientSecret', 'sessionId'\n ];\n \n // ============ RECURSIVE SCRUBBING ============\n \n function scrubObject(obj, path = '', depth = 0) {\n // Prevent infinite recursion\n if (depth > 50) return obj;\n \n if (!obj || typeof obj !== 'object') return obj;\n \n if (Array.isArray(obj)) {\n return obj.map((item, idx) => scrubObject(item, `${path}[${idx}]`, depth + 1));\n }\n \n const cleaned = {};\n \n for (const [key, value] of Object.entries(obj)) {\n const currentPath = path ? `${path}.${key}` : key;\n const lowerKey = key.toLowerCase();\n \n try {\n // Handle credentials object\n if (key === 'credentials' && isValidObject(value)) {\n scrubbed.credentials++;\n const credObj = {};\n for (const credKey of Object.keys(value)) {\n credObj[credKey] = { id: '' };\n }\n cleaned[key] = credObj;\n continue;\n }\n \n // Handle sensitive keys\n if (sensitiveKeys.some(sk => lowerKey.includes(sk.toLowerCase()))) {\n scrubbed.sensitiveFields++;\n cleaned[key] = '';\n continue;\n }\n \n // Handle ResourceLocator objects\n if (isValidObject(value) && value.__rl === true) {\n scrubbed.documentIds++;\n cleaned[key] = {\n __rl: true,\n mode: value.mode || 'id',\n value: 'YOUR_RESOURCE_ID_HERE'\n };\n continue;\n }\n \n // Handle string values\n if (typeof value === 'string') {\n let scrubbedValue = value;\n let wasModified = false;\n \n // Skip empty strings\n if (!value.trim()) {\n cleaned[key] = value;\n continue;\n }\n \n // Scrub URLs (but preserve expressions)\n if (patterns.url.test(value) && !value.includes('={{') && !value.includes('$json')) {\n scrubbed.urls++;\n if (value.includes('drive.google.com')) {\n scrubbedValue = 'YOUR_GOOGLE_DRIVE_URL_HERE';\n } else if (value.includes('webhook')) {\n scrubbedValue = 'YOUR_WEBHOOK_URL_HERE';\n } else if (value.includes('api')) {\n scrubbedValue = 'YOUR_API_ENDPOINT_HERE';\n } else {\n scrubbedValue = 'YOUR_URL_HERE';\n }\n wasModified = true;\n }\n \n // Scrub emails (preserve example.com)\n if (!wasModified && patterns.email.test(value) && !value.includes('example.com')) {\n scrubbed.emails++;\n scrubbedValue = scrubbedValue.replace(patterns.email, 'YOUR_EMAIL_HERE');\n wasModified = true;\n }\n \n // Scrub API keys/tokens (but preserve expressions)\n if (!wasModified && patterns.apiKey.test(value) && !value.includes('={{')) {\n scrubbed.apiKeys++;\n scrubbedValue = 'YOUR_API_KEY_HERE';\n wasModified = true;\n }\n \n // Scrub phone numbers\n if (!wasModified && lowerKey.includes('phone') && patterns.phone.test(value)) {\n scrubbed.phoneNumbers++;\n scrubbedValue = 'YOUR_PHONE_NUMBER_HERE';\n wasModified = true;\n }\n \n cleaned[key] = scrubbedValue;\n } else if (isValidObject(value)) {\n cleaned[key] = scrubObject(value, currentPath, depth + 1);\n } else {\n cleaned[key] = value;\n }\n } catch (err) {\n // If scrubbing fails for a field, preserve original\n cleaned[key] = value;\n }\n }\n \n return cleaned;\n }\n \n // ============ SPATIAL ANALYSIS WITH FALLBACKS ============\n \n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n let hasValidPositions = false;\n let actionNodes = [], triggerNodes = [];\n \n w.nodes.forEach(n => {\n try {\n // Categorize nodes\n if (n.type?.toLowerCase().includes('trigger')) {\n triggerNodes.push(n.name || n.type || 'Unknown Trigger');\n } else if (n.type !== 'n8n-nodes-base.stickyNote') {\n actionNodes.push(n.name || n.type || 'Unknown Node');\n }\n \n // Analyze positions (skip sticky notes)\n if (n.position && Array.isArray(n.position) && n.position.length >= 2 && n.type !== 'n8n-nodes-base.stickyNote') {\n const x = Number(n.position[0]);\n const y = Number(n.position[1]);\n \n if (!isNaN(x) && !isNaN(y) && isFinite(x) && isFinite(y)) {\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n hasValidPositions = true;\n }\n }\n } catch (err) {\n // Skip problematic nodes\n }\n });\n \n // Apply default bounds if no valid positions found\n if (!hasValidPositions || !isFinite(minX)) {\n minX = 0;\n minY = 0;\n maxX = 800;\n maxY = 600;\n }\n \n // ============ SCRUB ALL NODES ============\n \n w.nodes = w.nodes.map((node, idx) => {\n try {\n const scrubbed = scrubObject(node);\n \n // Ensure node has required fields\n if (!scrubbed.id) scrubbed.id = `node-${idx}-${Math.random().toString(36).substr(2, 9)}`;\n if (!scrubbed.name) scrubbed.name = scrubbed.type || `Node ${idx + 1}`;\n if (!scrubbed.type) scrubbed.type = 'n8n-nodes-base.noOp';\n if (!scrubbed.position || !Array.isArray(scrubbed.position)) {\n scrubbed.position = [minX + (idx * 200), minY];\n }\n \n return scrubbed;\n } catch (err) {\n // Return minimal valid node if scrubbing fails\n return {\n id: `node-${idx}-fallback`,\n name: node.name || `Node ${idx + 1}`,\n type: node.type || 'n8n-nodes-base.noOp',\n position: node.position || [minX + (idx * 200), minY],\n parameters: {}\n };\n }\n });\n \n // ============ CLEAN META FIELDS ============\n \n delete w.meta;\n delete w.pinData;\n delete w.instanceId;\n delete w.versionId;\n \n // Ensure connections exist\n if (!w.connections) w.connections = {};\n \n // ============ EXTRACT METADATA ============\n \nconst chatId = safeGet($('Telegram Trigger1').first(), 'json.message.chat.id') || \n safeGet($('Telegram Trigger1').first(), 'json.message.from.id') ||\n 'unknown';\n \n const originalFileName = safeGet(rawInput, 'originalFileName') || \n safeGet(rawInput, 'data.name') ||\n w.name || \n 'workflow';\n \n // ============ OUTPUT ============\n \n return [{\n json: {\n cleanedWorkflow: w,\n workflowString: JSON.stringify(w),\n chatId: chatId,\n originalFileName: originalFileName,\n analysis: {\n totalNodes: w.nodes.length,\n actionNodes: actionNodes.length,\n triggerNodes: triggerNodes.length,\n workflowBounds: { minX, minY, maxX, maxY },\n hasValidPositions: hasValidPositions,\n scrubbedItems: scrubbed,\n totalScrubbed: Object.values(scrubbed).reduce((a, b) => a + b, 0)\n }\n }\n }];\n \n} catch (error) {\n // ============ ERROR HANDLING ============\n return [{\n json: {\n error: true,\n errorMessage: error.message || 'Unknown error occurred',\n errorStack: error.stack,\n cleanedWorkflow: null,\n workflowString: null,\n chatId: 'error',\n originalFileName: 'error',\n analysis: {\n totalNodes: 0,\n workflowBounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 },\n scrubbedItems: {},\n totalScrubbed: 0\n }\n }\n }];\n}"
},
"typeVersion": 2
},
{
"id": "41cd2be2-6d07-4b35-a74f-a0110f7f1ed6",
"name": "Google Gemini Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
2576,
1472
],
"parameters": {
"options": {}
},
"typeVersion": 1
},
{
"id": "c93a398a-5163-4ccd-a9c0-0516bdc4ce05",
"name": "Structured Output Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
2720,
1472
],
"parameters": {
"jsonSchemaExample": "{\"mainSticky\":{\"parameters\":{\"content\":\"string\",\"height\":560,\"width\":500,\"color\":2},\"id\":\"string\",\"name\":\"Sticky Note\",\"type\":\"n8n-nodes-base.stickyNote\",\"position\":[-1232,-112],\"typeVersion\":1},\"sectionStickies\":[{\"parameters\":{\"content\":\"string\",\"height\":300,\"width\":600,\"color\":7},\"id\":\"string\",\"name\":\"string\",\"type\":\"n8n-nodes-base.stickyNote\",\"position\":[0,0],\"typeVersion\":1}],\"titleSuggestions\":[\"string\",\"string\"],\"templateDescription\":\"string\",\"tags\":[\"string\"],\"validationResults\":{\"titleFormat\":true,\"descriptionLength\":true,\"hasStickyNotes\":true,\"credentialsRemoved\":true}}"
},
"typeVersion": 1.3
},
{
"id": "eae6eb24-fd52-4027-bace-361059cdf878",
"name": "Check Input Type1",
"type": "n8n-nodes-base.if",
"position": [
1584,
1344
],
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.message.document }}",
"operation": "isNotEmpty"
}
]
}
},
"typeVersion": 1
},
{
"id": "cc3547f5-243a-4991-8f66-0f8f227c3554",
"name": "Send a text message1",
"type": "n8n-nodes-base.telegram",
"position": [
1808,
1440
],
"parameters": {
"text": "=\u26a0\ufe0f **Text Not Accepted**\n\nn8n workflows are too large for Telegram text messages (limit: 4096 chars).\n\n\ud83d\udc49 Please **drag and drop the .json file** instead.",
"chatId": "={{ $json.message.from.id }}",
"additionalFields": {}
},
"typeVersion": 1.2
},
{
"id": "8e0e77a7-2529-48d3-b724-b3e0dea44c4f",
"name": "Extract from File1",
"type": "n8n-nodes-base.extractFromFile",
"position": [
2144,
1264
],
"parameters": {
"options": {},
"operation": "fromJson"
},
"typeVersion": 1.1,
"alwaysOutputData": true
},
{
"id": "af7b9028-c3f0-40d3-a838-a06d04310731",
"name": "AI Template Generator1",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
2576,
1264
],
"parameters": {
"text": "=You are an expert n8n Template Engineer. Your task is to analyze a raw n8n workflow JSON and generate documentation that strictly adheres to the n8n Template Guidelines.\n\n### INPUT DATA:\n{{ $json.workflowString }}\n\n### GLOBAL RULES:\n1. NO Markdown code blocks in the output (return raw JSON).\n2. NO HTML tags.\n3. Language: Clear, professional English.\n\n### TASK 1: GENERATE MAIN STICKY NOTE\n**Requirements:**\n- **Color:** Yellow (ID: 2)\n- **Dimensions:** Width: 500px, Height: 600px.\n- **Content Structure (Markdown):**\n 1. **## Title**: format \"Action Verb + Thing + Context\" (e.g., \"Sync Contacts from HubSpot to Google Sheets\").\n 2. ****Description****: A 1-sentence value proposition.\n 3. **### How it works**: A numbered list of the logical steps. Keep bullet points short (max 10 words per bullet).\n 4. **### Setup**: A checklist of requirements (e.g., \"1. Configure Google Sheets credentials\").\n 5. **### Customization**: 1 tip on how to adapt the workflow.\n\n### TASK 2: GENERATE SECTION STICKIES\n**Requirements:**\n- **Condition:** Only generate if workflow has 5+ nodes.\n- **Color:** White (ID: 7).\n- **Content:** A short **## Heading** (e.g., \"## 1. Webhook Trigger\", \"## 2. Data Processing\").\n- **Goal:** Create logical groups (Trigger area, Action area).\n\n### TASK 3: METADATA & VALIDATION\n- **Title Suggestions:** 3 SEO-friendly titles.\n- **Description:** A STRICTLY CONCISE summary (Max 3 sentences or 60 words). Focus on the \"Why\" and \"Result\", not the \"How\". Do not list steps here.\n- **Tags:** 3-5 relevant tags.\n- **Security Check:** Set `credentialsRemoved` to false if hardcoded secrets are found.\n\n### OUTPUT SCHEMA:\nReturn ONLY a JSON object matching this structure:\n{\n \"mainSticky\": {\n \"parameters\": { \"content\": \"string\", \"height\": 600, \"width\": 500, \"color\": 2 },\n \"name\": \"Main Sticky\",\n \"type\": \"n8n-nodes-base.stickyNote\",\n \"typeVersion\": 1\n },\n \"sectionStickies\": [\n {\n \"parameters\": { \"content\": \"## Header\", \"height\": 200, \"width\": 300, \"color\": 7 },\n \"name\": \"Section 1\",\n \"type\": \"n8n-nodes-base.stickyNote\",\n \"typeVersion\": 1\n }\n ],\n \"titleSuggestions\": [\"Title 1\", \"Title 2\"],\n \"templateDescription\": \"string\",\n \"tags\": [\"tag1\", \"tag2\"],\n \"validationResults\": {\n \"titleFormat\": true,\n \"descriptionLength\": true,\n \"hasStickyNotes\": true,\n \"credentialsRemoved\": true\n }\n}",
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 1.4
},
{
"id": "402deaff-4573-45f1-8e38-001c78413360",
"name": "Assemble Final Template1",
"type": "n8n-nodes-base.code",
"position": [
3008,
1264
],
"parameters": {
"jsCode": "// ASSEMBLE FINAL TEMPLATE - ROBUST VERSION\n// Handles any workflow structure with intelligent positioning\n\n// ============ HELPER FUNCTIONS ============\n\nfunction safeGet(obj, path, defaultValue = null) {\n try {\n return path.split('.').reduce((acc, part) => acc?.[part], obj) ?? defaultValue;\n } catch (e) {\n return defaultValue;\n }\n}\n\nfunction isValidArray(arr) {\n return Array.isArray(arr) && arr.length > 0;\n}\n\nfunction isValidObject(obj) {\n return obj && typeof obj === 'object' && !Array.isArray(obj);\n}\n\nfunction ensureValidPosition(pos, fallback = [0, 0]) {\n if (!Array.isArray(pos) || pos.length < 2) return fallback;\n const x = Number(pos[0]);\n const y = Number(pos[1]);\n return [isFinite(x) ? x : fallback[0], isFinite(y) ? y : fallback[1]];\n}\n\n// ============ INPUT VALIDATION ============\n\ntry {\n // 1. GET INPUTS WITH FALLBACKS\n const cleanData = safeGet($('Scrub & Analyze Workflow'), 'item.json', {});\n const triggerData = safeGet($('Telegram Trigger1'), 'first.json', {});\n const aiData = safeGet($input, 'item.json', {});\n \n // Check for errors from previous node\n if (cleanData.error) {\n throw new Error(`Previous node error: ${cleanData.errorMessage}`);\n }\n \n // Validate required data\n if (!isValidObject(cleanData.cleanedWorkflow)) {\n throw new Error('Missing cleaned workflow data');\n }\n \n if (!isValidArray(cleanData.cleanedWorkflow.nodes)) {\n throw new Error('Workflow has no nodes');\n }\n \n // ============ PREPARE WORKFLOW ============\n \n const wf = JSON.parse(JSON.stringify(cleanData.cleanedWorkflow)); // Deep clone\n const baseFileName = (cleanData.originalFileName || 'workflow').replace(/\\.json$/i, '');\n const analysis = cleanData.analysis || {};\n \n // Get bounds with fallbacks\n let bounds = analysis.workflowBounds || { minX: 0, minY: 0, maxX: 800, maxY: 600 };\n \n // Validate bounds\n if (!isFinite(bounds.minX) || !isFinite(bounds.maxX)) {\n bounds = { minX: 0, minY: 0, maxX: 800, maxY: 600 };\n }\n \n // Recalculate bounds if needed\n if (bounds.minX === Infinity || !analysis.hasValidPositions) {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n let foundValid = false;\n \n wf.nodes.forEach(n => {\n if (n.position && n.type !== 'n8n-nodes-base.stickyNote') {\n const pos = ensureValidPosition(n.position);\n minX = Math.min(minX, pos[0]);\n minY = Math.min(minY, pos[1]);\n maxX = Math.max(maxX, pos[0]);\n maxY = Math.max(maxY, pos[1]);\n foundValid = true;\n }\n });\n \n if (foundValid && isFinite(minX)) {\n bounds = { minX, minY, maxX, maxY };\n } else {\n // Apply default grid layout\n wf.nodes.forEach((n, idx) => {\n if (n.type !== 'n8n-nodes-base.stickyNote') {\n n.position = [idx * 220, 0];\n }\n });\n bounds = { minX: 0, minY: 0, maxX: wf.nodes.length * 220, maxY: 400 };\n }\n }\n \n // ============ REMOVE OLD STICKIES ============\n \n wf.nodes = wf.nodes.filter(n => {\n try {\n return !(n.type === 'n8n-nodes-base.stickyNote' && [2, 7].includes(safeGet(n, 'parameters.color')));\n } catch (e) {\n return true;\n }\n });\n \n // ============ ADD MAIN STICKY ============\n \n const aiOutput = safeGet(aiData, 'output', {});\n \n if (isValidObject(aiOutput.mainSticky)) {\n try {\n const mainSticky = JSON.parse(JSON.stringify(aiOutput.mainSticky));\n mainSticky.id = 'sticky-main-' + Math.random().toString(36).substring(7);\n \n // Ensure parameters\n if (!mainSticky.parameters) mainSticky.parameters = {};\n mainSticky.parameters.color = 2;\n mainSticky.parameters.height = mainSticky.parameters.height || 600;\n mainSticky.parameters.width = mainSticky.parameters.width || 500;\n \n // Position to the left\n const offsetX = mainSticky.parameters.width + 40;\n mainSticky.position = [bounds.minX - offsetX, bounds.minY];\n \n // Ensure required fields\n mainSticky.name = mainSticky.name || 'Main Documentation';\n mainSticky.type = 'n8n-nodes-base.stickyNote';\n mainSticky.typeVersion = 1;\n \n wf.nodes.unshift(mainSticky);\n } catch (err) {\n // Skip if main sticky fails\n }\n }\n \n // ============ INTELLIGENT SECTION STICKY POSITIONING ============\n \n if (isValidArray(aiOutput.sectionStickies)) {\n try {\n const sectionStickies = JSON.parse(JSON.stringify(aiOutput.sectionStickies));\n const numSections = sectionStickies.length;\n \n // Get workflow nodes (non-sticky, with valid positions)\n const workflowNodes = wf.nodes\n .filter(n => {\n try {\n return n.type !== 'n8n-nodes-base.stickyNote' && \n Array.isArray(n.position) && \n isFinite(n.position[0]);\n } catch (e) {\n return false;\n }\n })\n .sort((a, b) => a.position[0] - b.position[0]);\n \n if (workflowNodes.length > 0 && numSections > 0) {\n // ============ SMART GROUPING ALGORITHM ============\n \n // Calculate workflow width and density\n const workflowWidth = bounds.maxX - bounds.minX;\n const avgNodeSpacing = workflowWidth / Math.max(workflowNodes.length - 1, 1);\n \n // Detect natural groupings based on spacing\n const groups = [];\n let currentGroup = [workflowNodes[0]];\n \n for (let i = 1; i < workflowNodes.length; i++) {\n const prevNode = workflowNodes[i - 1];\n const currNode = workflowNodes[i];\n const gap = currNode.position[0] - prevNode.position[0];\n \n // If gap is significantly larger than average, start new group\n if (gap > avgNodeSpacing * 1.5 && groups.length < numSections - 1) {\n groups.push(currentGroup);\n currentGroup = [currNode];\n } else {\n currentGroup.push(currNode);\n }\n }\n groups.push(currentGroup);\n \n // If we have fewer natural groups than stickies, divide evenly\n if (groups.length < numSections) {\n const nodesPerSection = Math.ceil(workflowNodes.length / numSections);\n groups.length = 0;\n for (let i = 0; i < numSections; i++) {\n const start = i * nodesPerSection;\n const end = Math.min(start + nodesPerSection, workflowNodes.length);\n if (start < workflowNodes.length) {\n groups.push(workflowNodes.slice(start, end));\n }\n }\n }\n \n // If we have more groups than stickies, merge smallest groups\n while (groups.length > numSections && groups.length > 1) {\n groups.sort((a, b) => a.length - b.length);\n const smallest = groups.shift();\n groups[0] = [...smallest, ...groups[0]];\n }\n \n // ============ POSITION STICKIES ============\n \n sectionStickies.forEach((sticky, index) => {\n try {\n sticky.id = `sticky-section-${index}-` + Math.random().toString(36).substring(7);\n \n // Ensure parameters\n if (!sticky.parameters) sticky.parameters = {};\n sticky.parameters.color = 7;\n const stickyWidth = sticky.parameters.width || 600;\n const stickyHeight = sticky.parameters.height || 200;\n \n // Ensure required fields\n sticky.name = sticky.name || `Section ${index + 1}`;\n sticky.type = 'n8n-nodes-base.stickyNote';\n sticky.typeVersion = 1;\n \n if (groups[index] && groups[index].length > 0) {\n const sectionNodes = groups[index];\n \n // Calculate section bounds\n const positions = sectionNodes.map(n => ensureValidPosition(n.position));\n const sectionMinX = Math.min(...positions.map(p => p[0]));\n const sectionMaxX = Math.max(...positions.map(p => p[0]));\n const sectionMinY = Math.min(...positions.map(p => p[1]));\n \n // Center sticky above section\n const sectionCenterX = (sectionMinX + sectionMaxX) / 2;\n const stickyX = sectionCenterX - (stickyWidth / 2);\n const stickyY = sectionMinY - stickyHeight - 50;\n \n sticky.position = [Math.round(stickyX), Math.round(stickyY)];\n } else {\n // Fallback: distribute horizontally\n const spacing = Math.max(400, workflowWidth / numSections);\n sticky.position = [\n Math.round(bounds.minX + (index * spacing)),\n Math.round(bounds.minY - stickyHeight - 50)\n ];\n }\n \n wf.nodes.push(sticky);\n } catch (err) {\n // Skip problematic sticky\n }\n });\n } else {\n // Fallback: no workflow nodes or no sections\n sectionStickies.forEach((sticky, index) => {\n try {\n sticky.id = `sticky-section-${index}-` + Math.random().toString(36).substring(7);\n if (!sticky.parameters) sticky.parameters = {};\n sticky.parameters.color = 7;\n const stickyHeight = sticky.parameters.height || 200;\n sticky.name = sticky.name || `Section ${index + 1}`;\n sticky.type = 'n8n-nodes-base.stickyNote';\n sticky.typeVersion = 1;\n sticky.position = [\n bounds.minX + (index * 400),\n bounds.minY - stickyHeight - 50\n ];\n wf.nodes.push(sticky);\n } catch (err) {\n // Skip\n }\n });\n }\n } catch (err) {\n // Skip section stickies if they fail\n }\n }\n \n // ============ GENERATE OUTPUT FILES ============\n \n const prettyJson = JSON.stringify(wf, null, 2);\n const outFileName = `${baseFileName}_TEMPLATE.json`;\n \n const validationResults = safeGet(aiOutput, 'validationResults', {});\n const titleSuggestions = safeGet(aiOutput, 'titleSuggestions', []);\n const tags = safeGet(aiOutput, 'tags', []);\n \n const checklist = `\u2705 **Template Processed**\\n${validationResults.credentialsRemoved ? '\u2705' : '\u274c'} Secrets scrubbed\\n${aiOutput.mainSticky ? '\u2705' : '\u274c'} Documentation added\\nNodes: ${analysis.totalNodes || wf.nodes.length}\\nScrubbed: ${analysis.totalScrubbed || 0} items`;\n \n const guide = `\ud83d\udccb **Submission Guide**\\n\\n**Title Suggestions:**\\n${isValidArray(titleSuggestions) ? titleSuggestions.map(t => `\u2022 ${t}`).join('\\\\n') : '\u2022 No suggestions generated'}\\n\\n**Description:**\\n${aiOutput.templateDescription || 'No description generated.'}\\n\\n**Tags:** ${isValidArray(tags) ? tags.join(', ') : 'No tags generated'}`;\n \n // ============ RETURN OUTPUT ============\n \n return [{\n json: {\n chatId: cleanData.chatId || 'unknown',\n triggerChatId: safeGet(triggerData, 'message.chat.id') || safeGet(triggerData, 'message.from.id') || cleanData.chatId || 'unknown',\n fileName: outFileName,\n checklist: checklist,\n guide: guide,\n success: true\n },\n binary: {\n data: {\n data: Buffer.from(prettyJson).toString('base64'),\n mimeType: 'application/json',\n fileName: outFileName\n }\n }\n }];\n \n} catch (error) {\n // ============ ERROR HANDLING ============\n \n const errorMsg = `\u274c **Template Generation Failed**\\n\\nError: ${error.message}\\n\\nPlease ensure you uploaded a valid n8n workflow JSON file.`;\n \n return [{\n json: {\n chatId: 'error',\n triggerChatId: 'error',\n fileName: 'error.json',\n checklist: errorMsg,\n guide: 'Error occurred during processing.',\n success: false,\n error: error.message,\n errorStack: error.stack\n },\n binary: {\n data: {\n data: Buffer.from(JSON.stringify({ error: error.message }, null, 2)).toString('base64'),\n mimeType: 'application/json',\n fileName: 'error.json'\n }\n }\n }];\n}"
},
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "f2790920-329d-43e1-a01f-2d4a8c94963d",
"name": "Send Template File",
"type": "n8n-nodes-base.telegram",
"position": [
3200,
1264
],
"parameters": {
"chatId": "={{ $('Assemble Final Template1').item.json.triggerChatId }}",
"operation": "sendDocument",
"binaryData": true,
"additionalFields": {
"caption": "\ud83c\udf89 Here is your standardized template:"
}
},
"typeVersion": 1.2
},
{
"id": "6de8287e-1054-4cbd-8894-973d2c5d697a",
"name": "Send Checklist1",
"type": "n8n-nodes-base.telegram",
"position": [
3392,
1264
],
"parameters": {
"text": "={{ $('Assemble Final Template1').item.json.checklist }}",
"chatId": "={{ $json.result.chat.id }}",
"additionalFields": {}
},
"typeVersion": 1.2
},
{
"id": "3fa18149-6e86-461c-8fcc-f31af57731ec",
"name": "Send Guide1",
"type": "n8n-nodes-base.telegram",
"position": [
3584,
1264
],
"parameters": {
"text": "={{ $('Assemble Final Template1').item.json.guide }}",
"chatId": "={{ $json.result.chat.id }}",
"additionalFields": {}
},
"typeVersion": 1.2
},
{
"id": "f8d8521c-f076-417e-85b9-693c3ab813bd",
"name": "Input & Validation1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1360,
1184
],
"parameters": {
"color": 7,
"width": 656,
"height": 440,
"content": "## 1. Input & Validation"
},
"typeVersion": 1
},
{
"id": "9385d526-6ce2-49b8-aa4c-56eeeda28003",
"name": "AI Documentation Generation1",
"type": "n8n-nodes-base.stickyNote",
"position": [
2064,
1184
],
"parameters": {
"color": 7,
"width": 832,
"height": 440,
"content": "## 2. AI Documentation Generation"
},
"typeVersion": 1
},
{
"id": "e59b1c9b-af29-4344-8aae-50410f82847e",
"name": "Template Assembly & Delivery1",
"type": "n8n-nodes-base.stickyNote",
"position": [
2944,
1184
],
"parameters": {
"color": 7,
"width": 864,
"height": 440,
"content": "## 3. Template Assembly & Delivery"
},
"typeVersion": 1
}
],
"connections": {
"Get a file1": {
"main": [
[
{
"node": "Extract from File1",
"type": "main",
"index": 0
}
]
]
},
"Send Checklist1": {
"main": [
[
{
"node": "Send Guide1",
"type": "main",
"index": 0
}
]
]
},
"Check Input Type1": {
"main": [
[
{
"node": "Get a file1",
"type": "main",
"index": 0
}
],
[
{
"node": "Send a text message1",
"type": "main",
"index": 0
}
]
]
},
"Telegram Trigger1": {
"main": [
[
{
"node": "Check Input Type1",
"type": "main",
"index": 0
}
]
]
},
"Extract from File1": {
"main": [
[
{
"node": "Scrub & Analyze Workflow",
"type": "main",
"index": 0
}
]
]
},
"Send Template File": {
"main": [
[
{
"node": "Send Checklist1",
"type": "main",
"index": 0
}
]
]
},
"AI Template Generator1": {
"main": [
[
{
"node": "Assemble Final Template1",
"type": "main",
"index": 0
}
]
]
},
"Assemble Final Template1": {
"main": [
[
{
"node": "Send Template File",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Template Generator1",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Scrub & Analyze Workflow": {
"main": [
[
{
"node": "AI Template Generator1",
"type": "main",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "AI Template Generator1",
"type": "ai_outputParser",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates the creation of professional documentation and template-ready sticky notes for any n8n workflow using AI. Receives an n8n workflow JSON file via Telegram Validates the input file type and extracts workflow data Scrubs sensitive information and analyzes…
Source: https://n8n.io/workflows/11467/ — 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 workflow contains community nodes that are only compatible with the self-hosted version of n8n.
This n8n workflow automates the entire lead nurturing process from initial contact through a 3-email follow-up sequence, with intelligent reply detection and personalized AI-generated content. It's de
> Optimize your AI workflows, cut costs, and get faster, more accurate answers.
Flexible and scalable chatbot template, designed mainly for Spanish conversations but capable of handling English and other languages. Integrates Google Gemini API for text and image generation, and T
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.