AutomationFlowsSlack & Telegram › Gitlab Mr Code Review

Gitlab Mr Code Review

GitLab MR Code Review. Uses httpRequest, slack. Scheduled trigger; 18 nodes.

Cron / scheduled trigger★★★★☆ complexity18 nodesHTTP RequestSlack
Slack & Telegram Trigger: Cron / scheduled Nodes: 18 Complexity: ★★★★☆ Added:

This workflow follows the HTTP Request → Slack 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
{
  "name": "GitLab MR Code Review",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 1
            }
          ]
        }
      },
      "id": "schedule",
      "name": "Every Minute",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        416,
        304
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://__GITLAB_HOST__/api/v4/projects/{{ encodeURIComponent('__GITLAB_PROJECT__') }}/merge_requests",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "state",
              "value": "opened"
            },
            {
              "name": "per_page",
              "value": "20"
            },
            {
              "name": "order_by",
              "value": "updated_at"
            },
            {
              "name": "sort",
              "value": "desc"
            }
          ]
        },
        "options": {}
      },
      "id": "get-mrs",
      "name": "Get Open MRs",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        608,
        304
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const CONFIG = { GITLAB_HOST: '__GITLAB_HOST__', GITLAB_PROJECT: '__GITLAB_PROJECT__' };\nconst mrs = $input.all().map(i => i.json);\n\nconst toReview = mrs.filter(mr => {\n  const labels = mr.labels || [];\n  const hasInProgress = labels.includes('ai-review::in-progress');\n  const reviewedSha = labels.find(l => l.startsWith('ai-review::sha:'))?.replace('ai-review::sha:', '');\n  return !hasInProgress && reviewedSha !== mr.sha;\n});\n\nif (toReview.length === 0) return [];\n\nconst mr = toReview[0];\nreturn [{\n  json: {\n    mr_iid: mr.iid,\n    mr_title: mr.title,\n    mr_url: mr.web_url,\n    source_branch: mr.source_branch,\n    target_branch: mr.target_branch,\n    author: mr.author?.username,\n    project_path: CONFIG.GITLAB_PROJECT,\n    gitlab_host: CONFIG.GITLAB_HOST,\n    sha: mr.sha,\n    reviewers: (mr.reviewers || []).map(r => r.username),\n    labels: mr.labels || []\n  }\n}];"
      },
      "id": "filter-mrs",
      "name": "Filter",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        784,
        304
      ]
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://{{ $json.gitlab_host }}/api/v4/projects/{{ encodeURIComponent($json.project_path) }}/merge_requests/{{ $json.mr_iid }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ add_labels: 'ai-review::in-progress', remove_labels: $json.labels.filter(l => l.startsWith('ai-review::')).join(',') || undefined }) }}",
        "options": {}
      },
      "id": "label-in-progress",
      "name": "In Progress",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        960,
        304
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://{{ $('Filter').first().json.gitlab_host }}/api/v4/projects/{{ encodeURIComponent($('Filter').first().json.project_path) }}/merge_requests/{{ $('Filter').first().json.mr_iid }}/discussions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "get-discussions",
      "name": "Get Discussions",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1152,
        304
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const AI_MARKER = 'AI Code Review';\nconst mr = $('Filter').first().json;\nconst discussions = $input.all().map(i => i.json);\n\nconst threads = [];\nfor (const d of discussions) {\n  const notes = d.notes || [];\n  if (notes.length === 0) continue;\n  \n  const isAI = n => (n.body || '').split('\\n')[0].includes(AI_MARKER);\n  const isUser = n => !n.system && !isAI(n);\n  \n  if (notes.some(isAI) && !notes.some(isUser)) continue;\n  \n  const thread = { notes: [], position: null };\n  for (const note of notes) {\n    if (note.system) continue;\n    thread.notes.push({ author: note.author?.username, body: (note.body || '').slice(0, 500), isAI: isAI(note) });\n    if (note.type === 'DiffNote' && note.position && !thread.position) {\n      thread.position = { file: note.position.new_path || note.position.old_path, line: note.position.new_line || note.position.old_line };\n    }\n  }\n  if (thread.notes.length > 0) threads.push(thread);\n}\n\nreturn { json: { ...mr, threads } };"
      },
      "id": "process-notes",
      "name": "Process Notes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1328,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "const mr = $input.first().json;\nconst threads = mr.threads || [];\n\nlet prompt = `Review MR !${mr.mr_iid}: ${mr.mr_title}\\n\\n**Branch**: \\`${mr.source_branch}\\` \u2192 \\`${mr.target_branch}\\`\\n**Author**: ${mr.author}\\n**URL**: ${mr.mr_url}`;\n\nif (threads.length > 0) {\n  prompt += '\\n\\n## Discussion Threads';\n  for (const thread of threads) {\n    prompt += thread.position ? `\\n\\n### \\`${thread.position.file}:${thread.position.line}\\`` : '\\n\\n### General Discussion';\n    for (const n of thread.notes) {\n      prompt += `\\n- ${n.isAI ? '\ud83e\udd16 AI' : `@${n.author}`}: \"${n.body}\"`;\n    }\n  }\n}\n\nreturn { json: { ...mr, formatted_message: prompt } };"
      },
      "id": "build-prompt",
      "name": "Build Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1504,
        304
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.N8N_API_URL }}/v1/projects/system/chat",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ user_message: $json.formatted_message, agent: 'MR Reviewer', source: 'gitlab', requester: 'gitlab-mr-workflow', metadata: { mr_iid: $json.mr_iid, mr_url: $json.mr_url, author: $json.author, source_branch: $json.source_branch, target_branch: $json.target_branch, workflow_execution_id: $executionId } }) }}",
        "options": {
          "timeout": 660000
        }
      },
      "id": "execute-claude",
      "name": "Execute",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1680,
        304
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "success",
              "leftValue": "={{ $json.status }}",
              "rightValue": "completed",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "check-result",
      "name": "Success?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1856,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "const mr = $('Build Prompt').item.json;\nconst result = $('Execute').item.json;\nconst output = result.structured_output || {};\n\nconst review = {\n  verdict: output.verdict || 'comment',\n  gitlab_comment: output.gitlab_comment || '## \ud83d\udd0d AI Code Review\\n\\nReview completed.',\n  summary: output.summary || '\ub9ac\ubdf0 \uc644\ub8cc',\n  points: output.points || []\n};\n\nreturn { json: { mr, review, reviewers: mr.reviewers || [] } };"
      },
      "id": "parse-review",
      "name": "Parse Review",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2032,
        208
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://{{ $json.mr.gitlab_host }}/api/v4/projects/{{ encodeURIComponent($json.mr.project_path) }}/merge_requests/{{ $json.mr.mr_iid }}/notes",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ body: $json.review.gitlab_comment }) }}",
        "options": {}
      },
      "id": "post-gitlab-comment",
      "name": "Post GitLab Comment",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2208,
        208
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const data = $('Parse Review').item.json;\nconst mr = data.mr;\nconst review = data.review;\nconst reviewers = data.reviewers || [];\nconst dashboardUrl = $env.N8N_DASHBOARD_URL;\n\n// Verdict emoji\nconst verdictEmoji = { approve: ':white_check_mark:', request_changes: ':warning:', comment: ':speech_balloon:' };\nconst emoji = verdictEmoji[review.verdict] || ':speech_balloon:';\n\n// Build message parts\nconst header = `${emoji} <${mr.mr_url}|!${mr.mr_iid}> ${mr.mr_title}`;\nconst summary = review.summary;\nconst points = (review.points || []).map(p => `\u2022 ${p}`).join('\\n');\n\n// Reviewer mentions\nconst mentions = [];\nfor (const username of reviewers) {\n  if (!username) continue;\n  try {\n    const res = await this.helpers.httpRequest({ method: 'GET', url: `${dashboardUrl}/api/plugins/slack/users`, qs: { q: username, limit: 1 } });\n    if (res.users?.[0]) mentions.push(`<@${res.users[0].id}>`);\n  } catch (e) {}\n}\n\n// Assemble final message\nlet message = header + '\\n' + summary;\nif (points) message += '\\n' + points;\nif (mentions.length > 0) message += '\\n' + mentions.join(' ');\n\nreturn { json: { message, mr, review } };"
      },
      "id": "build-message",
      "name": "Build Message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2384,
        208
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "__MR_REVIEW_CHANNEL__"
        },
        "text": "={{ $json.message }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "post-slack",
      "name": "Post Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        2576,
        208
      ],
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://{{ $('Build Prompt').item.json.gitlab_host }}/api/v4/projects/{{ encodeURIComponent($('Build Prompt').item.json.project_path) }}/merge_requests/{{ $('Build Prompt').item.json.mr_iid }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ add_labels: 'ai-review::done,ai-review::sha:' + $('Build Prompt').item.json.sha, remove_labels: 'ai-review::in-progress' }) }}",
        "options": {}
      },
      "id": "label-done",
      "name": "Done",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2752,
        208
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.N8N_API_URL }}/v1/workflows/stats",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ workflow: 'gitlab-mr-review', execution_id: $('Execute').item.json.id, status: 'success', duration_ms: $('Execute').item.json.duration_ms, metadata: { mr_iid: $('Build Prompt').item.json.mr_iid, mr_title: $('Build Prompt').item.json.mr_title } }) }}",
        "options": {
          "timeout": 5000
        }
      },
      "id": "record-stats-ok",
      "name": "Stats OK",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2928,
        208
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://{{ $('Build Prompt').item.json.gitlab_host }}/api/v4/projects/{{ encodeURIComponent($('Build Prompt').item.json.project_path) }}/merge_requests/{{ $('Build Prompt').item.json.mr_iid }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ add_labels: 'ai-review::failed', remove_labels: 'ai-review::in-progress' }) }}",
        "options": {}
      },
      "id": "label-failed",
      "name": "Failed",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2032,
        400
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "__MR_REVIEW_CHANNEL__"
        },
        "text": "={{ `:x: *MR Review Failed*\\n<${$('Build Prompt').item.json.mr_url}|!${$('Build Prompt').item.json.mr_iid}> ${$('Build Prompt').item.json.mr_title}\\n${$('Success?').item.json.status === 'timeout' ? ':hourglass: Timeout' : ':warning: ' + ($('Success?').item.json.error?.message || 'Unknown error')}` }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "notify-failure",
      "name": "Notify Failure",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        2208,
        400
      ],
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.N8N_API_URL }}/v1/workflows/stats",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ workflow: 'gitlab-mr-review', execution_id: $('Execute').item.json.id, status: $('Success?').item.json.status === 'timeout' ? 'timeout' : 'error', duration_ms: $('Execute').item.json.duration_ms, metadata: { mr_iid: $('Build Prompt').item.json.mr_iid, error: $('Success?').item.json.error?.message } }) }}",
        "options": {
          "timeout": 5000
        }
      },
      "id": "record-stats-err",
      "name": "Stats Err",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2384,
        400
      ],
      "onError": "continueRegularOutput"
    }
  ],
  "connections": {
    "Every Minute": {
      "main": [
        [
          {
            "node": "Get Open MRs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Open MRs": {
      "main": [
        [
          {
            "node": "Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter": {
      "main": [
        [
          {
            "node": "In Progress",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "In Progress": {
      "main": [
        [
          {
            "node": "Get Discussions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Discussions": {
      "main": [
        [
          {
            "node": "Process Notes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Notes": {
      "main": [
        [
          {
            "node": "Build Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Prompt": {
      "main": [
        [
          {
            "node": "Execute",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute": {
      "main": [
        [
          {
            "node": "Success?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Success?": {
      "main": [
        [
          {
            "node": "Parse Review",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Review": {
      "main": [
        [
          {
            "node": "Post GitLab Comment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post GitLab Comment": {
      "main": [
        [
          {
            "node": "Build Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Message": {
      "main": [
        [
          {
            "node": "Post Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Slack": {
      "main": [
        [
          {
            "node": "Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Done": {
      "main": [
        [
          {
            "node": "Stats OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Failed": {
      "main": [
        [
          {
            "node": "Notify Failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify Failure": {
      "main": [
        [
          {
            "node": "Stats Err",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false
  }
}

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

GitLab MR Code Review. Uses httpRequest, slack. Scheduled trigger; 18 nodes.

Source: https://github.com/junyeong-ai/claudio/blob/5899280b27ce362463a092893adca6f22728dc55/n8n-workflows/gitlab-mr-review.json — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

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

Slack & Telegram

debug. Uses httpRequest, slack, redis, mailgun. Scheduled trigger; 60 nodes.

HTTP Request, Slack, Redis +2
Slack & Telegram

Seller Follow-Up Engine (Enhanced). Uses httpRequest, slack. Scheduled trigger; 44 nodes.

HTTP Request, Slack
Slack & Telegram

This workflow is an automated employee time tracking and reporting system that monitors weekly work hours via TMetric, then delivers personalized summaries directly to each team member on Slack. It co

HTTP Request, Item Lists, Data Table +1
Slack & Telegram

Import Productboard Notes Companies And Features Into Snowflake. Uses stickyNote, httpRequest, splitOut, snowflake. Scheduled trigger; 35 nodes.

HTTP Request, Snowflake, Slack
Slack & Telegram

Import Productboard Notes, Companies and Features into Snowflake. Uses stickyNote, httpRequest, splitOut, snowflake. Scheduled trigger; 35 nodes.

HTTP Request, Snowflake, Slack