This workflow follows the Chainllm → 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 →
{
"nodes": [
{
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
880,
540
],
"parameters": {
"content": "## Edit your own prompt \u2b07\ufe0f\n"
},
"typeVersion": 1
},
{
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-380,
580
],
"parameters": {
"content": "## Filter comments and customize your trigger words \u2b07\ufe0f"
},
"typeVersion": 1
},
{
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-120,
560
],
"parameters": {
"content": "## Replace your gitlab URL and token \u2b07\ufe0f"
},
"typeVersion": 1
},
{
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-540,
760
],
"parameters": {
"path": "e21095c0-1876-4cd9-9e92-a2eac737f03e",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 1.1
},
{
"name": "Code",
"type": "n8n-nodes-base.code",
"position": [
720,
540
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nvar diff = $input.item.json.gitDiff\n\nlet lines = diff.trimEnd().split('\\n');\n\nlet originalCode = '';\nlet newCode = '';\n\nlines.forEach(line => {\n console.log(line)\n if (line.startsWith('-')) {\n originalCode += line + \"\\n\";\n } else if (line.startsWith('+')) {\n newCode += line + \"\\n\";\n } else {\n originalCode += line + \"\\n\";\n newCode += line + \"\\n\";\n }\n});\n\nreturn {\n originalCode:originalCode,\n newCode:newCode\n};\n\n"
},
"typeVersion": 2
},
{
"name": "Split Out1",
"type": "n8n-nodes-base.splitOut",
"position": [
140,
740
],
"parameters": {
"options": {},
"fieldToSplitOut": "changes"
},
"typeVersion": 1
},
{
"name": "OpenAI Chat Model1",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
900,
860
],
"parameters": {
"options": {
"baseURL": ""
}
},
"typeVersion": 1
},
{
"name": "Get Changes1",
"type": "n8n-nodes-base.httpRequest",
"position": [
-60,
740
],
"parameters": {
"url": "=https://gitlab.com/api/v4/projects/{{ $json[\"body\"][\"project_id\"] }}/merge_requests/{{ $json[\"body\"][\"merge_request\"][\"iid\"] }}/changes",
"options": {},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "PRIVATE-TOKEN"
}
]
}
},
"typeVersion": 4.1
},
{
"name": "Skip File Change1",
"type": "n8n-nodes-base.if",
"position": [
340,
740
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
},
"leftValue": "={{ $json.renamed_file }}",
"rightValue": ""
},
{
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
},
"leftValue": "={{ $json.deleted_file }}",
"rightValue": ""
},
{
"operator": {
"type": "string",
"operation": "startsWith"
},
"leftValue": "={{ $json.diff }}",
"rightValue": "@@"
}
]
}
},
"typeVersion": 2
},
{
"name": "Parse Last Diff Line1",
"type": "n8n-nodes-base.code",
"position": [
540,
540
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const parseLastDiff = (gitDiff) => {\n gitDiff = gitDiff.replace(/\\n\\\\ No newline at end of file/, '')\n \n const diffList = gitDiff.trimEnd().split('\\n').reverse();\n const lastLineFirstChar = diffList?.[0]?.[0];\n const lastDiff =\n diffList.find((item) => {\n return /^@@ \\-\\d+,\\d+ \\+\\d+,\\d+ @@/g.test(item);\n }) || '';\n\n const [lastOldLineCount, lastNewLineCount] = lastDiff\n .replace(/@@ \\-(\\d+),(\\d+) \\+(\\d+),(\\d+) @@.*/g, ($0, $1, $2, $3, $4) => {\n return `${+$1 + +$2},${+$3 + +$4}`;\n })\n .split(',');\n \n if (!/^\\d+$/.test(lastOldLineCount) || !/^\\d+$/.test(lastNewLineCount)) {\n return {\n lastOldLine: -1,\n lastNewLine: -1,\n gitDiff,\n };\n }\n\n\n const lastOldLine = lastLineFirstChar === '+' ? null : (parseInt(lastOldLineCount) || 0) - 1;\n const lastNewLine = lastLineFirstChar === '-' ? null : (parseInt(lastNewLineCount) || 0) - 1;\n\n return {\n lastOldLine,\n lastNewLine,\n gitDiff,\n };\n};\n\nreturn parseLastDiff($input.item.json.diff)\n"
},
"typeVersion": 2
},
{
"name": "Post Discussions1",
"type": "n8n-nodes-base.httpRequest",
"position": [
1280,
720
],
"parameters": {
"url": "=https://gitlab.com/api/v4/projects/{{ $('Webhook').item.json[\"body\"][\"project_id\"] }}/merge_requests/{{ $('Webhook').item.json[\"body\"][\"merge_request\"][\"iid\"] }}/discussions",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "multipart-form-data",
"sendHeaders": true,
"bodyParameters": {
"parameters": [
{
"name": "body",
"value": "={{ $('Basic LLM Chain1').item.json[\"text\"] }}"
},
{
"name": "position[position_type]",
"value": "text"
},
{
"name": "position[old_path]",
"value": "={{ $('Split Out1').item.json.old_path }}"
},
{
"name": "position[new_path]",
"value": "={{ $('Split Out1').item.json.new_path }}"
},
{
"name": "position[start_sha]",
"value": "={{ $('Get Changes1').item.json.diff_refs.start_sha }}"
},
{
"name": "position[head_sha]",
"value": "={{ $('Get Changes1').item.json.diff_refs.head_sha }}"
},
{
"name": "position[base_sha]",
"value": "={{ $('Get Changes1').item.json.diff_refs.base_sha }}"
},
{
"name": "position[new_line]",
"value": "={{ $('Parse Last Diff Line1').item.json.lastNewLine || '' }}"
},
{
"name": "position[old_line]",
"value": "={{ $('Parse Last Diff Line1').item.json.lastOldLine || '' }}"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "PRIVATE-TOKEN"
}
]
}
},
"typeVersion": 4.1
},
{
"name": "Need Review1",
"type": "n8n-nodes-base.if",
"position": [
-320,
760
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.body.object_attributes.note }}",
"rightValue": "+0"
}
]
}
},
"typeVersion": 2
},
{
"name": "Basic LLM Chain1",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
880,
720
],
"parameters": {
"prompt": "=File path\uff1a{{ $('Skip File Change1').item.json.new_path }}\n\n```Original code\n {{ $json.originalCode }}\n```\nchange to\n```New code\n {{ $json.newCode }}\n```\nPlease review the code changes in this section:",
"messages": {
"messageValues": [
{
"message": "# Overview:\n You are a senior programming expert Bot, responsible for reviewing code changes and providing review recommendations.\n At the beginning of the suggestion, it is necessary to clearly make a decision to \"reject\" or \"accept\" the code change, and rate the change in the format \"Change Score: Actual Score\", with a score range of 0-100 points.\n Then, point out the existing problems in concise language and a stern tone.\n If you feel it is necessary, you can directly provide the modified content.\n Your review proposal must use rigorous Markdown format."
}
]
}
},
"typeVersion": 1.2
},
{
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1200,
540
],
"parameters": {
"content": "## Replace your gitlab URL and token \u2b07\ufe0f"
},
"typeVersion": 1
}
],
"connections": {
"Code": {
"main": [
[
{
"node": "Basic LLM Chain1",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Need Review1",
"type": "main",
"index": 0
}
]
]
},
"Split Out1": {
"main": [
[
{
"node": "Skip File Change1",
"type": "main",
"index": 0
}
]
]
},
"Get Changes1": {
"main": [
[
{
"node": "Split Out1",
"type": "main",
"index": 0
}
]
]
},
"Need Review1": {
"main": [
[
{
"node": "Get Changes1",
"type": "main",
"index": 0
}
]
]
},
"Basic LLM Chain1": {
"main": [
[
{
"node": "Post Discussions1",
"type": "main",
"index": 0
}
]
]
},
"Skip File Change1": {
"main": [
[
{
"node": "Parse Last Diff Line1",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model1": {
"ai_languageModel": [
[
{
"node": "Basic LLM Chain1",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Parse Last Diff Line1": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
How this works
Streamline your GitLab merge request reviews by automating code analysis with ChatGPT, saving developers hours of manual scrutiny and catching issues early to maintain high code quality. This workflow is ideal for engineering teams using GitLab who want AI-assisted feedback without disrupting their routine. It triggers on incoming webhooks from GitLab, extracts code changes via HTTP requests, and employs the OpenAI Chat Model to generate detailed reviews, highlighting potential bugs, style improvements, and security concerns in a single, integrated step.
Use this workflow for routine code reviews in collaborative projects where speed and consistency matter, such as agile sprints or open-source contributions. Avoid it for highly specialised domains like embedded systems, where custom tools outperform general AI, or when compliance requires human-only audits. Common variations include adding Slack notifications for review summaries or integrating with Jira to link feedback to tickets.
About this workflow
Chatgpt Automatic Code Review In Gitlab Mr. Uses stickyNote, splitOut, lmChatOpenAi, httpRequest. Webhook trigger; 14 nodes.
Source: https://github.com/Zie619/n8n-workflows — 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.
Fill iOS localization gaps from `.strings` → Google Sheets and PR with placeholders. Uses httpRequest, googleSheets. Webhook trigger; 11 nodes.
🔥 n8n Members Sale – n8n Community Members Get ideoGener8r for Just $10! (Reg. $15) Use Coupon Code: (Valid for n8n community members)
Code Github. Uses manualTrigger, stickyNote, n8n, httpRequest. Event-driven trigger; 25 nodes.
Display Project Data On A Smashing Dashboard. Uses httpRequest, github. Scheduled trigger; 24 nodes.
Code Github. Uses manualTrigger, stickyNote, httpRequest, noOp. Event-driven trigger; 24 nodes.