AutomationFlowsAI & RAG › Automate Code Reviews for Gitlab Mrs with Gemini AI and Jira Context

Automate Code Reviews for Gitlab Mrs with Gemini AI and Jira Context

ByEvgeny Agronsky @jenyok on n8n.io

Automates code review by listening for a comment trigger on GitLab merge requests, summarising the diff, and using an LLM to post constructive, line‑specific feedback. If a JIRA ticket ID is found in the MR description, the ticket’s summary is used to inform the AI review.…

Event trigger★★★★★ complexityAI-powered41 nodesChain LlmGoogle Gemini ChatJiraHTTP RequestError Trigger
AI & RAG Trigger: Event Nodes: 41 Complexity: ★★★★★ AI nodes: yes Added:
Automate Code Reviews for Gitlab Mrs with Gemini AI and Jira Context — n8n workflow card showing Chain Llm, Google Gemini Chat, Jira integration

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

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 →

Download .json
{
  "id": "Rwpn5OG2ql8rIOzH",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Gitlab Code Review Template",
  "tags": [],
  "nodes": [
    {
      "id": "c3fedf3c-ed52-49d7-b9d5-30050d4324f2",
      "name": "Need Review",
      "type": "n8n-nodes-base.if",
      "position": [
        -4256,
        1260
      ],
      "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": "coro-bot-review"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d30412f1-f714-41cb-a867-3a45f202c378",
      "name": "Skip File Changes",
      "type": "n8n-nodes-base.if",
      "position": [
        -2016,
        1408
      ],
      "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.changes.renamed_file }}",
              "rightValue": ""
            },
            {
              "id": "bf6e9eb9-d72d-459c-a722-9614bab8842c",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.changes.deleted_file }}",
              "rightValue": ""
            },
            {
              "id": "03200577-a262-4f46-ad25-9c15b0c8146d",
              "operator": {
                "type": "string",
                "operation": "startsWith"
              },
              "leftValue": "={{ $json.changes.diff }}",
              "rightValue": "@@"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "17752d5e-75d2-43ff-8e1b-ff439e6329b8",
      "name": "Basic LLM Chain",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -1344,
        1104
      ],
      "parameters": {
        "text": "=First, consider the context from the associated JIRA ticket:\n------------\n{{ $json.jiraParentContext || $json.jiraContext || 'No JIRA context was provided.' }}\n------------\n\nFile path\uff1a{{ $('Skip File Changes').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": "=You are an automated code review bot. Your primary goal is to identify high-value issues in code.\n\n**Instructions:**\n\n1. **Analysis Focus:** Concentrate only on business logic, correctness, security, and performance. Ignore style, naming, filenames, version bumps, and non-source files. Assume placeholders for yml files will be filled with correct values.\n2. **Output for Issues:** When you find an issue, begin your response with the prefix `\ud83e\udd16 **AI Review:** `. Follow it with a concise, actionable explanation. Prefer one-liners that are immediately actionable - what or where or why and how to fix.\n3. **Output for No Issues:** If your analysis finds **zero** issues worth reporting, your **ENTIRE** response must be the single keyword: `ALL_CLEAR`. Do not add any other text or explanation.\n4. ** Findings to ignore in yaml files:**\n    - Ignore if a value for vaultSecretPath in a values file or a yaml contains a double slash (//)\n    - Ignore errors where tag appears to be very or too specific\n\n**Important Rules:**\n\n* NEVER write praise, summaries, or conversational phrases like \"I have no findings.\"\n* If you find issues, start every comment with the `\ud83e\udd16 **AI Review:** ` prefix.\n* If you find no issues, don't output anything."
            }
          ]
        },
        "promptType": "define"
      },
      "typeVersion": 1.5
    },
    {
      "id": "483ec727-0b0b-46a7-9bd9-fbd52f2bb51a",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -1272,
        1328
      ],
      "parameters": {
        "options": {
          "topK": 1,
          "temperature": 0
        },
        "modelName": "models/gemini-2.5-pro"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "06884594-3ad3-4329-b357-21359eb13503",
      "name": "Extract the JIRA Issue ID",
      "type": "n8n-nodes-base.code",
      "position": [
        -3584,
        1304
      ],
      "parameters": {
        "jsCode": "// Get the MR description from the previous node's input\nconst description = $input.first().json.description\n// Regex to find a JIRA issue key (e.g., PROJ-123).\n// The \\b ensures it matches a whole word.\nconst jiraRegex = /\\b([A-Z]+-\\d+)\\b/;\nconst match = description.match(jiraRegex);\n\n// If a key is found, return it.\nif (match && match[0]) {\n  // We name the output 'jiraIssueKey' for clarity.\n  return [{\n    json: {\n      jiraIssueKey: match[0]\n    }\n  }];\n} else {\n  // Return an empty array to stop this path if no key is found.\n  return [];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "24204fee-6c47-4026-a6a6-12ced75a49da",
      "name": "Get JIRA issue",
      "type": "n8n-nodes-base.jira",
      "position": [
        -3360,
        1304
      ],
      "parameters": {
        "issueKey": "={{ $json.jiraIssueKey }}",
        "operation": "get",
        "additionalFields": {}
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7b1da623-5452-4783-ba8f-77896d3c8ea9",
      "name": "Format JIRA Context",
      "type": "n8n-nodes-base.set",
      "position": [
        -2688,
        1452
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "59f4091a-0260-4f24-aa8d-c211f7c243e2",
              "name": "jiraContext",
              "type": "string",
              "value": "=JIRA Ticket Context: \nTitle: {{ $json.fields.summary }} \nType: {{ $json.fields.issuetype.name }} \nDescription: {{ $json.fields.description }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "2ac7ced8-60b5-4fab-902f-bd1360e41a3a",
      "name": "Extract MR Details",
      "type": "n8n-nodes-base.set",
      "position": [
        -3808,
        1356
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "59f4091a-0260-4f24-aa8d-c211f7c243e2",
              "name": "projectId",
              "type": "string",
              "value": "={{ $json.body.project_id }}"
            },
            {
              "id": "cc5bdd4b-812f-461d-b3a9-059994176291",
              "name": "iid",
              "type": "string",
              "value": "={{ $json.body.merge_request.iid }}"
            },
            {
              "id": "7afeacf0-888f-4e75-be3c-d024cd223e3b",
              "name": "description",
              "type": "string",
              "value": "={{ $json.body.merge_request.description }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "9aeed83e-1f0b-461a-8e2b-75efb7be5ca5",
      "name": "If JIRA Subtask",
      "type": "n8n-nodes-base.if",
      "position": [
        -3136,
        1260
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "5338bc5b-9f30-4b66-b6a2-5b85dc012765",
              "operator": {
                "type": "object",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.fields.parent }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "bf6f14f1-b684-4100-9085-f97399c8911f",
      "name": "Get JIRA Parent Issue",
      "type": "n8n-nodes-base.jira",
      "position": [
        -2912,
        1260
      ],
      "parameters": {
        "issueKey": "={{ $json.fields.parent.key }}",
        "operation": "get",
        "additionalFields": {}
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "5e547d2f-c978-42bc-acb2-25c194308154",
      "name": "Format JIRA Parent Context",
      "type": "n8n-nodes-base.set",
      "position": [
        -2688,
        1260
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "59f4091a-0260-4f24-aa8d-c211f7c243e2",
              "name": "jiraParentContext",
              "type": "string",
              "value": "=JIRA Parent Ticket Context: \nTitle: {{ $json.fields.summary }} \nType: {{ $json.fields.issuetype.name }} \nDescription: {{ $json.fields.description }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d5e3ffd7-03d0-4b54-ab34-67fe43830531",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        -2464,
        1392
      ],
      "parameters": {
        "mode": "combine",
        "options": {
          "includeUnpaired": true
        },
        "combineBy": "combineByPosition",
        "numberInputs": 3
      },
      "typeVersion": 3.2
    },
    {
      "id": "a49bf76b-1a4b-4aba-b392-419ddc07e110",
      "name": "Get MR Changes",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2688,
        1644
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.projectId }}/merge_requests/{{ $json.iid }}/changes",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "8a3beea4-640b-4ddc-a801-719ace2643d0",
      "name": "Prepare Code Changes",
      "type": "n8n-nodes-base.code",
      "position": [
        -1568,
        1408
      ],
      "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 { ...$json, originalCode, newCode };\n\n"
      },
      "typeVersion": 2
    },
    {
      "id": "057832e9-fa3b-4fe5-a561-78b69e60c813",
      "name": "Error Trigger",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        352,
        1692
      ],
      "parameters": {},
      "executeOnce": false,
      "retryOnFail": false,
      "typeVersion": 1,
      "alwaysOutputData": false
    },
    {
      "id": "aeba4982-b0fc-4d97-a06b-35b559135361",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -768,
        1408
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "8ff7eb74-fbcc-4d4f-8b53-50ed4d543a82",
      "name": "Merge LLM Output with Input",
      "type": "n8n-nodes-base.merge",
      "position": [
        -992,
        1408
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "39f6aa6b-3296-47e6-b71d-59bcc7efd34c",
      "name": "Filter Irrelevant Fields",
      "type": "n8n-nodes-base.set",
      "position": [
        -1280,
        1504
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f40092dc-5c3a-4882-b33c-29e8c50f90a6",
              "name": "iid",
              "type": "string",
              "value": "={{$json.iid}}"
            },
            {
              "id": "38bd9645-0f83-4b66-9850-403e675a360d",
              "name": "project_id",
              "type": "string",
              "value": "={{$json.project_id}}"
            },
            {
              "id": "6b432472-ad72-4eaf-977d-9b2c80cbe2bf",
              "name": "diff_refs",
              "type": "object",
              "value": "={{$json.diff_refs}}"
            },
            {
              "id": "d24732f4-3f12-4b77-8df1-f34edb0d7f4d",
              "name": "lastNewLine",
              "type": "string",
              "value": "={{ $json.lastNewLine }}"
            },
            {
              "id": "4baecc19-56b2-472e-a015-bfc2e36727ce",
              "name": "lastOldLine",
              "type": "string",
              "value": "={{ $json.lastOldLine }}"
            },
            {
              "id": "12489e32-3521-4fd1-af78-dcaaba028ee1",
              "name": "new_path",
              "type": "string",
              "value": "={{ $json.changes.new_path }}"
            },
            {
              "id": "602b5de5-aeab-43bf-8ccb-2fce3853278c",
              "name": "old_path",
              "type": "string",
              "value": "={{ $json.changes.old_path }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "ebe94dfb-7395-4788-896d-14c3d5380c18",
      "name": "Any Issues Found?",
      "type": "n8n-nodes-base.if",
      "position": [
        -320,
        1408
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "70dd215a-813b-4bcf-9fa6-db4581ccad77",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.noIssuesFound }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "21e1a59a-3af6-45ba-83d8-73260736b92a",
      "name": "Split Out Changes",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -2240,
        1408
      ],
      "parameters": {
        "include": "selectedOtherFields",
        "options": {
          "includeBinary": true
        },
        "fieldToSplitOut": "changes",
        "fieldsToInclude": "jiraParentContext, jiraContext,iid,project_id,diff_refs"
      },
      "typeVersion": 1
    },
    {
      "id": "43acb79d-02c5-406b-9ae0-288e365518fa",
      "name": "Split Out Comments",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -96,
        1428
      ],
      "parameters": {
        "include": "selectedOtherFields",
        "options": {
          "includeBinary": true
        },
        "fieldToSplitOut": "commentsToPost",
        "fieldsToInclude": "iid,project_id,diff_refs"
      },
      "typeVersion": 1
    },
    {
      "id": "95e75bb0-94c4-492d-af8f-e6d79f442d92",
      "name": "Prepare Request",
      "type": "n8n-nodes-base.code",
      "position": [
        128,
        1428
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\n// Reference the nested 'commentsToPost' object\nconst postData = item.commentsToPost;\n\n// Create the nested position object\nconst position = {\n  position_type: 'text',\n  old_path: postData.old_path,\n  new_path: postData.new_path,\n  base_sha: postData.diff_refs.base_sha,\n  start_sha: postData.diff_refs.start_sha,\n  head_sha: postData.diff_refs.head_sha,\n};\n\n// Conditionally add the line numbers to the nested position object\nif (postData.lastNewLine !== null && postData.lastNewLine !== '') {\n  position.new_line = parseInt(postData.lastNewLine, 10);\n}\nif (postData.lastOldLine !== null && postData.lastOldLine !== '') {\n  position.old_line = parseInt(postData.lastOldLine, 10);\n}\n\n// Build the final request body with the nested 'position' key\nconst requestBody = {\n  body: postData.text,\n  position: position,\n};\n\n// Attach the final body for the next node\nitem.requestBody = requestBody;\n\nreturn item;"
      },
      "typeVersion": 2
    },
    {
      "id": "4e2ab9f1-0b12-4a25-8f2c-981500889660",
      "name": "Prepare Request Without Position",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        1500
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\n// Reference the nested 'commentsToPost' object\nconst postData = item.commentsToPost;\n\n// Build the final request body with the nested 'position' key\nconst requestBody = {\n  body: postData.text,\n};\n\n// Attach the final body for the next node\nitem.requestBody = requestBody;\n\nreturn item;"
      },
      "typeVersion": 2
    },
    {
      "id": "975691b1-9667-4913-aa56-bc177e202dae",
      "name": "Set workflow execution information",
      "type": "n8n-nodes-base.code",
      "position": [
        -4032,
        1260
      ],
      "parameters": {
        "jsCode": "// initialize staticData object\nconst workflowStaticData = $getWorkflowStaticData('global');\nconst executionId = $execution.id\nconst projectId = $input.first().json[\"body\"][\"project_id\"]\nconst mrId = $input.first().json[\"body\"][\"merge_request\"][\"iid\"]\n\nworkflowStaticData[executionId] = {\"projectId\":projectId, \"mrId\": mrId}\n\nreturn $input.all();"
      },
      "typeVersion": 2
    },
    {
      "id": "a24362b0-e288-47af-9756-eff4acde1682",
      "name": "Post Review Started",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -3808,
        1164
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.body.project_id }}/merge_requests/{{ $json.body.merge_request.iid }}/notes",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "body",
              "value": "\ud83e\udd16 AI code review initiated. This may take up to 30 minutes for large merge requests. I'll post my findings as comments on the relevant files."
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "81762b68-7a5d-4f03-8479-c209d2696970",
      "name": "Listen For Gitlab Comments",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -4480,
        1260
      ],
      "parameters": {
        "path": "REPLACE_WITH_UNIQUE_PATH",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "7ed4ec35-2b23-4b97-ba7d-f434ff54b93c",
      "name": "Parse Diff",
      "type": "n8n-nodes-base.code",
      "position": [
        -1792,
        1408
      ],
      "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\nconst extra = parseLastDiff($json.changes.diff);\nreturn { ...$json, ...extra };"
      },
      "typeVersion": 2
    },
    {
      "id": "601bc26b-1349-499c-a4eb-f08e3a064503",
      "name": "Post No Issues Found",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        800,
        1212
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.projectId }}/merge_requests/{{ $json.mrId }}/notes",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "body",
              "value": "=\ud83e\udd16 AI review complete. No significant issues were found. LGTM!"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "aa200c64-05c9-4437-8f18-980642e4ec6c",
      "name": "Post Error Occurred",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        800,
        1692
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.projectId }}/merge_requests/{{ $json.mrId }}/notes",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "body",
              "value": "=\ud83e\udd16 An error occurred during the code review: {{ $json.error }}. Please trigger the review again."
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e108c8b4-e8bb-4c43-aa01-0aa2cb70e83c",
      "name": "Get Static Context Including Error",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        1692
      ],
      "parameters": {
        "jsCode": "const workflowStaticData = $getWorkflowStaticData('global');\nconst executionId = $input.first().json.execution.id;\nconst errorMessage = $input.first().json.execution.error.message;\n\nreturn {...workflowStaticData[executionId], error: errorMessage};"
      },
      "typeVersion": 2
    },
    {
      "id": "1afcd3cf-8d54-4e11-8518-a82f246023ec",
      "name": "Get Static Context",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        1212
      ],
      "parameters": {
        "jsCode": "const workflowStaticData = $getWorkflowStaticData('global');\nconst executionId = $execution.id;\nreturn workflowStaticData[executionId];"
      },
      "typeVersion": 2
    },
    {
      "id": "21c6891d-1a57-4c4b-b7c4-41d84b20b744",
      "name": "Cleanup Static Context",
      "type": "n8n-nodes-base.code",
      "position": [
        1024,
        1504
      ],
      "parameters": {
        "jsCode": "const workflowStaticData = $getWorkflowStaticData('global');\nconst executionId = $execution.id;\ndelete workflowStaticData[executionId];\nreturn [];"
      },
      "typeVersion": 2
    },
    {
      "id": "1d97e536-a1c5-4520-a078-fb06baa377c4",
      "name": "Process Comments",
      "type": "n8n-nodes-base.code",
      "position": [
        -544,
        1408
      ],
      "parameters": {
        "jsCode": "const allItems = $input.first().json.data;\n\n// Filter for items that have actual review text\nconst commentsToPost = allItems.filter(item => {\n  const text = item.text || '';\n  const isNotEmpty = text.trim() !== '';\n  const isNotClear = !text.includes('ALL_CLEAR');\n  const isNotNoFindings = !text.toLowerCase().includes('no findings');\n  const isNotNoIssues = !text.toLowerCase().includes('no issues found');\n  return isNotEmpty && isNotClear && isNotNoFindings && isNotNoIssues;\n});\n\nreturn [{\n  json: {\n    commentsToPost: commentsToPost,\n    noIssuesFound: commentsToPost.length === 0\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "db40d370-1216-4f36-9d87-86f26524e4cd",
      "name": "Post Gitlab MR Comments",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        352,
        1428
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.commentsToPost.project_id }}/merge_requests/{{ $json.commentsToPost.iid }}/discussions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.requestBody }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "retryOnFail": false,
      "typeVersion": 4.2
    },
    {
      "id": "eca87aa8-3d57-4513-a2d0-98d8cfcd1174",
      "name": "Try It Out!",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -5264,
        1072
      ],
      "parameters": {
        "color": 6,
        "width": 640,
        "height": 288,
        "content": "### \u2b50\ufe0f Try It Out!\n\nWelcome! This template adds AI-powered code reviews to your GitLab Merge Requests. Simply comment **`ai-review`** on any MR and watch the workflow:\n\n- Captures the comment event.\n- Fetches diff & optional JIRA context.\n- Asks an LLM for insights.\n- Posts inline comments or an 'all clear' note.\n\nPerfect for catching logic, security and performance issues fast."
      },
      "typeVersion": 1
    },
    {
      "id": "9bf7c660-9b1d-430c-95e9-37e89aae852d",
      "name": "Step 1: Listen & Capture",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4480,
        1488
      ],
      "parameters": {
        "color": 2,
        "width": 600,
        "height": 232,
        "content": "### \ud83d\udfe2 Step 1: Listen & Capture\n\nA Webhook node listens for merge request note events. When the comment text matches your trigger (default: `ai-review`), it:\n\n- Extracts the Project & MR IDs and stores them in a **Static Context** (see below) so later nodes always know where to post results.\n- Posts a short \"AI review started\" message back to the MR to let reviewers know the bot is working."
      },
      "typeVersion": 1
    },
    {
      "id": "f2e6745f-cb17-4d0c-a5c9-c3031759100a",
      "name": "Step 2: Prepare & Review",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2432,
        960
      ],
      "parameters": {
        "color": 4,
        "width": 600,
        "height": 280,
        "content": "### \ud83d\udfe3 Step 2: Prepare & Review\n\nThe workflow fetches the merge request changes via the GitLab API and parses the diff into original vs new code blocks.\n\nIf the MR description contains a Jira key, the workflow fetches the issue summary and, if it's a subtask, also fetches the parent ticket for extra context. Both summaries are combined into the prompt.\n\nAn LLM (Gemini by default) is prompted to find critical issues only\u2014logic bugs, security flaws and performance problems\u2014and to return comments with file and line numbers. If no issues exist, the model returns `ALL_CLEAR`."
      },
      "typeVersion": 1
    },
    {
      "id": "1bd4a994-6826-4b49-87a8-9214508b94c8",
      "name": "Step 3: Post & Fallback",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1232,
        1456
      ],
      "parameters": {
        "color": 5,
        "width": 600,
        "height": 184,
        "content": "### \ud83d\udfe0 Step 3: Post & Fallback\n\nReview comments are converted into GitLab discussion payloads and posted back to the MR at the correct file and line positions.\n\nIf a comment\u2019s position is not calculated properly, the workflow falls back to posting a comment as a thread at the MR level so nothing is lost."
      },
      "typeVersion": 1
    },
    {
      "id": "2d50734d-8c63-4a52-8459-2719710ca2a6",
      "name": "Static Context & Error Handling",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -96,
        1904
      ],
      "parameters": {
        "width": 600,
        "height": 216,
        "content": "### \ud83d\udd35 Static Context & Error Handling\n\nWorkflow Static Data is used to store persistent values\u2014like the project ID and the MR ID so that they will be available across branches.\n\nThis is critical for the Error Trigger: if something fails, the error path can still post to the correct MR using these stored IDs. Without static context, error comments wouldn\u2019t know where to go."
      },
      "typeVersion": 1
    },
    {
      "id": "223b57bd-6ea3-4e32-ad29-9fec88550b1b",
      "name": "Need Help & Customise",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1504,
        1744
      ],
      "parameters": {
        "color": 7,
        "width": 600,
        "height": 168,
        "content": "### \ud83d\udfe1 Need Help & Customise\n\n- Change the trigger word in the **IF** node to match your own.\n- Swap in your preferred LLM or adjust the prompt to suit your guidelines.\n- Filter by file type or exclude certain directories in the parsing logic."
      },
      "typeVersion": 1
    },
    {
      "id": "917539f7-c7a2-48a4-9227-93287eeeded9",
      "name": "Post Gitlab MR Comments WIthout Position",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        800,
        1500
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.commentsToPost.project_id }}/merge_requests/{{ $json.commentsToPost.iid }}/discussions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.requestBody }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "retryOnFail": false,
      "typeVersion": 4.2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "0798cf1c-bb7b-4765-bf7e-7dead605266f",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Split Out Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Process Comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Diff": {
      "main": [
        [
          {
            "node": "Prepare Code Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Need Review": {
      "main": [
        [
          {
            "node": "Set workflow execution information",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Trigger": {
      "main": [
        [
          {
            "node": "Get Static Context Including Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get JIRA issue": {
      "main": [
        [
          {
            "node": "Format JIRA Context",
            "type": "main",
            "index": 0
          },
          {
            "node": "If JIRA Subtask",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get MR Changes": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Basic LLM Chain": {
      "main": [
        [
          {
            "node": "Merge LLM Output with Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If JIRA Subtask": {
      "main": [
        [
          {
            "node": "Get JIRA Parent Issue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Request": {
      "main": [
        [
          {
            "node": "Post Gitlab MR Comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Comments": {
      "main": [
        [
          {
            "node": "Any Issues Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any Issues Found?": {
      "main": [
        [
          {
            "node": "Get Static Context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Split Out Comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip File Changes": {
      "main": [
        [
          {
            "node": "Parse Diff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out Changes": {
      "main": [
        [
          {
            "node": "Skip File Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract MR Details": {
      "main": [
        [
          {
            "node": "Get MR Changes",
            "type": "main",
            "index": 0
          },
          {
            "node": "Extract the JIRA Issue ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Static Context": {
      "main": [
        [
          {
            "node": "Post No Issues Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out Comments": {
      "main": [
        [
          {
            "node": "Prepare Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format JIRA Context": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Post Error Occurred": {
      "main": [
        [
          {
            "node": "Cleanup Static Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post No Issues Found": {
      "main": [
        [
          {
            "node": "Cleanup Static Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Code Changes": {
      "main": [
        [
          {
            "node": "Basic LLM Chain",
            "type": "main",
            "index": 0
          },
          {
            "node": "Filter Irrelevant Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get JIRA Parent Issue": {
      "main": [
        [
          {
            "node": "Format JIRA Parent Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Gitlab MR Comments": {
      "main": [
        [
          {
            "node": "Cleanup Static Context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Request Without Position",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Irrelevant Fields": {
      "main": [
        [
          {
            "node": "Merge LLM Output with Input",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Basic LLM Chain",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Extract the JIRA Issue ID": {
      "main": [
        [
          {
            "node": "Get JIRA issue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format JIRA Parent Context": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Listen For Gitlab Comments": {
      "main": [
        [
          {
            "node": "Need Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge LLM Output with Input": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Request Without Position": {
      "main": [
        [
          {
            "node": "Post Gitlab MR Comments WIthout Position",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Static Context Including Error": {
      "main": [
        [
          {
            "node": "Post Error Occurred",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set workflow execution information": {
      "main": [
        [
          {
            "node": "Post Review Started",
            "type": "main",
            "index": 0
          },
          {
            "node": "Extract MR Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Gitlab MR Comments WIthout Position": {
      "main": [
        [
          {
            "node": "Cleanup Static Context",
            "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.

Pro

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

About this workflow

Automates code review by listening for a comment trigger on GitLab merge requests, summarising the diff, and using an LLM to post constructive, line‑specific feedback. If a JIRA ticket ID is found in the MR description, the ticket’s summary is used to inform the AI review.…

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

Stop wasting hours watching long videos. This n8n workflow acts as your personal "TL;DW" (Too Long; Didn't Watch) assistant. It automatically pulls YouTube transcripts using Decodo, analyzes them with

HTTP Request, Telegram, Telegram Trigger +3
AI & RAG

This automated TLDW (Too Long; Didn't Watch) generator using Decodo's scraping API to extract complete video transcripts and metadata, then uses Google Gemini 3 to create intelligent summaries with ke

HTTP Request, Telegram, Telegram Trigger +3
AI & RAG

This workflow scrapes Google Maps via Decodo API, analyzes each business using Google Gemini 3 Flash, scores lead quality, and generates ready to send outreach emails. Time Savings: Reduces manual lea

Google Sheets, Error Trigger, Output Parser Structured +4
AI & RAG

This workflow creates a multi-talented AI assistant named Simran that interacts with users via Telegram. It can handle text and voice messages, understand the user's intent, and perform various tasks.

MongoDB, Chain Llm, Google Gemini Chat +11
AI & RAG

This workflow automates Invoice & Payment Tracking (with Approvals) across Notion and Slack. Ingest — You drop invoices/receipts (PDF/IMG/JSON) into the flow. Extract — OCR + parsing pulls out key fie

HTTP Request, Chain Llm, Anthropic Chat +5