This workflow corresponds to n8n.io template #14207 — we link there as the canonical source.
This workflow follows the Agent → 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 →
{
"id": "GRGQy17eKiwTWmlSRmig-",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "AI Markdown Proofreader with GitHub Auto-Commit",
"tags": [],
"nodes": [
{
"id": "33bdd4e6-8838-40e2-b496-09548ba8de69",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
544,
1444
],
"parameters": {},
"typeVersion": 1
},
{
"id": "4f99d9fb-b157-4032-86e3-764320fecafb",
"name": "Has Issues?",
"type": "n8n-nodes-base.if",
"position": [
2016,
1444
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "or",
"conditions": [
{
"id": "b9bee30e-1623-422f-9003-8d8c738c2b65",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.issueCount }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2
},
{
"id": "9d36d265-9c5f-496a-8dcc-6108117f5cb0",
"name": "Edits Applied?",
"type": "n8n-nodes-base.if",
"position": [
3040,
1244
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "918549c9-1a46-4e12-b333-1b07c201cb12",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.success }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "84feed8e-8ab1-403d-ac19-c5eaccedd6ab",
"name": "Fetch Blog Post from GitHub",
"type": "n8n-nodes-base.github",
"position": [
992,
1444
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $json.repoOwner }}"
},
"filePath": "={{ $json.filePath }}",
"resource": "file",
"operation": "get",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $json.repoName }}"
},
"authentication": "oAuth2",
"asBinaryProperty": false,
"additionalParameters": {
"reference": ""
}
},
"typeVersion": 1.1
},
{
"id": "5a646f46-5b55-475d-a8cf-7b16d88b67f6",
"name": "Decode Base64 & Add Line Numbers",
"type": "n8n-nodes-base.code",
"position": [
1216,
1444
],
"parameters": {
"jsCode": "// Decode base64 and add line numbers for QA\n\nconst fileContent = $input.first().json.content;\nconst fileEncoding = $input.first().json.encoding;\nconst fileSha = $input.first().json.sha;\nconst filePath = $input.first().json.path;\n\nlet decodedContent = fileContent;\nif (fileEncoding === 'base64') {\n decodedContent = Buffer.from(fileContent, 'base64').toString('utf-8');\n}\n\nconst lines = decodedContent.split('\\n');\nconst numberedLines = lines.map((line, index) => {\n const lineNumber = index + 1;\n return `${lineNumber}: ${line}`;\n});\nconst numberedContent = numberedLines.join('\\n');\n\nreturn {\n json: {\n originalContent: decodedContent,\n numberedContent: numberedContent,\n lineCount: lines.length,\n sha: fileSha,\n path: filePath\n }\n};"
},
"typeVersion": 2
},
{
"id": "c0c3c967-0d82-4d91-bcdd-3a3b651b9f7f",
"name": "QA Agent - Analyze Content",
"type": "@n8n/n8n-nodes-langchain.agent",
"onError": "continueErrorOutput",
"maxTries": 3,
"position": [
1440,
1444
],
"parameters": {
"text": "=Analyze this blog post and return issues as JSON array:\n\n{{ $json.numberedContent }}",
"options": {
"systemMessage": "You are a content QA specialist. Analyze the provided Markdown blog post and identify issues.\n\nFor each issue found, output a JSON object with these fields:\n- line_number: integer (1-indexed line number where issue occurs)\n- issue_type: one of \"tone\", \"clarity\", \"grammar\", \"structure\", \"accuracy\", \"style\"\n- severity: one of \"low\", \"medium\", \"high\"\n- description: brief explanation of the problem\n- suggested_fix: the corrected text for that line (or null if deletion recommended)\n\nCRITICAL RULES:\n1. Output ONLY valid JSON array. No markdown, no explanation, no preamble.\n2. Each issue must reference a specific line number.\n3. Be precise about line numbers - count from 1, include blank lines.\n4. Focus on substantive issues, not nitpicks.\n5. suggested_fix should be the COMPLETE corrected line, not a partial fix."
},
"promptType": "define",
"needsFallback": true
},
"retryOnFail": true,
"typeVersion": 3.1,
"waitBetweenTries": 5000
},
{
"id": "d0714374-0aa1-43c7-96e7-158188715b7f",
"name": "QA Agent LLM",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
1448,
1668
],
"parameters": {
"options": {}
},
"typeVersion": 1
},
{
"id": "bec92205-1dfc-476d-bf30-3b25584ff92f",
"name": "Parse QA Issues JSON",
"type": "n8n-nodes-base.code",
"position": [
1792,
1444
],
"parameters": {
"jsCode": "// Parse QA output and filter to high/medium severity only\n\nconst qaAgentResponse = $input.first().json.output;\n\nlet cleanedResponse = qaAgentResponse.trim();\nif (cleanedResponse.startsWith('```json')) {\n cleanedResponse = cleanedResponse.slice(7);\n}\nif (cleanedResponse.startsWith('```')) {\n cleanedResponse = cleanedResponse.slice(3);\n}\nif (cleanedResponse.endsWith('```')) {\n cleanedResponse = cleanedResponse.slice(0, -3);\n}\ncleanedResponse = cleanedResponse.trim();\n\ntry {\n const allIssues = JSON.parse(cleanedResponse);\n \n if (!Array.isArray(allIssues)) {\n throw new Error('QA output is not an array');\n }\n \n const validIssues = allIssues.filter(issue => {\n const hasLineNumber = issue.line_number && typeof issue.line_number === 'number';\n const hasIssueType = issue.issue_type;\n const hasDescription = issue.description;\n const isHighOrMedium = issue.severity === 'high' || issue.severity === 'medium';\n \n return hasLineNumber && hasIssueType && hasDescription && isHighOrMedium;\n });\n \n const originalContent = $('Decode Base64 & Add Line Numbers').first().json.originalContent;\n const fileSha = $('Decode Base64 & Add Line Numbers').first().json.sha;\n const filePath = $('Decode Base64 & Add Line Numbers').first().json.path;\n \n return {\n json: {\n issues: validIssues,\n issueCount: validIssues.length,\n originalContent: originalContent,\n sha: fileSha,\n path: filePath,\n parseSuccess: true\n }\n };\n\n} catch (error) {\n return {\n json: {\n issues: [],\n issueCount: 0,\n parseError: error.message,\n rawResponse: qaAgentResponse,\n parseSuccess: false\n }\n };\n}"
},
"typeVersion": 2
},
{
"id": "4ac7a9f7-a656-4c5b-baf3-e7f09ab00af9",
"name": "Editor Agent - Generate Edit Ops",
"type": "@n8n/n8n-nodes-langchain.agent",
"onError": "continueErrorOutput",
"maxTries": 3,
"position": [
2240,
1140
],
"parameters": {
"text": "=Convert these QA issues into edit operations:\n\n{{ JSON.stringify($json.issues, null, 2) }}",
"options": {
"systemMessage": "You are a precise text editor. You receive QA issues and convert them into executable edit instructions.\n\nOutput: A JSON array of edit operations. Each operation must have:\n- operation: one of \"replace\", \"insert_after\", \"delete\"\n- line_number: integer (which line to modify)\n- new_text: string (the new content, required for replace/insert_after, omit for delete)\n\nCRITICAL RULES:\n1. Output ONLY valid JSON array. No markdown, no explanation.\n2. Sort operations by line_number in DESCENDING order (highest first).\n3. Only include operations where suggested_fix is not null.\n4. For \"replace\": new_text is the complete replacement line.\n5. Validate that line_number is a positive integer."
},
"promptType": "define",
"needsFallback": true
},
"retryOnFail": true,
"typeVersion": 3.1,
"waitBetweenTries": 5000
},
{
"id": "7e2ccc24-e5de-4bc4-b3a8-5c35a6018c71",
"name": "Editor Agent LLM",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
2248,
1364
],
"parameters": {
"options": {}
},
"typeVersion": 1
},
{
"id": "1946202a-a925-4fc4-a2f0-f98e09f96aa7",
"name": "Parse Edit Operations JSON",
"type": "n8n-nodes-base.code",
"position": [
2592,
1244
],
"parameters": {
"jsCode": "// Parse editor output and sort edits descending (bottom-to-top)\n\nconst editorAgentResponse = $input.first().json.output;\n\nlet cleanedResponse = editorAgentResponse.trim();\nif (cleanedResponse.startsWith('```json')) {\n cleanedResponse = cleanedResponse.slice(7);\n}\nif (cleanedResponse.startsWith('```')) {\n cleanedResponse = cleanedResponse.slice(3);\n}\nif (cleanedResponse.endsWith('```')) {\n cleanedResponse = cleanedResponse.slice(0, -3);\n}\ncleanedResponse = cleanedResponse.trim();\n\ntry {\n const editOperations = JSON.parse(cleanedResponse);\n \n if (!Array.isArray(editOperations)) {\n throw new Error('Editor output is not an array');\n }\n \n const sortedEdits = editOperations.sort((a, b) => {\n return b.line_number - a.line_number;\n });\n \n const originalContent = $('Parse QA Issues JSON').first().json.originalContent;\n const fileSha = $('Parse QA Issues JSON').first().json.sha;\n const filePath = $('Parse QA Issues JSON').first().json.path;\n const issues = $('Parse QA Issues JSON').first().json.issues;\n \n return {\n json: {\n edits: sortedEdits,\n editCount: sortedEdits.length,\n originalContent: originalContent,\n sha: fileSha,\n path: filePath,\n issues: issues,\n parseSuccess: true\n }\n };\n\n} catch (error) {\n return {\n json: {\n edits: [],\n editCount: 0,\n parseError: error.message,\n rawResponse: editorAgentResponse,\n parseSuccess: false\n }\n };\n}"
},
"typeVersion": 2
},
{
"id": "c83f964b-4ca9-4666-9d26-a89cc99b0700",
"name": "Apply Line-by-Line Edits",
"type": "n8n-nodes-base.code",
"position": [
2816,
1244
],
"parameters": {
"jsCode": "// Apply edits to content, skip invalid lines, track success rate\n\nconst originalContent = $input.first().json.originalContent;\nconst editOperations = $input.first().json.edits;\nconst fileSha = $input.first().json.sha;\nconst filePath = $input.first().json.path;\nconst issues = $input.first().json.issues;\n\nlet lines = originalContent.split('\\n');\nconst totalLines = lines.length;\n\neditOperations.sort((a, b) => b.line_number - a.line_number);\n\nlet appliedCount = 0;\nlet skippedCount = 0;\nconst appliedChanges = [];\nconst skippedChanges = [];\n\nfor (const edit of editOperations) {\n const arrayIndex = edit.line_number - 1;\n \n if (arrayIndex < 0 || arrayIndex >= totalLines) {\n skippedCount++;\n skippedChanges.push({\n line: edit.line_number,\n reason: 'Line number out of range'\n });\n continue;\n }\n \n if (edit.operation === 'replace') {\n appliedChanges.push({\n line: edit.line_number,\n operation: 'replace',\n oldText: lines[arrayIndex],\n newText: edit.new_text\n });\n lines[arrayIndex] = edit.new_text;\n appliedCount++;\n \n } else if (edit.operation === 'insert_after') {\n appliedChanges.push({\n line: edit.line_number,\n operation: 'insert_after',\n insertedText: edit.new_text\n });\n lines.splice(arrayIndex + 1, 0, edit.new_text);\n appliedCount++;\n \n } else if (edit.operation === 'delete') {\n appliedChanges.push({\n line: edit.line_number,\n operation: 'delete',\n deletedText: lines[arrayIndex]\n });\n lines.splice(arrayIndex, 1);\n appliedCount++;\n \n } else {\n skippedCount++;\n skippedChanges.push({\n line: edit.line_number,\n reason: 'Unknown operation: ' + edit.operation\n });\n }\n}\n\nconst totalEdits = appliedCount + skippedCount;\nconst successRate = totalEdits > 0 ? appliedCount / totalEdits : 1;\nconst isSuccess = successRate >= 0.5 && appliedCount > 0;\nconst updatedContent = lines.join('\\n');\n\nreturn {\n json: {\n updatedContent: updatedContent,\n sha: fileSha,\n path: filePath,\n issues: issues,\n applied: appliedCount,\n skipped: skippedCount,\n successRate: successRate,\n success: isSuccess,\n appliedChanges: appliedChanges,\n skippedChanges: skippedChanges\n }\n};"
},
"typeVersion": 2
},
{
"id": "21eda142-611b-4d86-86aa-862dae40b7aa",
"name": "Commit Updated File to GitHub",
"type": "n8n-nodes-base.github",
"position": [
3264,
1296
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('Github Config').item.json.repoOwner }}"
},
"filePath": "={{ $('Github Config').item.json.filePath }}",
"resource": "file",
"operation": "edit",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('Github Config').item.json.repoName }}"
},
"fileContent": "={{ $json.updatedContent }}",
"commitMessage": "=AI QA: Applied {{ $json.applied }} edit(s) - {{ new Date().toISOString().slice(0,10) }}",
"authentication": "oAuth2"
},
"typeVersion": 1
},
{
"id": "02c73e7f-f250-42f7-a0ac-2f5bba509353",
"name": "Format QA Report",
"type": "n8n-nodes-base.code",
"position": [
3264,
1104
],
"parameters": {
"jsCode": "// Build markdown report with applied changes\n\nconst issues = $input.first().json.issues;\nconst appliedChanges = $input.first().json.appliedChanges;\nconst skippedChanges = $input.first().json.skippedChanges;\nconst successRate = $input.first().json.successRate;\nconst fileSha = $input.first().json.sha;\nconst filePath = $input.first().json.path;\nconst updatedContent = $input.first().json.updatedContent;\nconst isSuccess = $input.first().json.success;\n\nconst today = new Date().toISOString().slice(0, 10);\n\nlet report = '';\nreport += `# QA Report - ${today}\\n\\n`;\nreport += `## Summary\\n`;\nreport += `- Applied: ${appliedChanges.length}\\n`;\nreport += `- Skipped: ${skippedChanges.length}\\n`;\nreport += `- Success Rate: ${(successRate * 100).toFixed(0)}%\\n\\n`;\n\nreport += `## Issues Found\\n\\n`;\nfor (const issue of issues) {\n report += `### Line ${issue.line_number} (${issue.issue_type})\\n`;\n report += `${issue.description}\\n\\n`;\n}\n\nreport += `## Changes Applied\\n\\n`;\nfor (const change of appliedChanges) {\n report += `- Line ${change.line}: ${change.operation}\\n`;\n}\n\nif (skippedChanges.length > 0) {\n report += `\\n## Skipped Edits\\n\\n`;\n for (const skip of skippedChanges) {\n report += `- Line ${skip.line}: ${skip.reason}\\n`;\n }\n}\n\nreturn {\n json: {\n report: report,\n sha: fileSha,\n path: filePath,\n updatedContent: updatedContent,\n success: isSuccess\n }\n};"
},
"typeVersion": 2
},
{
"id": "50cff18d-b1a2-4863-aa60-5777652e116d",
"name": "Save QA Report to GitHub",
"type": "n8n-nodes-base.github",
"position": [
3488,
1104
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('Github Config').item.json.repoOwner }}"
},
"filePath": "=qa-reports/{{ new Date().toISOString().slice(0,19).replace(/:/g, '-') }}-report.md",
"resource": "file",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('Github Config').item.json.repoName }}"
},
"fileContent": "={{ $json.report }}",
"commitMessage": "=AI QA: Content improvements - {{ new Date().toISOString().slice(0,10) }}",
"authentication": "oAuth2"
},
"typeVersion": 1.1
},
{
"id": "67756c95-93a2-4a75-9ba6-d0d292bb6de3",
"name": "Save QA Report to GitHub Without Issues",
"type": "n8n-nodes-base.github",
"position": [
2624,
1664
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('Github Config').item.json.repoOwner }}"
},
"filePath": "=qa-reports/{{ new Date().toISOString().slice(0,19).replace(/:/g, '-') }}-CLEAN-report.md",
"resource": "file",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('Github Config').item.json.repoName }}"
},
"fileContent": "={{ $json.report }}",
"commitMessage": "=AI QA: Content improvements - {{ new Date().toISOString().slice(0,10) }}",
"authentication": "oAuth2"
},
"typeVersion": 1.1
},
{
"id": "4290553e-f274-4fe5-bac2-0938fd1076cc",
"name": "Format Clean Report",
"type": "n8n-nodes-base.code",
"position": [
2336,
1664
],
"parameters": {
"jsCode": "// Report when no issues found\n\nconst today = new Date().toISOString().slice(0, 10);\nconst filePath = $('Parse QA Issues JSON').first().json.path;\n\nlet report = '';\nreport += `# QA Report - ${today}\\n\\n`;\nreport += `## Summary\\n`;\nreport += `\u2705 No issues detected\\n\\n`;\nreport += `The blog post passed QA review with no changes required.\\n`;\n\nreturn {\n json: {\n report: report,\n path: filePath\n }\n};"
},
"typeVersion": 2
},
{
"id": "6bcfdc7f-1a6e-4e93-bd98-a79bcde46999",
"name": "Format Failure Report",
"type": "n8n-nodes-base.code",
"position": [
3264,
1488
],
"parameters": {
"jsCode": "// Report when edits failed to apply\n\nconst skippedChanges = $input.first().json.skippedChanges || [];\nconst appliedCount = $input.first().json.applied || 0;\nconst skippedCount = $input.first().json.skipped || 0;\nconst successRate = $input.first().json.successRate || 0;\n\nconst today = new Date().toISOString().slice(0, 10);\n\nlet report = '';\nreport += `# QA Report - ${today}\\n\\n`;\nreport += `## \u26a0\ufe0f Commit Skipped\\n\\n`;\nreport += `The edits could not be safely applied. No changes were made to the file.\\n\\n`;\nreport += `## Summary\\n`;\nreport += `- Applied: ${appliedCount}\\n`;\nreport += `- Skipped: ${skippedCount}\\n`;\nreport += `- Success Rate: ${(successRate * 100).toFixed(0)}%\\n\\n`;\n\nif (skippedChanges.length > 0) {\n report += `## Skipped Edits\\n\\n`;\n for (const skip of skippedChanges) {\n report += `- Line ${skip.line}: ${skip.reason}\\n`;\n }\n}\n\nreport += `\\n## Next Steps\\n`;\nreport += `- Review the QA issues manually\\n`;\nreport += `- Check if line numbers are correct\\n`;\nreport += `- Re-run the workflow after fixing the source content\\n`;\n\nreturn {\n json: {\n report: report,\n path: $input.first().json.path\n }\n};"
},
"typeVersion": 2
},
{
"id": "64fe16da-4204-4614-a4c6-cea41a45cea9",
"name": "Create a file",
"type": "n8n-nodes-base.github",
"position": [
3488,
1488
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('Github Config').item.json.repoOwner }}"
},
"filePath": "=qa-reports/{{ new Date().toISOString().slice(0,19).replace(/:/g, '-') }}-FAILED-report.md",
"resource": "file",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('Github Config').item.json.repoName }}"
},
"fileContent": "={{ $json.report }}",
"commitMessage": "=AI QA: Edit application failed - {{ new Date().toISOString().slice(0,10) }}",
"authentication": "oAuth2"
},
"typeVersion": 1.1
},
{
"id": "04705019-90b0-4a47-8856-8576d30923be",
"name": "Fallback Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatGroq",
"position": [
1576,
1668
],
"parameters": {
"model": "openai/gpt-oss-20b",
"options": {}
},
"typeVersion": 1
},
{
"id": "a85b931f-f5e2-4772-9f60-cb756aa6e7f9",
"name": "Fallback Model",
"type": "@n8n/n8n-nodes-langchain.lmChatGroq",
"position": [
2376,
1364
],
"parameters": {
"model": "openai/gpt-oss-20b",
"options": {}
},
"typeVersion": 1
},
{
"id": "5a71abda-d41c-4651-b945-2e94f252d9dd",
"name": "Github Config",
"type": "n8n-nodes-base.set",
"position": [
768,
1444
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "755e6a1a-c3ec-4428-890d-8c5ffa26d97f",
"name": "repoOwner",
"type": "string",
"value": "YOUR_GITHUB_USERNAME"
},
{
"id": "04fe558c-666e-4b7f-8423-8b19492852c4",
"name": "repoName",
"type": "string",
"value": "blog-n8n"
},
{
"id": "dc73920f-6c6c-4f6c-821c-7dbace4427d0",
"name": "filePath",
"type": "string",
"value": "blog-post.md"
},
{
"id": "2b742aa2-9782-4e8c-ba9f-864418cfbb77",
"name": "reportFolder",
"type": "string",
"value": "qa-reports"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "bac9b915-2374-47ed-a3dd-f0b3b6cd008b",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-32,
1088
],
"parameters": {
"width": 480,
"height": 832,
"content": "## AI Automated Markdown Blog QA\n\n### How it works\n\n1. Fetch: A manual trigger fetches your target markdown file from GitHub.\n\n2. Prep: The file is decoded and numbered for the AI.\n\n3. Agent 1 (Analysis): The QA Agent identifies tone, clarity, and grammar issues, outputting JSON.\n\n4. Agent 2 (Editing): The Editor Agent translates those issues into precise line-by-line edits.\n\n5. Execute: The workflow applies the edits, then commits the updated file and a Markdown report to GitHub.\n\n### Setup steps\n\n- [ ] Connect GitHub OAuth2 to all GitHub nodes.\n- [ ] Add Google Gemini and Groq API keys.\n- [ ] Update the Github Config node (repoOwner, repoName, filePath).\n\n\n### Customization\n\n- Customize\n\n- Style: Edit the QA Agent's prompt to enforce brand rules.\n\n- Models: Swap Gemini/Groq for OpenAI or Anthropic.\n\n- Routing: Adjust the GitHub nodes to change report destinations."
},
"typeVersion": 1
},
{
"id": "387ddb16-cabc-400a-9cf3-8c5b9369163c",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
480,
1360
],
"parameters": {
"color": 7,
"width": 912,
"height": 304,
"content": "## Get blog post"
},
"typeVersion": 1
},
{
"id": "26baa3ac-0294-47bf-be5f-224c15366366",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1408,
1360
],
"parameters": {
"color": 7,
"width": 736,
"height": 464,
"content": "## Analyze content\n"
},
"typeVersion": 1
},
{
"id": "3a88ad17-39f9-4563-9aa8-e410142ce4cd",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2192,
1040
],
"parameters": {
"color": 7,
"width": 976,
"height": 480,
"content": "## Generate & apply edits\n"
},
"typeVersion": 1
},
{
"id": "20e71b57-af0b-491e-811f-302837aca8d9",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
3200,
1024
],
"parameters": {
"color": 7,
"width": 512,
"height": 720,
"content": "## Update repository"
},
"typeVersion": 1
},
{
"id": "a30e6809-8331-4a78-bb9a-5dd3a0126120",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
2272,
1584
],
"parameters": {
"color": 7,
"width": 560,
"height": 288,
"content": "## Path: No Issues Found"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "f64eb8e8-dc8b-4f93-ab85-7d3d231858ef",
"connections": {
"Has Issues?": {
"main": [
[
{
"node": "Editor Agent - Generate Edit Ops",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Clean Report",
"type": "main",
"index": 0
}
]
]
},
"QA Agent LLM": {
"ai_languageModel": [
[
{
"node": "QA Agent - Analyze Content",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Github Config": {
"main": [
[
{
"node": "Fetch Blog Post from GitHub",
"type": "main",
"index": 0
}
]
]
},
"Edits Applied?": {
"main": [
[
{
"node": "Commit Updated File to GitHub",
"type": "main",
"index": 0
},
{
"node": "Format QA Report",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Failure Report",
"type": "main",
"index": 0
}
]
]
},
"Fallback Model": {
"ai_languageModel": [
[
{
"node": "Editor Agent - Generate Edit Ops",
"type": "ai_languageModel",
"index": 1
}
]
]
},
"Editor Agent LLM": {
"ai_languageModel": [
[
{
"node": "Editor Agent - Generate Edit Ops",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Format QA Report": {
"main": [
[
{
"node": "Save QA Report to GitHub",
"type": "main",
"index": 0
}
]
]
},
"Fallback Chat Model": {
"ai_languageModel": [
[
{
"node": "QA Agent - Analyze Content",
"type": "ai_languageModel",
"index": 1
}
]
]
},
"Format Clean Report": {
"main": [
[
{
"node": "Save QA Report to GitHub Without Issues",
"type": "main",
"index": 0
}
]
]
},
"Parse QA Issues JSON": {
"main": [
[
{
"node": "Has Issues?",
"type": "main",
"index": 0
}
]
]
},
"Format Failure Report": {
"main": [
[
{
"node": "Create a file",
"type": "main",
"index": 0
}
]
]
},
"Apply Line-by-Line Edits": {
"main": [
[
{
"node": "Edits Applied?",
"type": "main",
"index": 0
}
]
]
},
"Parse Edit Operations JSON": {
"main": [
[
{
"node": "Apply Line-by-Line Edits",
"type": "main",
"index": 0
}
]
]
},
"QA Agent - Analyze Content": {
"main": [
[
{
"node": "Parse QA Issues JSON",
"type": "main",
"index": 0
}
]
]
},
"Fetch Blog Post from GitHub": {
"main": [
[
{
"node": "Decode Base64 & Add Line Numbers",
"type": "main",
"index": 0
}
]
]
},
"Decode Base64 & Add Line Numbers": {
"main": [
[
{
"node": "QA Agent - Analyze Content",
"type": "main",
"index": 0
}
]
]
},
"Editor Agent - Generate Edit Ops": {
"main": [
[
{
"node": "Parse Edit Operations JSON",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Github Config",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Stop manually proofreading markdown files. This workflow uses two AI agents to review your blog posts, generate precise line-by-line fixes, and commit the edits back to GitHub automatically. You write, it checks.
Source: https://n8n.io/workflows/14207/ — 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 is designed for marketers, content creators, agencies, and solo founders who want to publish long‑form posts with visuals on autopilot using n8n and AI agents.
Episode 15: Startup ideas for YC RFS. Uses lmChatOpenAi, googleSheets, agent, informationExtractor. Event-driven trigger; 33 nodes.
This workflow analyzes any npm package and delivers a data-driven recommendation using Firecrawl + APIs + AI reasoning.
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
Simply send messages to your personal trade journal assistant (bot) on Telegram, and it will handle the rest. After some time, you can review your journal to reflect, learn, and improve your trading s