This workflow corresponds to n8n.io template #13597 — we link there as the canonical source.
This workflow follows the Google Sheets → HTTP Request 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 →
{
"id": "s4ccm6m0FaJRj60R",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Assess Technical Documentation Compliance with AI and Trigger Slack Alerts",
"tags": [],
"nodes": [
{
"id": "ca1ebea3-ef14-4220-8725-598530559057",
"name": "When clicking 'Execute workflow'",
"type": "n8n-nodes-base.manualTrigger",
"position": [
544,
544
],
"parameters": {},
"typeVersion": 1
},
{
"id": "6708020c-1b52-400e-a104-264a9917f816",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"width": 460,
"height": 992,
"content": "### This n8n template demonstrates how to use AI to automatically review technical documentation against predefined compliance standards \u2014 and alert your team via Slack.\n\nUse cases: Engineering teams maintaining API docs, product teams enforcing terminology standards, or localization teams checking readiness before translation handoff.\n\n### How it works\n* A document URL or raw text is passed in via manual trigger (or webhook).\n* The document content is fetched and extracted as plain text.\n* An AI model scores the document across 4 compliance dimensions: Structure, Terminology, Localization Readiness, and Completeness.\n* The AI returns a structured JSON report with scores and gap descriptions per dimension.\n* A compliance status is determined: PASS / WARNING / FAIL based on the overall score.\n* A formatted Slack alert is sent to the appropriate channel based on status severity.\n* All results are logged for historical tracking.\n\n### How to use\n* Replace the document URL in the `Set Document Input` node with your own source.\n* Update the AI prompt in the `Score Documentation with AI` node to match your standards.\n* Add your Slack webhook URLs for each severity channel.\n* Optionally connect to Google Sheets or PostgreSQL for audit logging.\n\n### Requirements\n* OpenAI or Anthropic API key for AI scoring\n* Slack incoming webhook URLs\n* Document accessible via URL or passed as text\n\n### Need Help?\nJoin the [Discord](https://discord.com/invite/XPKeKXeB7d) or ask in the [Forum](https://community.n8n.io/)!\n\n"
},
"typeVersion": 1
},
{
"id": "3a6f5078-932a-4d58-8cc9-40dbe42d9ae4",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
496,
288
],
"parameters": {
"color": 7,
"width": 1012,
"height": 460,
"content": "## 1. Fetch & Prepare Documentation\n[Read more about the HTTP Request node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/)\n\nThe document is fetched from a URL or passed as raw text. The content is then cleaned and prepared for AI analysis. Supports HTML pages, markdown files, plain text, or API-served documentation."
},
"typeVersion": 1
},
{
"id": "6c284627-75d4-4241-a12e-9a3f44287067",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1568,
288
],
"parameters": {
"color": 7,
"width": 580,
"height": 460,
"content": "## 2. Score Documentation with AI\n[Read more about the HTTP Request node for AI APIs](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/)\n\nThe AI model evaluates the document against 4 compliance dimensions: Structure (headings, sections), Terminology (approved glossary), Localization Readiness (i18n-safe language), and Completeness (required sections present). Each is scored 0\u2013100 and gaps are described."
},
"typeVersion": 1
},
{
"id": "a27b8e69-6540-44ea-9947-586ecfeba478",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2208,
288
],
"parameters": {
"color": 7,
"width": 580,
"height": 460,
"content": "## 3. Parse Results & Route by Status\n[Read more about the Switch node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.switch/)\n\nThe AI's JSON response is parsed and an overall compliance score is calculated. The Switch node routes the result to the correct Slack channel based on status: FAIL (score < 60) goes to the engineering alert channel, WARNING (60\u201379) to the review channel, and PASS (80+) to the general docs channel."
},
"typeVersion": 1
},
{
"id": "d952a438-5f85-4c13-8232-d4dd3557ef5f",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
2832,
112
],
"parameters": {
"color": 7,
"width": 480,
"height": 940,
"content": "## 4. Send Slack Compliance Report\n[Read more about Slack webhooks](https://api.slack.com/messaging/webhooks)\n\nA richly formatted Slack message is sent to the appropriate channel with: overall score, per-dimension breakdown, detected gaps, and a recommended action. Critical failures also trigger a direct @channel mention to ensure immediate visibility."
},
"typeVersion": 1
},
{
"id": "f72b0e25-bc83-4470-a95b-e88af852a163",
"name": "Set Document Input",
"type": "n8n-nodes-base.set",
"position": [
784,
544
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "doc-input-001",
"name": "doc_url",
"type": "string",
"value": "https://your-docs-site.com/api/endpoint-reference"
},
{
"id": "doc-input-002",
"name": "doc_title",
"type": "string",
"value": "API Endpoint Reference v2.1"
},
{
"id": "doc-input-003",
"name": "doc_type",
"type": "string",
"value": "API Reference"
},
{
"id": "doc-input-004",
"name": "review_date",
"type": "string",
"value": "={{ new Date().toISOString().split('T')[0] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "8a0d7d09-62c6-4a3a-acd9-e5ebc84aa61b",
"name": "Fetch Document Content",
"type": "n8n-nodes-base.httpRequest",
"position": [
1024,
544
],
"parameters": {
"url": "={{ $json.doc_url }}",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
}
}
},
"typeVersion": 4.2
},
{
"id": "defe8b77-176d-4cf3-922c-5ad433af389c",
"name": "Prepare Document for AI",
"type": "n8n-nodes-base.code",
"position": [
1280,
544
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Clean and prepare document content for AI compliance scoring\nconst rawContent = $input.item.json.data || '';\nconst docMeta = $input.item.json;\n\n// Strip HTML tags if present, normalize whitespace\nconst cleanContent = rawContent\n .replace(/<[^>]*>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\\s+/g, ' ')\n .trim();\n\n// Truncate to ~6000 chars to stay within token limits\nconst truncated = cleanContent.length > 6000\n ? cleanContent.substring(0, 6000) + '... [truncated for analysis]'\n : cleanContent;\n\nconst wordCount = cleanContent.split(/\\s+/).length;\nconst hasCodeBlocks = rawContent.includes('```') || rawContent.includes('<code>');\nconst hasHeaders = rawContent.includes('#') || /<h[1-6]/i.test(rawContent);\nconst hasExamples = /example|sample|e\\.g\\.|for instance/i.test(cleanContent);\nconst hasWarnings = /warning|caution|note|important|deprecated/i.test(cleanContent);\n\nreturn {\n json: {\n doc_title: docMeta.doc_title || 'Untitled Document',\n doc_type: docMeta.doc_type || 'Unknown',\n doc_url: docMeta.doc_url || '',\n review_date: docMeta.review_date || new Date().toISOString().split('T')[0],\n clean_content: truncated,\n word_count: wordCount,\n pre_analysis: {\n has_code_blocks: hasCodeBlocks,\n has_headers: hasHeaders,\n has_examples: hasExamples,\n has_warnings: hasWarnings\n }\n }\n};"
},
"typeVersion": 2
},
{
"id": "9cbbc2a0-94e6-4e32-b8f1-1e27d4c2ebd7",
"name": "Score Documentation with AI",
"type": "n8n-nodes-base.httpRequest",
"position": [
1664,
544
],
"parameters": {
"url": "https://api.openai.com/v1/chat/completions",
"method": "POST",
"options": {},
"jsonBody": "={\n \"model\": \"gpt-4o\",\n \"temperature\": 0.2,\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You are a technical documentation compliance auditor. Evaluate documentation against these 4 dimensions and return ONLY valid JSON \u2014 no markdown, no explanation.\\n\\nDimensions:\\n1. Structure (0-100): Logical headings hierarchy, introduction, clear sections, conclusion/summary\\n2. Terminology (0-100): Consistent use of approved terms, no jargon, no ambiguous language\\n3. Localization Readiness (0-100): No idioms, date/number formats are locale-neutral, text is translation-friendly\\n4. Completeness (0-100): All required sections present (overview, prerequisites, steps/usage, parameters if API, examples, error handling)\\n\\nReturn this exact JSON structure:\\n{\\\"structure\\\": {\\\"score\\\": 0, \\\"gaps\\\": []}, \\\"terminology\\\": {\\\"score\\\": 0, \\\"gaps\\\": []}, \\\"localization\\\": {\\\"score\\\": 0, \\\"gaps\\\": []}, \\\"completeness\\\": {\\\"score\\\": 0, \\\"gaps\\\": []}, \\\"summary\\\": \\\"one sentence overall assessment\\\"}\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Document Title: {{ $json.doc_title }}\\nDocument Type: {{ $json.doc_type }}\\nWord Count: {{ $json.word_count }}\\n\\nContent:\\n{{ $json.clean_content }}\"\n }\n ]\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi"
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "2ebe691b-cff6-4cf0-be3e-15265943cf7b",
"name": "Parse AI Compliance Report",
"type": "n8n-nodes-base.code",
"position": [
1920,
544
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Parse AI response and calculate overall compliance score\nconst aiRaw = $input.item.json.choices?.[0]?.message?.content || '{}';\n\nlet scores;\ntry {\n const cleaned = aiRaw.replace(/```json|```/g, '').trim();\n scores = JSON.parse(cleaned);\n} catch (e) {\n scores = {\n structure: { score: 0, gaps: ['Failed to parse AI response'] },\n terminology: { score: 0, gaps: [] },\n localization: { score: 0, gaps: [] },\n completeness: { score: 0, gaps: [] },\n summary: 'AI response could not be parsed.'\n };\n}\n\nconst structure = scores.structure?.score || 0;\nconst terminology = scores.terminology?.score || 0;\nconst localization = scores.localization?.score || 0;\nconst completeness = scores.completeness?.score || 0;\n\n// Weighted overall score\nconst overallScore = Math.round(\n (structure * 0.25) +\n (terminology * 0.25) +\n (localization * 0.20) +\n (completeness * 0.30)\n);\n\nlet complianceStatus = 'PASS';\nlet statusEmoji = '\u2705';\nlet statusColor = 'good';\nif (overallScore < 60) { complianceStatus = 'FAIL'; statusEmoji = '\ud83d\udea8'; statusColor = 'danger'; }\nelse if (overallScore < 80) { complianceStatus = 'WARNING'; statusEmoji = '\u26a0\ufe0f'; statusColor = 'warning'; }\n\nconst allGaps = [\n ...(scores.structure?.gaps || []).map(g => ({ dimension: 'Structure', gap: g })),\n ...(scores.terminology?.gaps || []).map(g => ({ dimension: 'Terminology', gap: g })),\n ...(scores.localization?.gaps || []).map(g => ({ dimension: 'Localization', gap: g })),\n ...(scores.completeness?.gaps || []).map(g => ({ dimension: 'Completeness', gap: g }))\n];\n\nconst reportId = `DOC-${Date.now()}`;\n\nreturn {\n json: {\n reportId,\n doc_title: $input.item.json.doc_title || 'Unknown',\n doc_type: $input.item.json.doc_type || 'Unknown',\n doc_url: $input.item.json.doc_url || '',\n review_date: $input.item.json.review_date || new Date().toISOString().split('T')[0],\n overallScore,\n complianceStatus,\n statusEmoji,\n statusColor,\n scores: { structure, terminology, localization, completeness },\n gaps: allGaps,\n gapCount: allGaps.length,\n summary: scores.summary || '',\n requiresImmediateAction: complianceStatus === 'FAIL',\n pre_analysis: $input.item.json.pre_analysis || {}\n }\n};"
},
"typeVersion": 2
},
{
"id": "35ffe942-f3b9-4e92-ba56-998abac3ce52",
"name": "Route by Compliance Status",
"type": "n8n-nodes-base.switch",
"position": [
2320,
544
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "FAIL",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.complianceStatus }}",
"rightValue": "FAIL"
}
]
},
"renameOutput": true
},
{
"outputKey": "WARNING",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.complianceStatus }}",
"rightValue": "WARNING"
}
]
},
"renameOutput": true
},
{
"outputKey": "PASS",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.complianceStatus }}",
"rightValue": "PASS"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3
},
{
"id": "d58f37bb-d6e5-4deb-adc6-10abf63f601b",
"name": "Build Slack Report",
"type": "n8n-nodes-base.code",
"position": [
2592,
544
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const d = $input.item.json;\n\nconst bar = (score) => {\n const filled = Math.round(score / 10);\n return '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled) + ` ${score}/100`;\n};\n\nconst gapList = d.gaps.slice(0, 6).map(g => `\u2022 *${g.dimension}:* ${g.gap}`).join('\\n');\n\nconst slackBlocks = [\n {\n type: 'header',\n text: { type: 'plain_text', text: `${d.statusEmoji} Doc Compliance Report \u2014 ${d.complianceStatus}` }\n },\n {\n type: 'section',\n fields: [\n { type: 'mrkdwn', text: `*Document:*\\n${d.doc_title}` },\n { type: 'mrkdwn', text: `*Type:*\\n${d.doc_type}` },\n { type: 'mrkdwn', text: `*Overall Score:*\\n*${d.overallScore}/100*` },\n { type: 'mrkdwn', text: `*Review Date:*\\n${d.review_date}` }\n ]\n },\n {\n type: 'section',\n text: {\n type: 'mrkdwn',\n text: `*Dimension Scores:*\\n\ud83d\udcd0 Structure: ${bar(d.scores.structure)}\\n\ud83d\udd24 Terminology: ${bar(d.scores.terminology)}\\n\ud83c\udf0d Localization: ${bar(d.scores.localization)}\\n\ud83d\udccb Completeness: ${bar(d.scores.completeness)}`\n }\n },\n {\n type: 'section',\n text: {\n type: 'mrkdwn',\n text: `*AI Summary:*\\n${d.summary}`\n }\n },\n ...(d.gaps.length > 0 ? [{\n type: 'section',\n text: {\n type: 'mrkdwn',\n text: `*Detected Gaps (${d.gapCount}):*\\n${gapList}${d.gaps.length > 6 ? `\\n_...and ${d.gaps.length - 6} more_` : ''}`\n }\n }] : [{\n type: 'section',\n text: { type: 'mrkdwn', text: '*No compliance gaps detected \u2705*' }\n }]),\n {\n type: 'context',\n elements: [{ type: 'mrkdwn', text: `Report ID: ${d.reportId} | Source: ${d.doc_url || 'N/A'}` }]\n },\n { type: 'divider' }\n];\n\nreturn {\n json: {\n ...d,\n slackPayload: {\n text: `${d.statusEmoji} Documentation Compliance: *${d.complianceStatus}* (${d.overallScore}/100) \u2014 ${d.doc_title}`,\n blocks: slackBlocks\n }\n }\n};"
},
"typeVersion": 2
},
{
"id": "52627fb3-d244-47f2-988f-6468bf33e0e9",
"name": "Slack \u2014 FAIL Alert (Engineering Channel)",
"type": "n8n-nodes-base.httpRequest",
"position": [
2880,
384
],
"parameters": {
"url": "YOUR_SLACK_FAIL_WEBHOOK_URL",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify($json.slackPayload) }}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.2
},
{
"id": "00fec4bd-5971-44ec-b58a-ac678ae68458",
"name": "Slack \u2014 WARNING Alert (Review Channel)",
"type": "n8n-nodes-base.httpRequest",
"position": [
2880,
592
],
"parameters": {
"url": "YOUR_SLACK_WARNING_WEBHOOK_URL",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify($json.slackPayload) }}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.2
},
{
"id": "10f3e6ac-5c2a-4c26-9126-10d923391df4",
"name": "Slack \u2014 PASS Notice (Docs Channel)",
"type": "n8n-nodes-base.httpRequest",
"position": [
2880,
784
],
"parameters": {
"url": "YOUR_SLACK_PASS_WEBHOOK_URL",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify($json.slackPayload) }}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.2
},
{
"id": "7aab96fa-6518-451c-9ea8-e3a2d30aa5fe",
"name": "Log Result to Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
3184,
592
],
"parameters": {
"columns": {
"value": {},
"schema": [],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "Sheet1",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID",
"cachedResultName": "Doc Compliance Log"
},
"authentication": "serviceAccount"
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.5
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "002daf7e-ff72-46c2-b8f9-d62d7c342deb",
"connections": {
"Build Slack Report": {
"main": [
[
{
"node": "Slack \u2014 FAIL Alert (Engineering Channel)",
"type": "main",
"index": 0
},
{
"node": "Slack \u2014 WARNING Alert (Review Channel)",
"type": "main",
"index": 0
},
{
"node": "Slack \u2014 PASS Notice (Docs Channel)",
"type": "main",
"index": 0
}
]
]
},
"Set Document Input": {
"main": [
[
{
"node": "Fetch Document Content",
"type": "main",
"index": 0
}
]
]
},
"Fetch Document Content": {
"main": [
[
{
"node": "Prepare Document for AI",
"type": "main",
"index": 0
}
]
]
},
"Prepare Document for AI": {
"main": [
[
{
"node": "Score Documentation with AI",
"type": "main",
"index": 0
}
]
]
},
"Parse AI Compliance Report": {
"main": [
[
{
"node": "Route by Compliance Status",
"type": "main",
"index": 0
}
]
]
},
"Route by Compliance Status": {
"main": [
[
{
"node": "Build Slack Report",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Slack Report",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Slack Report",
"type": "main",
"index": 0
}
]
]
},
"Score Documentation with AI": {
"main": [
[
{
"node": "Parse AI Compliance Report",
"type": "main",
"index": 0
}
]
]
},
"When clicking 'Execute workflow'": {
"main": [
[
{
"node": "Set Document Input",
"type": "main",
"index": 0
}
]
]
},
"Slack \u2014 PASS Notice (Docs Channel)": {
"main": [
[
{
"node": "Log Result to Google Sheets",
"type": "main",
"index": 0
}
]
]
},
"Slack \u2014 WARNING Alert (Review Channel)": {
"main": [
[
{
"node": "Log Result to Google Sheets",
"type": "main",
"index": 0
}
]
]
},
"Slack \u2014 FAIL Alert (Engineering Channel)": {
"main": [
[
{
"node": "Log Result to Google Sheets",
"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.
googleApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Use cases: Engineering teams maintaining API docs, product teams enforcing terminology standards, or localization teams checking readiness before translation handoff. A document URL or raw text is passed in via manual trigger (or webhook). The document content is fetched and…
Source: https://n8n.io/workflows/13597/ — 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.
Splitout Code. Uses manualTrigger, httpRequest, stickyNote, splitOut. Event-driven trigger; 46 nodes.
Automate CSV imports into HubSpot without the mess. Powered by n8n. Supercharged by Pollup AI.
AICARE Email Blast System. Uses googleDrive, httpRequest, googleSheets, gmail. Event-driven trigger; 39 nodes.
Get notified if the actual data release is positive or negative for the relevant currency. Use the Telegram chat message about the news release as a trigger to open a trading position in MetaTrader 4.
Automatically processes new orders added to Google Sheets. Small orders are approved instantly; large orders trigger an HTML email with one-click Approve / Reject links — each handled by an independen