This workflow corresponds to n8n.io template #14338 — we link there as the canonical source.
This workflow follows the Agent → 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": "3y8HmuJr5BXYuHGo",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Review GitLab merge requests with parallel AI reviewers",
"tags": [],
"nodes": [
{
"id": "7b4c951f-df41-479b-894b-a2807530a58f",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3120,
-384
],
"parameters": {
"width": 768,
"height": 848,
"content": "## How it works\n\nThis workflow runs an AI-assisted review for GitLab merge requests.\n\nWhen a reviewer posts the trigger comment in a merge request discussion, the workflow fetches the changed files, filters unsupported diffs, and prepares review context for each file.\n\nEach changed file is analyzed in parallel by three reviewers:\n- Bug reviewer\n- Security reviewer\n- Maintainability reviewer\n\nTheir findings are merged and then checked by a verifier, which removes weak, duplicate, or pre-existing issues.\n\nVerified findings that pass the posting checks can be posted as inline GitLab comments.\nA separate summary reply is posted back to the original trigger discussion.\n\n## Setup\n\nBefore using this template:\n\n- Connect your GitLab credential\n- Connect your LLM credential\n- Update the values in **Workflow Configuration** for your environment:\n - GitLab base URL\n - Review trigger phrase\n - Start / summary / no-issues messages\n - Minimum confidence required for posting. \n- Review the prompts and model settings used by each reviewer and the verifier\n- Test the workflow on a sample merge request discussion before enabling it\n\nThe verifier assigns a confidence score to each finding.\nOnly findings with status=keep and final_confidence at or above this threshold are posted. \nFindings with a resolved inline position are posted as inline comments; otherwise they are posted as reply comments.\n\nLower threshold values increase recall but may allow more false positives.\nHigher threshold values are stricter and may suppress borderline findings."
},
"typeVersion": 1
},
{
"id": "e8c95236-4071-43ce-926f-4fa1a7c50dd1",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2256,
0
],
"parameters": {
"color": 7,
"width": 880,
"height": 448,
"content": "## Trigger the review\n\nPost the configured trigger comment in a GitLab merge request discussion to start the workflow.\n\nDefault trigger: `+0`\nChange it in **Workflow Configuration**."
},
"typeVersion": 1
},
{
"id": "3529266d-af18-4585-989b-01af994b9c18",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1312,
-112
],
"parameters": {
"color": 7,
"width": 800,
"height": 560,
"content": "## Fetch and filter changed files\n\nLoad the merge request changes, split them into one file per item, and skip unsupported diffs such as renamed, deleted, or non-commentable files."
},
"typeVersion": 1
},
{
"id": "a3ada654-1ded-4275-8d84-ed3b6e5b3f61",
"name": "GitLab Discussion Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-2160,
240
],
"parameters": {
"path": "8a1a0ef5-a1ab-49e0-9cd5-321d0c4bc080",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2
},
{
"id": "8dbf3590-f83f-437e-bcca-88846af6ff2c",
"name": "Check Review Trigger Comment",
"type": "n8n-nodes-base.if",
"position": [
-1648,
240
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "617eb2c5-dd4b-4e28-b533-0c32ea6ca961",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.body.object_attributes.note }}",
"rightValue": "={{ $('Workflow Configuration').first().json.reviewTriggerPhrase }}"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "4a7768fd-9c71-4e5c-ae77-06395b94d044",
"name": "Post Review Started Reply",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1232,
48
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.gitlabBaseUrl }}/projects/{{ $('GitLab Discussion Webhook').item.json.body.project.id }}/merge_requests/{{ $('GitLab Discussion Webhook').item.json.body.merge_request.iid }}/discussions/{{ $('GitLab Discussion Webhook').item.json.body.object_attributes.discussion_id }}/notes",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "multipart-form-data",
"authentication": "predefinedCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "body",
"value": "={{ $('Workflow Configuration').first().json.reviewStartedMessage }}"
}
]
},
"nodeCredentialType": "gitlabApi"
},
"credentials": {
"gitlabApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "ced91e4b-aea1-48d5-baca-11e61cf53e11",
"name": "Fetch Merge Request Changes",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1232,
224
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.gitlabBaseUrl }}/projects/{{ $json[\"body\"][\"project_id\"] }}/merge_requests/{{ $json[\"body\"][\"merge_request\"][\"iid\"] }}/changes",
"options": {},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "gitlabApi"
},
"credentials": {
"gitlabApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "bb0320ac-0086-484f-aba4-812dd83ada77",
"name": "Split Changed Files",
"type": "n8n-nodes-base.splitOut",
"position": [
-960,
224
],
"parameters": {
"options": {},
"fieldToSplitOut": "changes"
},
"typeVersion": 1
},
{
"id": "eb1c3141-4dbc-44e5-b232-2b96c6aca112",
"name": "Filter Supported Diffs",
"type": "n8n-nodes-base.if",
"position": [
-720,
224
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c6e1430b-84a7-47ce-8fe9-7b94da0f2d31",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
},
"leftValue": "={{ $json.renamed_file }}",
"rightValue": ""
},
{
"id": "bf6e9eb9-d72d-459c-a722-9614bab8842c",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
},
"leftValue": "={{ $json.deleted_file }}",
"rightValue": ""
},
{
"id": "501623a9-9515-4034-bb13-a5a6a4f924eb",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ /(^|\\n)@@ -\\d+(?:,\\d+)? \\+\\d+(?:,\\d+)? @@/m.test($json.diff ?? '') }}",
"rightValue": "@@"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "3e8222aa-d68a-4366-9167-f039831d8ed5",
"name": "Prepare Review Context",
"type": "n8n-nodes-base.code",
"position": [
-288,
208
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const diff = $input.item.json.diff ?? '';\n\nconst lines = diff\n .replace(/\\n\\\\ No newline at end of file/g, '')\n .split('\\n');\n\nlet originalCode = '';\nlet newCode = '';\nlet newCodeLogicalLine = 0;\nconst newCodeLineMap = {};\n\nlet oldLine = 0;\nlet newLine = 0;\n\nlet reviewableNewCode = '';\n\nfor (const line of lines) {\n const hunk = line.match(/^@@ -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@/);\n if (hunk) {\n oldLine = Number(hunk[1]);\n newLine = Number(hunk[2]);\n continue;\n }\n\n if (line.startsWith('+++') || line.startsWith('---')) {\n continue;\n }\n\n if (line.startsWith('-')) {\n originalCode += line + '\\n';\n oldLine += 1;\n continue;\n }\n\n if (line.startsWith('+')) {\n newCode += line + '\\n';\n newCodeLogicalLine += 1;\n newCodeLineMap[newCodeLogicalLine] = newLine;\n reviewableNewCode += `[L${newLine}] ${line}\\n`;\n newLine += 1;\n continue;\n }\n\n // context line\n newCode += line + '\\n';\n newCodeLogicalLine += 1;\n newCodeLineMap[newCodeLogicalLine] = newLine;\n reviewableNewCode += `[L${newLine}] ${line}\\n`;\n oldLine += 1;\n newLine += 1;\n}\n\nreturn {\n path: $input.item.json.new_path || $input.item.json.old_path || '',\n oldPath: $input.item.json.old_path || '',\n newPath: $input.item.json.new_path || '',\n gitDiff: diff,\n originalCode,\n newCode,\n newCodeLineMap,\n reviewableNewCode,\n mrTitle: $('GitLab Discussion Webhook').item.json.body.merge_request.title || '',\n mrDescription: $('GitLab Discussion Webhook').item.json.body.merge_request.description || '',\n projectId: $('GitLab Discussion Webhook').item.json.body.project.id,\n mrIid: $('GitLab Discussion Webhook').item.json.body.merge_request.iid,\n discussionId: $('GitLab Discussion Webhook').item.json.body.object_attributes.discussion_id,\n startSha: $('Fetch Merge Request Changes').item.json.diff_refs.start_sha || '',\n headSha: $('Fetch Merge Request Changes').item.json.diff_refs.head_sha || '',\n baseSha: $('Fetch Merge Request Changes').item.json.diff_refs.base_sha || ''\n};"
},
"typeVersion": 2
},
{
"id": "f979e02c-9f39-41ac-80af-5b02b7735101",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
96,
-288
],
"parameters": {
"color": 7,
"width": 608,
"height": 1184,
"content": "## Run parallel AI reviewers\n\nAnalyze each changed file with bug, security, and maintainability reviewers.\nEach reviewer returns structured findings for final verification."
},
"typeVersion": 1
},
{
"id": "e2045ae9-9360-4b10-84ba-001dbfe67613",
"name": "Merge Reviewer Results",
"type": "n8n-nodes-base.merge",
"position": [
928,
192
],
"parameters": {
"numberInputs": 3
},
"typeVersion": 3.2
},
{
"id": "833740be-aae9-45e3-ba88-9d326cfb0cb9",
"name": "Combine Findings",
"type": "n8n-nodes-base.code",
"position": [
1184,
208
],
"parameters": {
"jsCode": "const allItems = $input.all();\n\nlet merged = [];\n\n// Assuming that the reviewer\u2019s output is stored in item.json.output\nfor (const item of allItems) {\n const src = item.json.output ?? item.json;\n const findings = src.findings || [];\n merged = merged.concat(findings);\n}\n\nconst uniqueMap = new Map();\n\nfor (const f of merged) {\n const key = `${f.title}_${f.path}_${f.line}`;\n if (!uniqueMap.has(key)) {\n uniqueMap.set(key, f);\n }\n}\n\nreturn [\n {\n json: {\n path: $('Prepare Review Context').first().json.path || '',\n oldPath: $('Prepare Review Context').first().json.oldPath || '',\n newPath: $('Prepare Review Context').first().json.newPath || '',\n gitDiff: $('Prepare Review Context').first().json.gitDiff || '',\n originalCode: $('Prepare Review Context').first().json.originalCode || '',\n newCode: $('Prepare Review Context').first().json.newCode || '',\n newCodeLineMap: $('Prepare Review Context').first().json.newCodeLineMap || {},\n reviewableNewCode: $('Prepare Review Context').first().json.reviewableNewCode || '',\n mrTitle: $('Prepare Review Context').first().json.mrTitle || '',\n mrDescription: $('Prepare Review Context').first().json.mrDescription || '',\n projectId: $('Prepare Review Context').first().json.projectId || '',\n mrIid: $('Prepare Review Context').first().json.mrIid || '',\n discussionId: $('Prepare Review Context').first().json.discussionId || '',\n startSha: $('Prepare Review Context').first().json.startSha || '',\n headSha: $('Prepare Review Context').first().json.headSha || '',\n baseSha: $('Prepare Review Context').first().json.baseSha || '',\n findings: Array.from(uniqueMap.values())\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "90de14bc-3f94-44bd-95c3-9ffbf2840ee4",
"name": "Analyze Bugs",
"type": "@n8n/n8n-nodes-langchain.agent",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
288,
-128
],
"parameters": {
"text": "={{ JSON.stringify($json, null, 2) }}",
"options": {
"systemMessage": "You are the Bug Reviewer for a pull request.\n\nGoal:\nDetect only newly introduced functional or logical bugs caused by this change.\n\nFocus areas:\n- Incorrect logic\n- Wrong conditions or boundary checks\n- Incorrect calculations (length, offsets, indices)\n- Off-by-one errors\n- Wrong assumptions about input values\n\nDo NOT report:\n- Style issues\n- Pure maintainability concerns\n- Security concerns unless they are primarily functional bugs in this change\n\nInput:\n- path: {{ $json.path }}\n- gitDiff: {{ $json.gitDiff }}\n- originalCode: {{ $json.originalCode }}\n- reviewableNewCode: {{ $json.reviewableNewCode }}\n- mr: {{ $json.mrTitle }}, {{ $json.mrDescription }}\n\nRules:\n- Report only issues introduced by this change\n- Do not speculate beyond the provided diff and code\n- If there is no real bug, return no findings\n- Use reviewableNewCode for line selection\n- line must be the L-number from reviewableNewCode\n- Prefer the exact changed statement over nearby context\n- reviewer must be \"bug\"\n- category must be \"bug\"\n- severity must be one of: high, medium, low\n- confidence must be an integer from 0 to 100\n- path must be the input path\n- Return a single JSON object matching the structured output schema exactly\n- Do not wrap the result in an \"output\" field\n- Return JSON only\n- Do not add explanations or markdown\n- If there is no real issue, return no findings.\n- Do not omit required fields when returning a finding."
},
"promptType": "define",
"hasOutputParser": true
},
"retryOnFail": true,
"typeVersion": 3.1,
"alwaysOutputData": true
},
{
"id": "d8a1225a-8bfc-4973-9374-7aed82883310",
"name": "Bug Reviewer Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAzureOpenAi",
"position": [
208,
48
],
"parameters": {
"model": "gpt-5.4-mini",
"options": {
"topP": 1
}
},
"credentials": {
"azureOpenAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "109ef89e-9a1b-44fd-a002-8e8bb6ae7c19",
"name": "Parse Bug Review Output",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"maxTries": 2,
"position": [
528,
48
],
"parameters": {
"jsonSchemaExample": "{\n \"reviewer\": \"bug\",\n \"findings\": [\n {\n \"title\": \"CRC is computed over the wrong byte range\",\n \"severity\": \"high\",\n \"confidence\": 98,\n \"path\": \"project/src/packet.c\",\n \"line\": 42,\n \"category\": \"bug\",\n \"why\": \"The CRC input length was changed so one required byte is no longer included in the checksum.\",\n \"suggestion\": \"Restore the original CRC input length so the decoder validates the intended packet range.\"\n }\n ]\n}"
},
"retryOnFail": true,
"typeVersion": 1.3,
"alwaysOutputData": true
},
{
"id": "222b4278-17ea-4390-aed2-48c8d72e4282",
"name": "Analyze Security Risks",
"type": "@n8n/n8n-nodes-langchain.agent",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
288,
208
],
"parameters": {
"text": "={{ JSON.stringify($json, null, 2) }}",
"options": {
"systemMessage": "You are the Security Reviewer for a pull request.\n\nGoal:\nDetect only newly introduced security risks caused by this change.\n\nFocus areas:\n- Missing validation of external input\n- Buffer overflow or out-of-bounds access\n- Unsafe memory operations\n- Injection risks\n- Sensitive data exposure\n- Missing checks that allow malformed input to cause damage\n\nDo NOT report:\n- General bugs unless they are real security risks\n- Pure maintainability concerns\n- Style issues\n\nInput:\n- path: {{ $json.path }}\n- gitDiff: {{ $json.gitDiff }}\n- originalCode: {{ $json.originalCode }}\n- reviewableNewCode: {{ $json.reviewableNewCode }}\n- mr: {{ $json.mrTitle }}, {{ $json.mrDescription }}\n\nRules:\n- Report only real security risks introduced by this change\n- If the issue is only a bug and not a real security risk, do not report it\n- If there is no real security issue, return no findings\n- Use reviewableNewCode for line selection\n- line must be the L-number from reviewableNewCode\n- Prefer the exact changed statement over nearby context\n- reviewer must be \"security\"\n- category must be \"security\"\n- severity must be one of: high, medium, low\n- confidence must be an integer from 0 to 100\n- path must be the input path\n- Return a single JSON object matching the structured output schema exactly\n- Do not wrap the result in an \"output\" field\n- Return JSON only\n- Do not add explanations or markdown\n- If there is no real issue, return no findings.\n- Do not omit required fields when returning a finding."
},
"promptType": "define",
"hasOutputParser": true
},
"notesInFlow": false,
"retryOnFail": true,
"typeVersion": 3.1,
"alwaysOutputData": true
},
{
"id": "2e4fe6a8-ed29-41f2-9987-73e9d45028eb",
"name": "Security Reviewer Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAzureOpenAi",
"position": [
208,
384
],
"parameters": {
"model": "gpt-5.4-mini",
"options": {
"topP": 1
}
},
"credentials": {
"azureOpenAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "7810d8b4-07d2-4eda-8e61-7a7d855e0645",
"name": "Parse Security Review Output",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
528,
384
],
"parameters": {
"jsonSchemaExample": "{\n \"reviewer\": \"security\",\n \"findings\": [\n {\n \"title\": \"Missing upper-bound validation before payload copy\",\n \"severity\": \"high\",\n \"confidence\": 95,\n \"path\": \"project/src/packet.c\",\n \"line\": 46,\n \"category\": \"security\",\n \"why\": \"The decoded length is copied into the payload buffer without checking that it fits the destination buffer.\",\n \"suggestion\": \"Validate that len does not exceed the payload buffer size before calling memcpy.\"\n }\n ]\n}"
},
"typeVersion": 1.3,
"alwaysOutputData": true
},
{
"id": "a91382fc-e303-4dff-91f8-1cd15d2de5c5",
"name": "Analyze Maintainability Risks",
"type": "@n8n/n8n-nodes-langchain.agent",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
288,
560
],
"parameters": {
"text": "={{ JSON.stringify($json, null, 2) }}",
"options": {
"systemMessage": "You are the Maintainability Reviewer for a pull request.\n\nGoal:\nDetect only meaningful maintainability issues introduced by this change.\n\nFocus areas:\n- Missing defensive checks\n- Risky assumptions not validated\n- Changes that significantly reduce readability\n- Logic that becomes harder to test or reason about\n- Changes that increase future defect risk even if they are not immediate bugs\n\nDo NOT report:\n- Formatting issues\n- Naming preferences\n- Minor readability suggestions\n- General advice not tied to changed code\n- Pure bug or security issues unless the maintainability concern is clearly distinct\n\nInput:\n- path: {{ $json.path }}\n- gitDiff: {{ $json.gitDiff }}\n- originalCode: {{ $json.originalCode }}\n- reviewableNewCode: {{ $json.reviewableNewCode }}\n- mr: {{ $json.mrTitle }}, {{ $json.mrDescription }}\n\nRules:\n- Report only meaningful issues introduced by this change\n- If the issue is weak, subjective, or minor, do not report it\n- If there is no real maintainability issue, return no findings\n- Use reviewableNewCode for line selection\n- line must be the L-number from reviewableNewCode\n- Prefer the exact changed statement over nearby context\n- reviewer must be \"maintainability\"\n- category must be \"maintainability\"\n- severity must be one of: medium, low\n- confidence must be an integer from 0 to 100\n- path must be the input path\n- Return a single JSON object matching the structured output schema exactly\n- Do not wrap the result in an \"output\" field\n- Return JSON only\n- Do not add explanations\n- If there is no real issue, return no findings.\n- Do not omit required fields when returning a finding."
},
"promptType": "define",
"hasOutputParser": true
},
"notesInFlow": false,
"retryOnFail": true,
"typeVersion": 3.1,
"alwaysOutputData": true
},
{
"id": "6ce560ca-d1b0-450d-ab26-f1aa13f5b7ba",
"name": "Maintainability Reviewer Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAzureOpenAi",
"position": [
208,
736
],
"parameters": {
"model": "gpt-5.4-mini",
"options": {
"topP": 1
}
},
"credentials": {
"azureOpenAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "bf350a82-ed47-425c-ab99-0da161717062",
"name": "Parse Maintainability Review Output",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
512,
736
],
"parameters": {
"jsonSchemaExample": "{\n \"reviewer\": \"maintainability\",\n \"findings\": [\n {\n \"title\": \"Length validation is missing in the decode path\",\n \"severity\": \"medium\",\n \"confidence\": 88,\n \"path\": \"project/src/packet.c\",\n \"line\": 46,\n \"category\": \"maintainability\",\n \"why\": \"The decode path relies on an unchecked length value before copying data into the payload buffer.\",\n \"suggestion\": \"Add an explicit upper-bound check for len before copying the payload.\"\n }\n ]\n}"
},
"typeVersion": 1.3,
"alwaysOutputData": true
},
{
"id": "3d6a3c26-9f14-4f61-bc1f-20d6f0186575",
"name": "Verify Findings",
"type": "@n8n/n8n-nodes-langchain.agent",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
1632,
208
],
"parameters": {
"text": "={{ JSON.stringify($json, null, 2) }}",
"options": {
"systemMessage": "You are the verifier for pull request review findings.\n\nGoal:\nValidate and normalize findings from the reviewers.\n\nInput:\n- path: {{ $json.path }}\n- gitDiff: {{ $json.gitDiff }}\n- originalCode: {{ $json.originalCode }}\n- reviewableNewCode: {{ $json.reviewableNewCode }}\n- findings: {{ JSON.stringify($json.findings, null, 2) }}\n\nTasks:\n- Keep only findings grounded in the diff\n- Remove speculative or weak findings\n- Remove duplicates\n- Prefer the most useful classification when findings overlap\n- Normalize severity and confidence\n- Produce concise, GitLab-ready English comments\n\nRules:\n\n[General]\n- Reviewer output may be imperfect; always re-evaluate findings\n- Drop findings that are not clearly supported by the diff\n- Path must not be empty; if missing or unknown, use input path\n\n[Line selection]\n- Use reviewableNewCode for line selection\n- line must be the L-number from reviewableNewCode\n- Prefer the exact changed statement over nearby context\n- If unclear, select the closest relevant changed line only when the finding clearly applies\n- If no specific changed line can be identified, set line to null and post_inline to false\n\n[Status]\n- status must be one of: keep, drop, duplicate, pre_existing\n- Use \"keep\" only for findings worth posting after verification\n- Never use \"open\" or any other value\n\n[Severity normalization]\n- final_severity must be one of: high, medium, low\n- Always re-evaluate severity; do not trust reviewer severity\n\nUse the following strict criteria:\n\nhigh:\n- Causes incorrect behavior, broken functionality, or protocol mismatch\n- Can lead to crash, data corruption, or memory safety issues\n- Security vulnerabilities (e.g., OOB write, injection)\n\nmedium:\n- Potential bug, missing validation, or fragile logic\n- May become a bug under certain conditions\n- Maintainability issues that can realistically cause future defects\n\nlow:\n- Readability, naming, minor structure or style issues\n- No realistic impact on correctness\n\n- When multiple findings describe the same root problem:\n - Keep the most critical one\n - Downgrade or mark others as duplicate\n - Avoid multiple high severity findings for the same root cause\n\n[Confidence normalization]\n- final_confidence must be one of: 0, 25, 50, 75, 100\n- Use 100 only when directly proven by the diff with almost no ambiguity\n- Use 75 when strongly supported and very likely correct\n- Use 50 when plausible but requires assumptions or missing context\n- Use 25 when weak signal; generally should be dropped unless still useful\n- Use 0 when unsupported or incorrect\n\n- Findings with final_confidence = 0 should normally have status = \"drop\"\n\n[Inline decision]\n- post_inline must be boolean\n- Set post_inline = true only when:\n - status = \"keep\"\n - AND a specific changed line is clearly identified\n- Otherwise set post_inline = false\n- Do NOT use severity or confidence to decide inline vs reply\n\n[Output quality]\n- title must be concise English\n- comment must be short, practical, and directly actionable\n- Avoid repeating the same issue in multiple findings\n\n[Output format]\n- Return a single JSON object matching the structured output schema exactly\n- Do not wrap the result in an \"output\" field\n- Return JSON only\n- Do not add explanations or markdown\n- Do not omit any required field in validated_findings\n- If no finding should remain, return an empty validated_findings array"
},
"promptType": "define",
"hasOutputParser": true
},
"retryOnFail": true,
"typeVersion": 3.1,
"alwaysOutputData": true
},
{
"id": "800ab383-e391-407a-85ec-8dc5b8361974",
"name": "Verifier Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAzureOpenAi",
"position": [
1552,
416
],
"parameters": {
"model": "gpt-5.4-mini",
"options": {
"topP": 1
}
},
"credentials": {
"azureOpenAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "c424f47a-cbe8-41bd-bcd5-83f7b1793fde",
"name": "Parse Verification Output",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
1840,
416
],
"parameters": {
"jsonSchemaExample": "{\n \"validated_findings\": [\n {\n \"title\": \"Restore CRC length and byte order\",\n \"path\": \"project/src/packet.c\",\n \"line\": 42,\n \"final_severity\": \"high\",\n \"final_confidence\": 100,\n \"status\": \"keep\",\n \"post_inline\": true,\n \"comment\": \"The CRC validation now uses the wrong byte range and byte order, so valid packets can be rejected. Restore the original CRC length and trailer byte order.\"\n },\n {\n \"title\": \"Clarify unchecked length assumption\",\n \"path\": \"project/src/packet.c\",\n \"line\": null,\n \"final_severity\": \"low\",\n \"final_confidence\": 50,\n \"status\": \"keep\",\n \"post_inline\": false,\n \"comment\": \"The safety of this path depends on an external length guarantee. Consider documenting or enforcing that contract closer to the use site.\"\n }\n ]\n}"
},
"typeVersion": 1.3,
"alwaysOutputData": true
},
{
"id": "3ee34738-ec78-48ff-b1f3-b257c2a0929a",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1488,
-16
],
"parameters": {
"color": 7,
"width": 752,
"height": 592,
"content": "## Verify and normalize findings\n\nRe-check each finding against the diff, remove weak or duplicate issues, and normalize the final severity and confidence."
},
"typeVersion": 1
},
{
"id": "24d31f5a-dbf6-448f-9141-d562b336813b",
"name": "Normalize Verified Findings",
"type": "n8n-nodes-base.code",
"position": [
2000,
208
],
"parameters": {
"jsCode": "const out = $json.output ?? $json;\nconst meta = $('Combine Findings').first().json;\n\nconst validatedFindings = (out.validated_findings || []).map(f => {\n const rawLine = f.line;\n const line = Number(rawLine);\n const lineMap = meta.newCodeLineMap || {};\n const mappedLine = Number.isInteger(line) && line > 0\n ? Number(lineMap[line] ?? line)\n : null;\n\n const normalizedLine =\n Number.isInteger(mappedLine) && mappedLine > 0 ? mappedLine : null;\n\n const normalizedStatus = f.status || 'drop';\n const normalizedInline =\n normalizedStatus === 'keep' &&\n normalizedLine !== null &&\n f.post_inline === true;\n\n return {\n ...f,\n path: f.path && f.path !== 'unknown' ? f.path : (meta.path || ''),\n oldPath: meta.oldPath || '',\n newPath: meta.newPath || '',\n projectId: meta.projectId || '',\n mrIid: meta.mrIid || '',\n discussionId: meta.discussionId || '',\n startSha: meta.startSha || '',\n headSha: meta.headSha || '',\n baseSha: meta.baseSha || '',\n line: normalizedLine,\n post_inline: normalizedInline\n };\n});\n\nconst keepCount = validatedFindings.filter(f => f.status === 'keep').length;\n\nreturn [\n {\n json: {\n validated_findings: validatedFindings,\n keep_count: keepCount\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "2d6de0fe-3507-470c-bd76-df6365079d53",
"name": "Check Findings Exist",
"type": "n8n-nodes-base.if",
"position": [
2416,
352
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "90a27f07-8e7f-4b1d-bbde-05e0f73846f9",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{$json.keep_count}}",
"rightValue": 1
}
]
}
},
"typeVersion": 2.3
},
{
"id": "35f1a3ee-f0bf-42a6-a0a9-193a477708ef",
"name": "Build Summary Comment",
"type": "n8n-nodes-base.code",
"position": [
3360,
432
],
"parameters": {
"jsCode": "return {\n text: $('Workflow Configuration').first().json.summaryMessage\n || '\u2705 Review finished. See inline comments for details.'\n};"
},
"typeVersion": 2
},
{
"id": "5ac8e5c4-d800-4f15-9ced-1bc334832716",
"name": "Build No-Issues Comment",
"type": "n8n-nodes-base.code",
"position": [
3360,
640
],
"parameters": {
"jsCode": "return {\n text: $('Workflow Configuration').first().json.noIssuesMessage\n};"
},
"typeVersion": 2
},
{
"id": "26500903-ef4a-42d8-bdb0-9b50504d1dd9",
"name": "Post Summary Reply",
"type": "n8n-nodes-base.httpRequest",
"position": [
3664,
544
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.gitlabBaseUrl }}/projects/{{ $('Combine Findings').item.json.projectId }}/merge_requests/{{ $('Combine Findings').item.json.mrIid }}/discussions/{{ $('Combine Findings').item.json.discussionId }}/notes",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "multipart-form-data",
"authentication": "predefinedCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "body",
"value": "={{ $json.text }}"
}
]
},
"nodeCredentialType": "gitlabApi"
},
"credentials": {
"gitlabApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "a6977948-6c59-4b2a-a609-61a2b18e9b91",
"name": "Split Verified Findings",
"type": "n8n-nodes-base.splitOut",
"position": [
2704,
208
],
"parameters": {
"options": {},
"fieldToSplitOut": "validated_findings"
},
"typeVersion": 1
},
{
"id": "d01239bd-af12-48f5-b5eb-d7ef74889886",
"name": "Filter Publishable Findings",
"type": "n8n-nodes-base.if",
"position": [
2944,
208
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "67f9d8b0-7268-4068-8572-873a8e4f8f43",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{$json.status}}",
"rightValue": "keep"
},
{
"id": "7d9ed55a-e8d5-491d-b7b0-9f26b466280c",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{$json.final_confidence}}",
"rightValue": "={{ $('Workflow Configuration').first().json.minConfidenceToPost }}"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "1a91dabf-b880-4c18-b97c-bc6cb1ff764a",
"name": "Build Inline Comment",
"type": "n8n-nodes-base.code",
"position": [
3616,
-224
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const iconMap = {\n high: \"\ud83d\udd34\",\n medium: \"\ud83d\udfe1\",\n low: \"\ud83d\udd35\",\n};\n\nconst icon = iconMap[$json.final_severity] || \"\ud83d\udd35\";\n\nreturn {\n ...$json,\n text: `${icon} **${$json.title}**\\n\\n${$json.comment}`\n};"
},
"typeVersion": 2
},
{
"id": "0ed853a6-492a-4a20-aacf-d07c6c46e9a1",
"name": "Post Inline Review Comment",
"type": "n8n-nodes-base.httpRequest",
"position": [
4192,
-400
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.gitlabBaseUrl }}/projects/{{ $json.projectId }}/merge_requests/{{ $json.mrIid }}/discussions",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "multipart-form-data",
"authentication": "predefinedCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "body",
"value": "={{ $json.text }}"
},
{
"name": "position[position_type]",
"value": "text"
},
{
"name": "position[old_path]",
"value": "={{ $json.oldPath }}"
},
{
"name": "position[new_path]",
"value": "={{ $json.newPath }}"
},
{
"name": "position[start_sha]",
"value": "={{ $json.startSha }}"
},
{
"name": "position[head_sha]",
"value": "={{ $json.headSha }}"
},
{
"name": "position[base_sha]",
"value": "={{ $json.baseSha }}"
},
{
"name": "position[new_line]",
"value": "={{ $json.positionNewLine ?? '' }}"
},
{
"name": "position[old_line]",
"value": "={{ $json.positionOldLine ?? '' }}"
}
]
},
"nodeCredentialType": "gitlabApi"
},
"credentials": {
"gitlabApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "726cc9e8-312f-4bac-b824-c0019a54cabe",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
2320,
-16
],
"parameters": {
"color": 7,
"width": 848,
"height": 592,
"content": "## Decide what to publish\n\nPost only findings with `status = keep` and confidence at or above the configured threshold.\n\nIf a valid diff position is found, post an inline comment.\nOtherwise, post a reply comment in the trigger discussion."
},
"typeVersion": 1
},
{
"id": "49308ef9-c8a9-4480-9bf7-cb76200dc741",
"name": "Resolve GitLab Diff Position",
"type": "n8n-nodes-base.code",
"position": [
3344,
-224
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const diff = $('Combine Findings').first().json.gitDiff || '';\nconst targetLine = Number($json.line);\n\nfunction buildNewSideMap(gitDiff) {\n const lines = gitDiff.replace(/\\n\\\\ No newline at end of file/g, '').split('\\n');\n\n let oldLine = 0;\n let newLine = 0;\n const map = new Map();\n\n for (const line of lines) {\n const hunk = line.match(/^@@ -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@/);\n if (hunk) {\n oldLine = Number(hunk[1]);\n newLine = Number(hunk[2]);\n continue;\n }\n\n if (line.startsWith('+++') || line.startsWith('---')) {\n continue;\n }\n\n if (line.startsWith('+')) {\n map.set(newLine, {\n kind: 'added',\n newLine,\n oldLine: null,\n });\n newLine += 1;\n continue;\n }\n\n if (line.startsWith('-')) {\n oldLine += 1;\n continue;\n }\n\n // context line\n map.set(newLine, {\n kind: 'context',\n newLine,\n oldLine,\n });\n oldLine += 1;\n newLine += 1;\n }\n\n return map;\n}\n\nconst lineMap = buildNewSideMap(diff);\nconst pos = lineMap.get(targetLine);\n\nreturn {\n ...$json,\n positionKind: pos?.kind || 'unknown',\n positionNewLine: pos?.newLine ?? null,\n positionOldLine: pos?.oldLine ?? null,\n canPostInline: !!pos\n};"
},
"typeVersion": 2
},
{
"id": "77fb631b-0c24-4bf4-a115-77b420b407ac",
"name": "Check Inline Position",
"type": "n8n-nodes-base.if",
"position": [
3872,
-224
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "692505f7-cf7f-457e-b0b0-6d200026be6d",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{$json.canPostInline}}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "05e25cbd-06e6-44e9-a33f-3269835c552d",
"name": "Workflow Configuration",
"type": "n8n-nodes-base.set",
"position": [
-1904,
240
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "588e5976-88ab-4cff-8db7-5394f8bf59b2",
"name": "gitlabBaseUrl",
"type": "string",
"value": "https://gitlab.example.com/api/v4"
},
{
"id": "c3a738df-33de-4cb7-af13-d3a42008b769",
"name": "reviewTriggerPhrase",
"type": "string",
"value": "+0"
},
{
"id": "1bd7d1e8-1c9e-4c05-ab54-78a8b97f76ac",
"name": "minConfidenceToPost",
"type": "number",
"value": 75
},
{
"id": "570ecfa2-f25c-4810-8c74-8af77c19f07c",
"name": "reviewStartedMessage",
"type": "string",
"value": "\ud83d\udd0d I'm reviewing the code right now. Please hang tight for a moment."
},
{
"id": "a4fcbea2-ea1d-4931-b6ff-35a0245c04a4",
"name": "noIssuesMessage",
"type": "string",
"value": "\ud83d\udfe2 The changes look good this time!"
},
{
"id": "77ba45e7-b313-45a3-bfb8-05323d66a5d0",
"name": "summaryMessage",
"type": "string",
"value": "\u2705 Review complete!"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "b3812b08-44e5-48dc-9e52-1c885051b13d",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-448,
-112
],
"parameters": {
"color": 7,
"width": 464,
"height": 560,
"content": "## Prepare review context\n\nConvert each diff into a clean review payload with file paths, code snippets, merge request metadata, and diff SHAs needed later for GitLab comments."
},
"typeVersion": 1
},
{
"id": "1d5b16f8-004b-4040-b426-751ebad11b7c",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
3248,
-592
],
"parameters": {
"color": 7,
"width": 1408,
"height": 656,
"content": "## Publish review comments\n\nResolve the GitLab diff position for each verified finding and post the final review comment.\n\nIf a valid inline position is available, create an inline diff discussion.\nOtherwise, post a reply comment in the trigger discussion."
},
"typeVersion": 1
},
{
"id": "e877d584-a678-4c86-9645-dcb0872dd440",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
3248,
224
],
"parameters": {
"color": 7,
"width": 656,
"height": 608,
"content": "## Post a summary reply\n\nAlways reply to the trigger discussion with either a completion message or a no-issues message.\nInline and fallback review comments are posted separately."
},
"typeVersion": 1
},
{
"id": "5653a6fb-b1a1-4e70-bbf5-6bb8d709fa0d",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
800,
-112
],
"parameters": {
"color": 7,
"width": 592,
"height": 576,
"content": "## Merge reviewer outputs\n\nCombine findings from all reviewers, remove obvious duplicates, and keep the file and merge request context for final verification."
},
"typeVersion": 1
},
{
"id": "17e3c9d5-0f05-4b66-adb5-7da2d903a8c1",
"name": "Post Fallback Reply",
"type": "n8n-nodes-base.httpRequest",
"position": [
4416,
-160
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.gitlabBaseUrl }}/projects/{{ $json.projectId }}/merge_requests/{{ $json.mrIid }}/discussions/{{ $json.discussionId }}/notes",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "multipart-form-data",
"authentication": "predefinedCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "body",
"value": "={{ $json.fallbackBody }}"
}
]
},
"nodeCredentialType": "gitlabApi"
},
"credentials": {
"gitlabApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "8300ae3d-115d-44d3-8717-fa123959d1ba",
"name": "Build Fallback Comment",
"type": "n8n-nodes-base.code",
"position": [
4192,
-160
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const iconMap = {\n high: '\ud83d\udd34',\n medium: '\ud83d\udfe1',\n low: '\ud83d\udd35',\n};\n\nconst severity = ($json.final_severity || 'low').toLowerCase();\nconst icon = iconMap[severity] || '\ud83d\udd35';\n\nreturn {\n ...$json,\n fallbackBody: [\n `${icon} General review comment (exact diff line could not be resolved)`,\n '',\n `**${$json.title}**`,\n $json.comment,\n '',\n `File: ${$json.path || $json.newPath || $json.oldPath || 'unknown'}`\n ].join('\\n')\n};"
},
"typeVersion": 2
}
],
"active": false,
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"timeSavedMode": "fixed",
"availableInMCP": false,
"executionOrder": "v1",
"executionTimeout": 300,
"timeSavedPerExecution": 5
},
"versionId": "9e8d2b52-6272-431e-86b5-f691e835270b",
"connections": {
"Analyze Bugs": {
"main": [
[
{
"node": "Merge Reviewer Results",
"type": "main",
"index": 0
}
]
]
},
"Verifier Model": {
"ai_languageModel": [
[
{
"node": "Verify Findings",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Verify Findings": {
"main": [
[
{
"node": "Normalize Verified Findings",
"type": "main",
"index": 0
}
]
]
},
"Combine Findings": {
"main": [
[
{
"node": "Verify Findings",
"type": "main",
"index": 0
}
]
]
},
"Bug Reviewer Model": {
"ai_languageModel": [
[
{
"node": "Analyze Bugs",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Split Changed Files": {
"main": [
[
{
"node": "Filter Supported Diffs",
"type": "main",
"index": 0
}
]
]
},
"Build Inline Comment": {
"main": [
[
{
"node": "Check Inline Position",
"type": "main",
"index": 0
}
]
]
},
"Check Findings Exist": {
"main": [
[
{
"node": "Build Summary Comment",
"type": "main",
"index": 0
},
{
"node": "Split Verified Findings",
"type": "main",
"index": 0
}
],
[
{
"node": "Build No-Issues Comment",
"type": "main",
"index": 0
}
]
]
},
"Build Summary Comment": {
"main": [
[
{
"node": "Post Summary Reply",
"type": "main",
"index": 0
}
]
]
},
"Check Inline Position": {
"main": [
[
{
"node": "Post Inline Review Comment",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Fallback Comment",
"type": "main",
"index": 0
}
]
]
},
"Analyze Security Risks": {
"main": [
[
{
"node": "Merge Reviewer Results",
"type": "main",
"index": 1
}
]
]
},
"Build Fallback Comment": {
"main": [
[
{
"node": "Post Fallback Reply",
"type": "main",
"index": 0
}
]
]
},
"Filter Supported Diffs": {
"main": [
[
{
"node": "Prepare Review Context",
"type": "main",
"index": 0
}
]
]
},
"Merge Reviewer Results": {
"main": [
[
{
"node": "Combine Findings",
"type": "main",
"index": 0
}
]
]
},
"Prepare Review Context": {
"main": [
[
{
"node": "Analyze Security Risks",
"type": "main",
"index": 0
},
{
"node": "Analyze Bugs",
"type": "main",
"index": 0
},
{
"node": "Analyze Maintainability Risks",
"type": "main",
"index": 0
}
]
]
},
"Workflow Configuration": {
"main": [
[
{
"node": "Check Review Trigger Comment",
"type": "main",
"index": 0
}
]
]
},
"Build No-Issues Comment": {
"main": [
[
{
"node": "Post Summary Reply",
"type": "main",
"index": 0
}
]
]
},
"Parse Bug Review Output": {
"ai_outputParser": [
[
{
"node": "Analyze Bugs",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Security Reviewer Model": {
"ai_languageModel": [
[
{
"node": "Analyze Security Risks",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Split Verified Findings": {
"main": [
[
{
"node": "Filter Publishable Findings",
"type": "main",
"index": 0
}
]
]
},
"GitLab Discussion Webhook": {
"main": [
[
{
"node": "Workflow Configuration",
"type": "main",
"index": 0
}
]
]
},
"Parse Verification Output": {
"ai_outputParser": [
[
{
"node": "Verify Findings",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Post Inline Review Comment": {
"main": [
[]
]
},
"Fetch Merge Request Changes": {
"main": [
[
{
"node": "Split Changed Files",
"type": "main",
"index": 0
}
]
]
},
"Filter Publishable Findings": {
"main": [
[
{
"node": "Resolve GitLab Diff Position",
"type": "main",
"index": 0
}
]
]
},
"Normalize Verified Findings": {
"main": [
[
{
"node": "Check Findings Exist",
"type": "main",
"index": 0
}
]
]
},
"Check Review Trigger Comment": {
"main": [
[
{
"node": "Fetch Merge Request Changes",
"type": "main",
"index": 0
},
{
"node": "Post Review Started Reply",
"type": "main",
"index": 0
}
]
]
},
"Parse Security Review Output": {
"ai_outputParser": [
[
{
"node": "Analyze Security Risks",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Resolve GitLab Diff Position": {
"main": [
[
{
"node": "Build Inline Comment",
"type": "main",
"index": 0
}
]
]
},
"Analyze Maintainability Risks": {
"main": [
[
{
"node": "Merge Reviewer Results",
"type": "main",
"index": 2
}
]
]
},
"Maintainability Reviewer Model": {
"ai_languageModel": [
[
{
"node": "Analyze Maintainability Risks",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Parse Maintainability Review Output": {
"ai_outputParser": [
[
{
"node": "Analyze Maintainability Risks",
"type": "ai_outputParser",
"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.
azureOpenAiApigitlabApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This template is for teams that use GitLab merge requests and want a practical AI-assisted review workflow in n8n. It is useful for engineering teams that want faster first-pass reviews, consistent review comments, and a simple way to separate likely bugs, security risks, and…
Source: https://n8n.io/workflows/14338/ — 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.
⏺ 🚀 How it works
Resume Screening & Behavioral Interviews with Gemini, Elevenlabs, & Notion ATS copy. Uses outputParserStructured, chainLlm, googleDrive, stickyNote. Webhook trigger; 67 nodes.
Candidate Engagement | Resume Screening | AI Voice Interviews | Applicant Insights
Fully automates your service order pipeline from incoming booking to supplier confirmation — with built-in SLA enforcement and automatic escalation if a supplier goes silent. 📥 Receives orders via web
🧠 Gwen – The AI Voice Marketing Agent Gwen is your intelligent voice-powered marketing assistant built in n8n. She combines the power of OpenAI, ElevenLabs, and automation workflows to handle content