AutomationFlowsAI & RAG › Detect Kb Gaps and Auto-draft Articles with Gpt-4.1, Slack and Gmail

Detect Kb Gaps and Auto-draft Articles with Gpt-4.1, Slack and Gmail

ByCarlo B. @logicfoxai on n8n.io

Support teams, knowledge managers, and ops builders who are drowning in outdated KB articles and repeat tickets. If your team keeps answering the same questions because your knowledge base has gaps nobody has time to find — this template finds them automatically and writes the…

Cron / scheduled trigger★★★★★ complexityAI-powered38 nodesHTTP RequestAgentOpenAI ChatError TriggerGmail
AI & RAG Trigger: Cron / scheduled Nodes: 38 Complexity: ★★★★★ AI nodes: yes Added:
Detect Kb Gaps and Auto-draft Articles with Gpt-4.1, Slack and Gmail — n8n workflow card showing HTTP Request, Agent, OpenAI Chat integration

This workflow corresponds to n8n.io template #14951 — we link there as the canonical source.

This workflow follows the Agent → Error Trigger 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 →

Download .json
{
  "id": "eYs3u37cjc21aiO5",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Multi-Agent Knowledge Base Curator with AI Gap Detection & Drafting",
  "tags": [],
  "nodes": [
    {
      "id": "8c441f1a-e515-4836-9207-cf59d955ea2d",
      "name": "Daily Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "notes": "Runs every 24 hours. Adjust the interval to match your support volume.",
      "position": [
        1136,
        896
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 24
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e3f3d9ce-d0dd-409b-b907-a85d691dda2e",
      "name": "Fetch Recent Support Tickets",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Fetches resolved support tickets from the last 7 days. Replace the URL and auth with your helpdesk API (Zendesk, Freshdesk, Intercom, etc.).",
      "position": [
        1584,
        800
      ],
      "parameters": {
        "url": "={{ $json.ticketApiUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "status",
              "value": "resolved"
            },
            {
              "name": "created_after",
              "value": "={{ $now.minus({days: 7}).toISO() }}"
            },
            {
              "name": "per_page",
              "value": "100"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d06cd284-c323-4476-b32d-ef90a5c6e15d",
      "name": "Fetch KB Articles",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Fetches all published knowledge base articles. Replace URL/auth with your KB platform API (Notion, Confluence, Zendesk Guide, GitBook, etc.).",
      "position": [
        1584,
        1104
      ],
      "parameters": {
        "url": "={{ $json.kbApiUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "per_page",
              "value": "500"
            },
            {
              "name": "status",
              "value": "published"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "db89d5af-ed9c-4738-929d-975181ee97ff",
      "name": "Configuration",
      "type": "n8n-nodes-base.code",
      "notes": "\u2699\ufe0f EDIT THIS FIRST \u2014 Set your helpdesk API URL, KB API URL, Slack webhook, thresholds, and company context.",
      "position": [
        1360,
        896
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// CONFIGURATION \u2014 Edit these values for your environment\n// ============================================================\nconst CONFIG = {\n  // Helpdesk / ticketing API\n  ticketApiUrl: 'https://YOUR_HELPDESK.zendesk.com/api/v2/tickets.json',\n  // Knowledge base API\n  kbApiUrl: 'https://YOUR_HELPDESK.zendesk.com/api/v2/help_center/articles.json',\n  // Slack webhook for notifications\n  slackWebhookUrl: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL',\n  // Minimum tickets on a topic before it's flagged as a gap\n  gapThreshold: 3,\n  // Days after which an article is considered potentially stale\n  staleDays: 90,\n  // Company/product name for article drafting context\n  emailTo: 'user@example.com',\n  emailFrom: 'user@example.com',\n  emailEnabled: true,\n  companyName: 'YourCompany',\n  productName: 'YourProduct'\n};\n\nreturn [{ json: CONFIG }];"
      },
      "typeVersion": 2
    },
    {
      "id": "3889ac27-6ea6-4c11-93ae-9bdbaa4e9dd0",
      "name": "Normalize Ticket Data",
      "type": "n8n-nodes-base.code",
      "notes": "Normalizes tickets from any helpdesk format into a standard schema. Adapt field mappings for your specific API.",
      "position": [
        1792,
        800
      ],
      "parameters": {
        "jsCode": "// Normalize support tickets into a standard format\nconst tickets = $input.all();\nconst normalized = [];\n\nfor (const item of tickets) {\n  const ticket = item.json;\n  // Adapt field names to your helpdesk's API response\n  normalized.push({\n    id: ticket.id || ticket.ticket_id,\n    subject: ticket.subject || ticket.title || '',\n    description: ticket.description || ticket.body || ticket.content || '',\n    tags: ticket.tags || [],\n    category: ticket.category || ticket.type || 'general',\n    created_at: ticket.created_at || ticket.createdAt,\n    resolution: ticket.resolution || ticket.comment || ticket.answer || '',\n    customer_satisfaction: ticket.satisfaction_rating?.score || null,\n    url: ticket.url || ticket.html_url || ''\n  });\n}\n\n// Truncate descriptions to save tokens\nconst truncated = normalized.map(t => ({\n  ...t,\n  description: t.description.substring(0, 500),\n  resolution: t.resolution.substring(0, 500)\n}));\n\nreturn [{ json: { tickets: truncated, ticketCount: truncated.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "b828ab70-7d11-41f0-9628-ac38f84a64de",
      "name": "Normalize KB Data",
      "type": "n8n-nodes-base.code",
      "notes": "Normalizes KB articles from any platform into a standard schema. Adapt field mappings for your specific KB API.",
      "position": [
        1792,
        1104
      ],
      "parameters": {
        "jsCode": "// Normalize KB articles into a standard format\nconst articles = $input.all();\nconst normalized = [];\n\nfor (const item of articles) {\n  const article = item.json;\n  normalized.push({\n    id: article.id || article.article_id,\n    title: article.title || article.name || '',\n    body: (article.body || article.content || article.description || '').substring(0, 800),\n    category: article.section?.name || article.category || article.folder || 'uncategorized',\n    tags: article.label_names || article.tags || [],\n    last_updated: article.updated_at || article.editedTime || article.lastModified,\n    view_count: article.view_count || article.views || 0,\n    vote_sum: article.vote_sum || article.helpful_count || 0,\n    url: article.html_url || article.url || ''\n  });\n}\n\nreturn [{ json: { articles: normalized, articleCount: normalized.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "57efc46e-9454-463e-8787-718bce1416c4",
      "name": "Merge Tickets + KB",
      "type": "n8n-nodes-base.merge",
      "position": [
        2016,
        960
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3
    },
    {
      "id": "9ce3d1fd-6b22-42ff-8026-7f66ab45e798",
      "name": "Prepare Analysis Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        2240,
        960
      ],
      "parameters": {
        "jsCode": "// Prepare the combined payload for the Gap Analysis Agent\nconst items = $input.all();\nlet tickets = [];\nlet articles = [];\nlet config = {};\n\nfor (const item of items) {\n  if (item.json.tickets) tickets = item.json.tickets;\n  if (item.json.articles) articles = item.json.articles;\n  if (item.json.gapThreshold) config = item.json;\n}\n\n// Create a condensed article index for the AI (titles + categories only)\nconst articleIndex = articles.map(a => ({\n  title: a.title,\n  category: a.category,\n  tags: a.tags,\n  last_updated: a.last_updated\n}));\n\n// Group tickets by subject similarity (simple keyword extraction)\nconst topicMap = {};\nfor (const ticket of tickets) {\n  const key = ticket.category + '::' + (ticket.tags?.[0] || 'general');\n  if (!topicMap[key]) topicMap[key] = [];\n  topicMap[key].push({\n    subject: ticket.subject,\n    description: ticket.description.substring(0, 200)\n  });\n}\n\nreturn [{\n  json: {\n    ticketCount: tickets.length,\n    articleCount: articles.length,\n    topicGroups: topicMap,\n    articleIndex: articleIndex,\n    tickets: tickets,\n    articles: articles,\n    config: config\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "978b3688-70ab-41ea-a947-efbd8793f78f",
      "name": "Gap Analysis Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "notes": "\ud83e\udd16 AGENT 1: Analyzes support tickets vs KB articles to identify knowledge gaps, weak coverage, and stale content.",
      "position": [
        2464,
        960
      ],
      "parameters": {
        "text": "You are an expert Knowledge Base Analyst. You MUST respond with ONLY a valid JSON object \u2014 no markdown, no code fences, no explanation text, no preamble. Just the raw JSON object starting with { and ending with }. Be thorough but practical \u2014 prioritize gaps that will reduce the most ticket volume.",
        "options": {
          "systemMessage": "You are an expert Knowledge Base Analyst AI agent. You excel at pattern recognition across support tickets and identifying documentation gaps. Always respond with valid JSON. Be thorough but practical \u2014 prioritize gaps that will reduce the most ticket volume."
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "b405c3d9-cb39-48c9-ad5a-05a5822d268d",
      "name": "OpenAI GPT-4.1 (Gap Analysis)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        2464,
        1168
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1"
        },
        "options": {
          "maxTokens": 4096,
          "temperature": 0.2
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "7da079d6-0cbd-4583-8557-9737c8ffef06",
      "name": "Parse Gap Analysis",
      "type": "n8n-nodes-base.code",
      "position": [
        2736,
        960
      ],
      "parameters": {
        "jsCode": "// Parse the Gap Analysis Agent output\nconst output = $input.first().json;\nlet analysis;\n\ntry {\n  // The agent may return the JSON in .output or .text\n  const raw = output.output || output.text || JSON.stringify(output);\n  // Extract JSON from the response (handle potential markdown wrapping)\n  const jsonMatch = raw.match(/\\{[\\s\\S]*\\}/);\n  analysis = JSON.parse(jsonMatch ? jsonMatch[0] : raw);\n} catch (e) {\n  analysis = {\n    gaps: [],\n    weak_coverage: [],\n    stale_articles: [],\n    summary: {\n      total_gaps_found: 0,\n      total_weak_coverage: 0,\n      total_stale: 0,\n      overall_kb_health_score: 0,\n      top_priority_action: 'ERROR: Could not parse gap analysis \u2014 check agent output'\n    },\n    _parse_error: e.message,\n    _raw_output: (output.output || output.text || '').substring(0, 500)\n  };\n}\n\n// Pass through the original data for downstream agents\nreturn [{\n  json: {\n    analysis: analysis,\n    hasGaps: (analysis.gaps?.length || 0) > 0,\n    gapCount: analysis.gaps?.length || 0,\n    config: $('Prepare Analysis Payload').first().json.config,\n    articles: $('Prepare Analysis Payload').first().json.articles\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "95a87e08-41f6-40eb-a74e-8e581aec82e5",
      "name": "Gaps Found?",
      "type": "n8n-nodes-base.if",
      "position": [
        2896,
        960
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "has-gaps",
              "operator": {
                "type": "boolean",
                "operation": "equals",
                "singleValue": true
              },
              "leftValue": "={{ $json.hasGaps }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "8e2df13f-007e-4c28-ac12-b204a6edca3e",
      "name": "Split Into Individual Gaps",
      "type": "n8n-nodes-base.code",
      "notes": "Splits each knowledge gap into a separate item so the Article Drafter can process them individually.",
      "position": [
        3120,
        800
      ],
      "parameters": {
        "jsCode": "// Split gaps into individual items for the Article Drafter\nconst data = $input.first().json;\nconst gaps = data.analysis.gaps || [];\nconst config = data.config || {};\n\nreturn gaps.map(gap => ({\n  json: {\n    gap: gap,\n    companyName: config.companyName || 'YourCompany',\n    productName: config.productName || 'YourProduct'\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "9c07ccc5-f1a3-487a-aef1-1aec98146ef6",
      "name": "Article Drafter Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "notes": "\ud83e\udd16 AGENT 2: Drafts publication-ready KB articles for each identified knowledge gap.",
      "position": [
        3344,
        800
      ],
      "parameters": {
        "text": "You are a Technical Writer AI agent specializing in knowledge base articles. Draft a complete, publication-ready KB article for the following knowledge gap.\n\n## KNOWLEDGE GAP\n\n**Topic:** {{ $json.gap.suggested_title }}\n**Urgency:** {{ $json.gap.urgency }}\n**Key questions to answer:**\n{{ $json.gap.key_questions.join('\\n- ') }}\n**Sample ticket subjects that triggered this gap:**\n{{ $json.gap.sample_ticket_subjects.join('\\n- ') }}\n\n**Company:** {{ $json.companyName }}\n**Product:** {{ $json.productName }}\n\n## WRITING GUIDELINES\n\n- Write for a non-technical end user unless the topic is clearly developer-facing\n- Start with a one-sentence summary of what this article covers\n- Use clear headings (##) to organize sections\n- Include step-by-step instructions where applicable\n- Add a \"Troubleshooting\" section if relevant\n- End with \"Still need help? Contact our support team.\"\n- Keep the tone professional, friendly, and concise\n- Target 400-800 words\n\n## OUTPUT FORMAT\n\nRespond with valid JSON only (no markdown fences):\n{\n  \"title\": \"string\",\n  \"slug\": \"string (url-friendly)\",\n  \"category\": \"string\",\n  \"tags\": [\"string\"],\n  \"summary\": \"string (1-2 sentences)\",\n  \"body_markdown\": \"string (full article in markdown)\",\n  \"estimated_reading_time_minutes\": number,\n  \"seo_description\": \"string (under 160 chars)\"\n}",
        "options": {
          "systemMessage": "You are an expert technical writer who creates clear, helpful knowledge base articles. Your articles consistently reduce support ticket volume by addressing common customer questions proactively. Always output valid JSON."
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "cb5c848d-4358-484a-ab57-c1e99edf1073",
      "name": "OpenAI GPT-4.1 (Article Drafter)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        3344,
        1024
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1"
        },
        "options": {
          "maxTokens": 4096,
          "temperature": 0.4
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "34cc3da9-bc3f-4759-b413-cd13f4541f13",
      "name": "Parse Drafted Article",
      "type": "n8n-nodes-base.code",
      "position": [
        3632,
        800
      ],
      "parameters": {
        "jsCode": "// Parse the drafted article from the agent's output\nconst output = $input.first().json;\nlet article;\n\ntry {\n  const raw = output.output || output.text || JSON.stringify(output);\n  const jsonMatch = raw.match(/\\{[\\s\\S]*\\}/);\n  article = JSON.parse(jsonMatch ? jsonMatch[0] : raw);\n} catch (e) {\n  article = {\n    title: 'DRAFT PARSE ERROR',\n    body_markdown: (output.output || output.text || '').substring(0, 2000),\n    _error: e.message\n  };\n}\n\nreturn [{ json: { drafted_article: article, status: 'draft' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "74042866-84ce-41d4-bd84-3d0e2b0a3708",
      "name": "Quality Reviewer Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "notes": "\ud83e\udd16 AGENT 3: Reviews each drafted article for quality, scoring it across 6 dimensions and recommending publish/revise/reject.",
      "position": [
        3776,
        800
      ],
      "parameters": {
        "text": "You are a Quality Reviewer AI agent. Review the following drafted KB article for quality, accuracy signals, and completeness.\n\n## ARTICLE TO REVIEW\n\n**Title:** {{ $json.drafted_article.title }}\n**Category:** {{ $json.drafted_article.category }}\n**Summary:** {{ $json.drafted_article.summary }}\n\n**Full Body:**\n{{ $json.drafted_article.body_markdown }}\n\n## REVIEW CRITERIA\n\n1. **Completeness** (1-10): Does the article fully answer the key questions from the gap analysis?\n2. **Clarity** (1-10): Is the language clear and jargon-free for the target audience?\n3. **Structure** (1-10): Are headings logical? Is there a good flow?\n4. **Actionability** (1-10): Can the reader follow instructions and solve their problem?\n5. **Tone** (1-10): Professional, friendly, brand-appropriate?\n6. **SEO Readiness** (1-10): Good title, description, headings for discoverability?\n\n## OUTPUT FORMAT\n\nRespond with valid JSON only:\n{\n  \"overall_score\": number,\n  \"scores\": {\n    \"completeness\": number,\n    \"clarity\": number,\n    \"structure\": number,\n    \"actionability\": number,\n    \"tone\": number,\n    \"seo_readiness\": number\n  },\n  \"verdict\": \"publish|revise|reject\",\n  \"issues\": [\"string\"],\n  \"suggested_improvements\": [\"string\"],\n  \"revised_title\": \"string (only if original needs improvement, else null)\",\n  \"revised_summary\": \"string (only if original needs improvement, else null)\"\n}",
        "options": {
          "systemMessage": "You are an expert content quality reviewer for knowledge base articles. You evaluate articles on completeness, clarity, structure, actionability, tone, and SEO readiness. Be constructive but rigorous \u2014 only articles scoring 7+ overall should be marked as 'publish'. Always output valid JSON."
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "6eb182a1-dd9c-4228-8b62-136cf80aacef",
      "name": "OpenAI GPT-4.1-mini (Quality Review)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        3776,
        1024
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {
          "maxTokens": 2048,
          "temperature": 0.1
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "17af806b-d502-4db1-baf0-b18d26e93764",
      "name": "Combine Article + Review",
      "type": "n8n-nodes-base.code",
      "position": [
        4048,
        800
      ],
      "parameters": {
        "jsCode": "// Parse the quality review and combine with the article\nconst output = $input.first().json;\nlet review;\n\ntry {\n  const raw = output.output || output.text || JSON.stringify(output);\n  const jsonMatch = raw.match(/\\{[\\s\\S]*\\}/);\n  review = JSON.parse(jsonMatch ? jsonMatch[0] : raw);\n} catch (e) {\n  review = {\n    overall_score: 0,\n    verdict: 'revise',\n    issues: ['Could not parse quality review'],\n    _error: e.message\n  };\n}\n\n// Get the original article from upstream\nconst articleData = $('Parse Drafted Article').first().json;\n\nreturn [{\n  json: {\n    article: articleData.drafted_article,\n    review: review,\n    verdict: review.verdict,\n    score: review.overall_score,\n    ready_to_publish: review.verdict === 'publish'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "2557dded-bc16-4acf-a573-58a22b160817",
      "name": "Staleness Check",
      "type": "n8n-nodes-base.code",
      "notes": "Checks all existing KB articles for staleness based on the configured threshold (default: 90 days).",
      "position": [
        3120,
        1104
      ],
      "parameters": {
        "jsCode": "// Staleness check on existing KB articles\nconst data = $input.first().json;\nconst articles = data.articles || [];\nconst staleDays = data.config?.staleDays || 90;\nconst analysis = data.analysis || {};\n\nconst now = new Date();\nconst staleArticles = [];\n\nfor (const article of articles) {\n  if (!article.last_updated) continue;\n  const updated = new Date(article.last_updated);\n  const daysSince = Math.floor((now - updated) / (1000 * 60 * 60 * 24));\n\n  if (daysSince > staleDays) {\n    staleArticles.push({\n      title: article.title,\n      category: article.category,\n      last_updated: article.last_updated,\n      days_since_update: daysSince,\n      view_count: article.view_count,\n      vote_sum: article.vote_sum,\n      url: article.url,\n      priority: daysSince > 180 ? 'high' : daysSince > 120 ? 'medium' : 'low'\n    });\n  }\n}\n\n// Sort by priority (high first) then by days since update\nstaleArticles.sort((a, b) => {\n  const prio = { high: 0, medium: 1, low: 2 };\n  return (prio[a.priority] - prio[b.priority]) || (b.days_since_update - a.days_since_update);\n});\n\nreturn [{\n  json: {\n    staleArticles: staleArticles,\n    staleCount: staleArticles.length,\n    analysis: analysis\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "db7549c4-426e-4da7-873d-a0c8b2ae365f",
      "name": "Merge All Results",
      "type": "n8n-nodes-base.merge",
      "notes": "Combines drafted articles (with quality reviews) and staleness data for the final report.",
      "position": [
        4240,
        960
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3
    },
    {
      "id": "a9c12eaf-b463-44bd-bfff-77d084f37a5b",
      "name": "Report Generator Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "notes": "\ud83e\udd16 AGENT 4: Generates an executive-friendly Knowledge Base Health Report with scores, trends, and prioritized actions.",
      "position": [
        4432,
        960
      ],
      "parameters": {
        "text": "You are an Executive Report Generator AI agent. Compile a comprehensive Knowledge Base Health Report from the following data.\n\n## GAP ANALYSIS SUMMARY\n{{ JSON.stringify($json.analysis?.summary || {}, null, 2) }}\n\n## IDENTIFIED GAPS\n{{ JSON.stringify($json.analysis?.gaps || [], null, 2) }}\n\n## WEAK COVERAGE AREAS\n{{ JSON.stringify($json.analysis?.weak_coverage || [], null, 2) }}\n\n## STALE ARTICLES ({{ $json.staleCount || 0 }} found)\n{{ JSON.stringify(($json.staleArticles || []).slice(0, 20), null, 2) }}\n\n## DRAFTED ARTICLES (if any)\n{{ JSON.stringify($json.drafted_articles || [], null, 2) }}\n\n## YOUR TASK\n\nGenerate an executive-friendly Knowledge Base Health Report that includes:\n\n1. **Health Score** \u2014 Overall KB health (0-100) with a letter grade (A-F)\n2. **Executive Summary** \u2014 2-3 sentence overview of KB state\n3. **Key Findings** \u2014 Bullet points of the most important discoveries\n4. **Gaps Requiring Immediate Action** \u2014 Priority-ordered list with expected ticket deflection impact\n5. **Stale Content Alert** \u2014 Articles that need urgent review\n6. **Articles Drafted This Cycle** \u2014 Summary of auto-drafted articles with quality scores\n7. **Recommended Actions** \u2014 Prioritized next steps for the knowledge team\n8. **Trend Indicator** \u2014 Whether KB health is improving, stable, or declining (based on gap count vs article count ratio)\n\n## OUTPUT FORMAT\n\nRespond with valid JSON:\n{\n  \"report_date\": \"string (ISO date)\",\n  \"health_score\": number,\n  \"health_grade\": \"A|B|C|D|F\",\n  \"executive_summary\": \"string\",\n  \"key_findings\": [\"string\"],\n  \"critical_gaps\": [\n    {\n      \"topic\": \"string\",\n      \"urgency\": \"string\",\n      \"estimated_ticket_deflection\": \"string\",\n      \"action_required\": \"string\"\n    }\n  ],\n  \"stale_content_alerts\": [\n    {\n      \"article\": \"string\",\n      \"days_stale\": number,\n      \"action\": \"string\"\n    }\n  ],\n  \"articles_drafted\": [\n    {\n      \"title\": \"string\",\n      \"quality_score\": number,\n      \"verdict\": \"string\"\n    }\n  ],\n  \"recommended_actions\": [\n    {\n      \"priority\": number,\n      \"action\": \"string\",\n      \"expected_impact\": \"string\",\n      \"effort\": \"low|medium|high\"\n    }\n  ],\n  \"trend\": \"improving|stable|declining\",\n  \"slack_summary\": \"string (2-3 line summary for Slack notification)\"\n}",
        "options": {
          "systemMessage": "You are an expert at creating actionable executive reports from data. Your reports are concise, data-driven, and focused on driving decisions. Always output valid JSON. Focus on business impact: ticket deflection, time savings, and customer satisfaction."
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "779e42d8-a74e-469c-b078-b1bb1c820164",
      "name": "OpenAI GPT-4.1-mini (Report Generator)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        4432,
        1168
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {
          "maxTokens": 4096,
          "temperature": 0.3
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "54625db3-4068-4e9c-ae99-554801d033a5",
      "name": "Format Outputs",
      "type": "n8n-nodes-base.code",
      "notes": "Formats the report into Slack blocks and a full JSON report for downstream use.",
      "position": [
        4736,
        960
      ],
      "parameters": {
        "jsCode": "// Parse the final report and format for Slack + Email\nconst output = $input.first().json;\nlet report;\n\ntry {\n  const raw = output.output || output.text || JSON.stringify(output);\n  const jsonMatch = raw.match(/\\{[\\s\\S]*\\}/);\n  report = JSON.parse(jsonMatch ? jsonMatch[0] : raw);\n} catch (e) {\n  report = {\n    executive_summary: 'Report generation encountered an error: ' + e.message,\n    health_score: 0,\n    health_grade: 'F',\n    critical_gaps: [],\n    recommended_actions: [],\n    stale_articles: [],\n    metrics: {}\n  };\n}\n\n// Determine health emoji\nconst score = report.health_score || 0;\nconst grade = report.health_grade || 'N/A';\nlet healthEmoji = '\ud83d\udd34';\nif (score >= 90) healthEmoji = '\ud83d\udfe2';\nelse if (score >= 75) healthEmoji = '\ud83d\udfe1';\nelse if (score >= 60) healthEmoji = '\ud83d\udfe0';\n\nconst config = $('Configuration').first().json;\n\n// === SLACK PAYLOAD ===\nconst slackBlocks = {\n  blocks: [\n    { type: 'header', text: { type: 'plain_text', text: healthEmoji + ' KB Health Report: ' + grade + ' (' + score + '/100)' } },\n    { type: 'section', text: { type: 'mrkdwn', text: '*Executive Summary*\\n' + (report.executive_summary || 'No summary available') } },\n    { type: 'divider' },\n    { type: 'section', text: { type: 'mrkdwn', text: '*Key Findings*\\n\u2022 Critical Gaps: ' + (report.critical_gaps?.length || 0) + '\\n\u2022 Stale Articles: ' + (report.stale_articles?.length || 0) + '\\n\u2022 Recommended Actions: ' + (report.recommended_actions?.length || 0) } }\n  ]\n};\n\nif (report.recommended_actions?.length > 0) {\n  slackBlocks.blocks.push({ type: 'divider' });\n  slackBlocks.blocks.push({ type: 'section', text: { type: 'mrkdwn', text: '*Top Actions*\\n' + report.recommended_actions.slice(0, 5).map((a, i) => (i+1) + '. ' + (a.action || a.title || a)).join('\\n') } });\n}\n\n// === EMAIL HTML ===\nconst scoreColor = score >= 90 ? '#22c55e' : score >= 75 ? '#eab308' : score >= 60 ? '#f97316' : '#ef4444';\n\nlet gapsHtml = '';\nif (report.critical_gaps?.length > 0) {\n  gapsHtml = '<table style=\"width:100%;border-collapse:collapse;margin:16px 0\"><tr style=\"background:#f1f5f9\"><th style=\"text-align:left;padding:8px;border:1px solid #e2e8f0\">Gap</th><th style=\"text-align:left;padding:8px;border:1px solid #e2e8f0\">Urgency</th><th style=\"text-align:left;padding:8px;border:1px solid #e2e8f0\">Impact</th></tr>';\n  report.critical_gaps.forEach(g => {\n    const urgencyColor = (g.urgency||'').toLowerCase() === 'high' ? '#ef4444' : (g.urgency||'').toLowerCase() === 'medium' ? '#f97316' : '#22c55e';\n    gapsHtml += '<tr><td style=\"padding:8px;border:1px solid #e2e8f0\">' + (g.topic || g.title || 'Unknown') + '</td><td style=\"padding:8px;border:1px solid #e2e8f0\"><span style=\"background:' + urgencyColor + ';color:white;padding:2px 8px;border-radius:12px;font-size:12px\">' + (g.urgency || 'N/A') + '</span></td><td style=\"padding:8px;border:1px solid #e2e8f0\">' + (g.impact || g.ticket_volume || 'N/A') + '</td></tr>';\n  });\n  gapsHtml += '</table>';\n}\n\nlet actionsHtml = '';\nif (report.recommended_actions?.length > 0) {\n  actionsHtml = '<table style=\"width:100%;border-collapse:collapse;margin:16px 0\"><tr style=\"background:#f1f5f9\"><th style=\"text-align:left;padding:8px;border:1px solid #e2e8f0\">#</th><th style=\"text-align:left;padding:8px;border:1px solid #e2e8f0\">Action</th><th style=\"text-align:left;padding:8px;border:1px solid #e2e8f0\">Priority</th></tr>';\n  report.recommended_actions.forEach((a, i) => {\n    actionsHtml += '<tr><td style=\"padding:8px;border:1px solid #e2e8f0\">' + (i+1) + '</td><td style=\"padding:8px;border:1px solid #e2e8f0\">' + (a.action || a.title || a) + '</td><td style=\"padding:8px;border:1px solid #e2e8f0\">' + (a.priority || 'Normal') + '</td></tr>';\n  });\n  actionsHtml += '</table>';\n}\n\nconst emailHtml = `<div style=\"font-family:Arial,sans-serif;max-width:640px;margin:0 auto;padding:24px\">\n  <div style=\"text-align:center;margin-bottom:24px\">\n    <div style=\"display:inline-block;width:80px;height:80px;line-height:80px;border-radius:50%;background:${scoreColor};color:white;font-size:28px;font-weight:bold\">${score}</div>\n    <h1 style=\"margin:12px 0 4px;color:#1e293b\">${healthEmoji} KB Health Report</h1>\n    <p style=\"color:#64748b;margin:0\">Grade: ${grade} | Generated: ${new Date().toLocaleDateString()}</p>\n  </div>\n  <div style=\"background:#f8fafc;border-left:4px solid ${scoreColor};padding:16px;margin:16px 0;border-radius:0 8px 8px 0\">\n    <h3 style=\"margin:0 0 8px;color:#1e293b\">Executive Summary</h3>\n    <p style=\"margin:0;color:#475569\">${report.executive_summary || 'No summary available'}</p>\n  </div>\n  <div style=\"display:flex;gap:12px;margin:16px 0\">\n    <div style=\"flex:1;background:#fef2f2;padding:12px;border-radius:8px;text-align:center\"><strong style=\"font-size:24px;color:#ef4444\">${report.critical_gaps?.length || 0}</strong><br><span style=\"color:#64748b;font-size:13px\">Critical Gaps</span></div>\n    <div style=\"flex:1;background:#fefce8;padding:12px;border-radius:8px;text-align:center\"><strong style=\"font-size:24px;color:#eab308\">${report.stale_articles?.length || 0}</strong><br><span style=\"color:#64748b;font-size:13px\">Stale Articles</span></div>\n    <div style=\"flex:1;background:#eff6ff;padding:12px;border-radius:8px;text-align:center\"><strong style=\"font-size:24px;color:#3b82f6\">${report.recommended_actions?.length || 0}</strong><br><span style=\"color:#64748b;font-size:13px\">Actions</span></div>\n  </div>\n  ${gapsHtml ? '<h2 style=\"color:#1e293b;border-bottom:2px solid #e2e8f0;padding-bottom:8px\">Critical Gaps</h2>' + gapsHtml : ''}\n  ${actionsHtml ? '<h2 style=\"color:#1e293b;border-bottom:2px solid #e2e8f0;padding-bottom:8px\">Recommended Actions</h2>' + actionsHtml : ''}\n  <div style=\"margin-top:24px;padding:16px;background:#f8fafc;border-radius:8px;text-align:center;color:#94a3b8;font-size:12px\">Generated by AI KB Auto-Curator | ${config.companyName || 'YourCompany'}</div>\n</div>`;\n\nconst emailSubject = healthEmoji + ' KB Health Report: Score ' + score + '/100 (' + grade + ')';\n\n// Return TWO items: one for Slack, one for Email\nreturn [\n  { json: { report, slackPayload: slackBlocks, type: 'slack_notification' } },\n  { json: { report, emailHtml, emailSubject, type: 'email_report' } }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "9699e574-bd6c-4c2c-9437-e818b7119247",
      "name": "Send Slack Report",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Sends the formatted KB health report to Slack via webhook. Replace the webhook URL in the Configuration node.",
      "onError": "continueRegularOutput",
      "position": [
        4896,
        848
      ],
      "parameters": {
        "url": "={{ $('Configuration').first().json.slackWebhookUrl }}",
        "method": "POST",
        "options": {
          "timeout": 10000
        },
        "jsonBody": "={{ JSON.stringify($json.slackPayload) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "6e9482f2-5de4-459e-93d3-b6bcc499a423",
      "name": "Create Draft Articles in KB",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Creates draft articles in your KB platform. Customize the API endpoint and payload format for your specific platform (Zendesk Guide, Notion, Confluence, etc.).",
      "position": [
        4896,
        1056
      ],
      "parameters": {
        "url": "={{ $('Configuration').first().json.kbApiUrl }}",
        "method": "POST",
        "options": {
          "timeout": 15000
        },
        "jsonBody": "={{ JSON.stringify({ title: $json.report.articles_drafted?.[0]?.title || 'Draft Article', body: 'See full report', status: 'draft' }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "ec558471-495e-4fd9-911f-5354fb690cfa",
      "name": "No Gaps \u2014 Skip Drafting",
      "type": "n8n-nodes-base.code",
      "notes": "When no gaps are found, this node prepares a clean report payload and skips article drafting.",
      "position": [
        3120,
        960
      ],
      "parameters": {
        "jsCode": "// No gaps found \u2014 generate a 'clean' report\nconst data = $input.first().json;\n\nreturn [{\n  json: {\n    analysis: data.analysis || { summary: { total_gaps_found: 0, overall_kb_health_score: 90 } },\n    staleArticles: [],\n    staleCount: 0,\n    drafted_articles: [],\n    articles: data.articles || [],\n    config: data.config || {},\n    noGapsMessage: 'No knowledge gaps detected in this cycle. Your KB is in good shape!'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "89aa83aa-c1b3-4939-8bc8-c0b511147d1b",
      "name": "On Error",
      "type": "n8n-nodes-base.errorTrigger",
      "notes": "Catches any workflow errors for logging and alerting.",
      "position": [
        1136,
        1296
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "11c32a0b-4469-4432-850a-a96c24656ea2",
      "name": "Send Error Alert",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Sends error notifications to Slack so the team knows if the auto-curator fails.",
      "position": [
        1360,
        1296
      ],
      "parameters": {
        "url": "={{ $('Configuration').first().json.slackWebhookUrl }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ blocks: [{ type: 'section', text: { type: 'mrkdwn', text: '\ud83d\udea8 *KB Auto-Curator Error*\\n```' + ($json.message || 'Unknown error') + '```\\nWorkflow execution failed. Please check n8n for details.' } }] }) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "34913189-9c7e-47c6-8b57-faa8994ae056",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "notes": "Use this to test the workflow manually before enabling the daily schedule.",
      "position": [
        1136,
        704
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "614f6c56-03b6-4096-b333-7d2a5e69ab50",
      "name": "Send Email Report",
      "type": "n8n-nodes-base.gmail",
      "onError": "continueRegularOutput",
      "position": [
        5120,
        960
      ],
      "parameters": {
        "sendTo": "={{ $('Configuration').first().json.emailTo }}",
        "message": "={{ $json.emailHtml || '<p>No report generated</p>' }}",
        "options": {},
        "subject": "={{ $json.emailSubject || 'KB Health Report' }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "e1369242-6762-4199-98bb-74a3cc404199",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        512,
        704
      ],
      "parameters": {
        "width": 520,
        "height": 620,
        "content": "# AI Multi-Agent KB Auto-Curator & Gap Analyzer\n\nKeeps your knowledge base in sync with the questions your customers actually ask. The workflow compares recent support tickets against your existing KB, drafts new articles for the gaps, and reports the health of your KB to Slack and email.\n\n### How it works\n\n1. Fetches recent tickets and the current KB from your helpdesk API.\n2. A Gap Analysis agent (GPT-4.1) finds missing topics, weak coverage, and stale content.\n3. For each gap, a Drafter agent writes a complete article using real ticket data as context; a Reviewer agent (GPT-4.1-mini) scores each draft on accuracy, completeness, clarity, and actionability.\n4. A Report agent produces a 0-100 health score, letter grade, executive summary, and prioritised actions.\n5. In parallel, a staleness check flags any article not updated within a configurable threshold.\n6. Results are sent to Slack (blocks) and Gmail (HTML). Either channel can be disabled.\n\n### Setup\n\n1. Add OpenAI credentials to the 4 model nodes (2x GPT-4.1, 2x GPT-4.1-mini).\n2. Connect Gmail OAuth on the email report node.\n3. Edit the Configuration node: helpdesk URLs, Slack webhook, email addresses, gap and staleness thresholds.\n4. Add your helpdesk auth header to the 2 HTTP Request fetch nodes.\n5. Run once manually. When it works end-to-end, attach a schedule trigger."
      },
      "typeVersion": 1
    },
    {
      "id": "e66ec9df-12f6-494e-8893-ec4ee9c03f46",
      "name": "Data Collection Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1536,
        560
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 200,
        "content": "## Stage 1 - Data collection\n\nPulls recent support tickets and current KB articles from your helpdesk. Normalises both into a shared shape so the agents downstream can compare them."
      },
      "typeVersion": 1
    },
    {
      "id": "a2dc2899-6a30-4dce-8ff3-c6a5df5ea6fa",
      "name": "Gap Analysis Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2464,
        624
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 230,
        "content": "## Stage 2 - Gap analysis\n\nGPT-4.1 cross-references tickets against KB articles. Outputs missing topics, weak coverage, and stale content, each with an urgency rating and estimated ticket-volume impact. If no gaps are found, drafting is skipped but the run still produces a report."
      },
      "typeVersion": 1
    },
    {
      "id": "3c115685-e6ca-4484-a1a2-d13a0cc6dc54",
      "name": "Drafting & Review Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3312,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 740,
        "height": 220,
        "content": "## Stage 3 - Draft and review\n\nDrafter agent (GPT-4.1) writes a full article for each gap using your ticket data as context. Reviewer agent (GPT-4.1-mini) scores each draft on accuracy, completeness, clarity, and actionability."
      },
      "typeVersion": 1
    },
    {
      "id": "fd4c62c5-11e9-4c55-a226-e02c01e55281",
      "name": "Output & Notifications Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4432,
        512
      ],
      "parameters": {
        "color": 7,
        "width": 800,
        "height": 260,
        "content": "## Stage 4 - Report and notify\n\nReport agent (GPT-4.1-mini) produces a 0-100 health score, letter grade, summary, and prioritised action items. Sent via Slack and Gmail. Either channel can be disabled in the Configuration node."
      },
      "typeVersion": 1
    },
    {
      "id": "e4c90b32-4a99-4f4f-b8a1-59048f790ccd",
      "name": "Staleness Check Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3104,
        1312
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 180,
        "content": "## Staleness check\n\nRuns in parallel with the main pipeline. Flags any KB article not updated within the staleDays threshold in Configuration (default 90). Results feed into the health report."
      },
      "typeVersion": 1
    },
    {
      "id": "5605c2fc-2e24-4d36-8d2a-548ce9713799",
      "name": "Error Handling Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        1472
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 200,
        "content": "## Error handling\n\nThe Error Trigger catches any node failure and posts to the webhook in the Configuration node. Notification nodes use `continueRegularOutput` so one channel failing wont block the other."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "50856c49-e524-40df-99c3-2cafd5716291",
  "connections": {
    "On Error": {
      "main": [
        [
          {
            "node": "Send Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gaps Found?": {
      "main": [
        [
          {
            "node": "Split Into Individual Gaps",
            "type": "main",
            "index": 0
          },
          {
            "node": "Staleness Check",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Gaps \u2014 Skip Drafting",
            "type": "main",
            "index": 0
          },
          {
            "node": "Staleness Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Fetch Recent Support Tickets",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch KB Articles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Schedule": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Outputs": {
      "main": [
        [
          {
            "node": "Send Slack Report",
            "type": "main",
            "index": 0
          },
          {
            "node": "Create Draft Articles in KB",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Email Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Staleness Check": {
      "main": [
        [
          {
            "node": "Merge All Results",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Fetch KB Articles": {
      "main": [
        [
          {
            "node": "Normalize KB Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge All Results": {
      "main": [
        [
          {
            "node": "Report Generator Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize KB Data": {
      "main": [
        [
          {
            "node": "Merge Tickets + KB",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Gap Analysis Agent": {
      "main": [
        [
          {
            "node": "Parse Gap Analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Tickets + KB": {
      "main": [
        [
          {
            "node": "Prepare Analysis Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Gap Analysis": {
      "main": [
        [
          {
            "node": "Gaps Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Article Drafter Agent": {
      "main": [
        [
          {
            "node": "Parse Drafted Article",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Ticket Data": {
      "main": [
        [
          {
            "node": "Merge Tickets + KB",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Drafted Article": {
      "main": [
        [
          {
            "node": "Quality Reviewer Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Quality Reviewer Agent": {
      "main": [
        [
          {
            "node": "Combine Article + Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Report Generator Agent": {
      "main": [
        [
          {
            "node": "Format Outputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Article + Review": {
      "main": [
        [
          {
            "node": "Merge All Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Analysis Payload": {
      "main": [
        [
          {
            "node": "Gap Analysis Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No Gaps \u2014 Skip Drafting": {
      "main": [
        [
          {
            "node": "Merge All Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Into Individual Gaps": {
      "main": [
        [
          {
            "node": "Article Drafter Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Recent Support Tickets": {
      "main": [
        [
          {
            "node": "Normalize Ticket Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI GPT-4.1 (Gap Analysis)": {
      "ai_languageModel": [
        [
          {
            "node": "Gap Analysis Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI GPT-4.1 (Article Drafter)": {
      "ai_languageModel": [
        [
          {
            "node": "Article Drafter Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI GPT-4.1-mini (Quality Review)": {
      "ai_languageModel": [
        [
          {
            "node": "Quality Reviewer Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI GPT-4.1-mini (Report Generator)": {
      "ai_languageModel": [
        [
          {
            "node": "Report Generator Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Support teams, knowledge managers, and ops builders who are drowning in outdated KB articles and repeat tickets. If your team keeps answering the same questions because your knowledge base has gaps nobody has time to find — this template finds them automatically and writes the…

Source: https://n8n.io/workflows/14951/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

The Multi-Model Agency Content Engine is a high-performance editorial system designed for agencies. It solves the "blank page" problem by alternating between real-world social proof and strategic expe

Google Sheets, Gmail, Google Drive +6
AI & RAG

This workflow automates short-interval market signal evaluation for intraday trading using live technical indicators and deterministic decision logic. It is designed for traders, analysts, and automat

HTTP Request, Agent, OpenAI Chat +5
AI & RAG

This n8n automation workflow automates the creation, scripting, production, and posting of YouTube videos. It leverages AI (OpenAI), image generation (PIAPI), video rendering (Shotstack), and platform

Agent, OpenAI Chat, Airtable Tool +7
AI & RAG

Created by: Peyton Leveillee Last updated: October 2025

OpenAI Chat, Google Sheets, HTTP Request +5
AI & RAG

This workflow automates the creation, rendering, approval, and posting of TikTok-style POV (Point of View) videos to Instagram, with cross-posting to Facebook and YouTube. It eliminates manual video p

OpenAI Chat, Output Parser Item List, HTTP Request +10