MergeMe logoMergeMe
Published on

GitLab Draft Merge Request Slack Notifications: Alert When Ready

Authors
  • Name
    Maruan

A developer opens a GitLab merge request thirty minutes into a task. They mark it as a draft so pipelines can run against their branch, test artifacts generate, and colleagues can inspect the diff if asked. Five seconds later, your engineering Slack channel pings three senior reviewers with a review request.

The reviewers click through, find unformatted code and failing tests, and close the tab. Two hours later, the author removes the draft prefix and actually needs feedback. Nobody responds. The notification fired at the wrong moment, so the team ignored the real handoff.

Good GitLab draft merge request Slack notifications treat readiness as a state change, not a static boolean. If your integration checks only whether a merge request is published, you either spam channels on every push or miss the exact moment code needs eyes. The fix is to inspect the webhook payload transition.

Why draft-to-ready is a transition, not a state

Static checks create notification fatigue. If a bot inspects a webhook and asks only, "Is this merge request currently ready?", the answer is yes on almost every event after publication. A title fix, a new commit push, an added label, or a rebased branch will each evaluate to true. That logic turns your engineering channels into an unreadable stream of repetitive alerts.

A transition is different. It needs two distinct points in time: the merge request was a draft an instant ago, and it is ready right now. Merge requests often sit idle for hours before pickup. When teams get alerts for intermediate commits on an already open request, pickup times grow because developers tune out channel activity.

Your automation has to detect the boundary crossing. Your webhook listener should fire exactly once: when the author clicks "Mark as ready" in the GitLab interface, or changes the title from Draft: feat: new parser to feat: new parser. Everything else is background maintenance.

What the Merge Request Hook payload gives you: event header, object_attributes.draft, and changes.draft

GitLab delivers webhook payloads as HTTP POST requests with specific headers and a structured JSON body. For merge requests, the request always carries the header X-Gitlab-Event set to Merge Request Hook. If the header says anything else, your receiver should ignore the payload immediately.

Inside the JSON body, two objects matter: object_attributes and changes. The object_attributes dictionary reflects the state of the merge request after the event completed. Within it, draft is a boolean showing the current status. Historically, GitLab represented drafts with a work_in_progress boolean and a WIP: title prefix. GitLab documents draft as the current field and work_in_progress as deprecated, though work_in_progress is often kept for backwards compatibility.

The field that matters for ready alerts is changes. When an author toggles draft status, GitLab populates changes.draft with previous and current values:

{ "changes": { "draft": { "previous": true, "current": false } } }

If changes.draft is present, GitLab recorded an explicit transition. If it's missing, the draft state did not change during this webhook event, whatever object_attributes.draft reports.

Three cases to separate: draft to ready, ordinary update, and created as non-draft

Reliable routing means sorting incoming payloads into three mutually exclusive categories before you compose a Slack message.

Case 1 is the draft-to-ready transition. Here, object_attributes.action equals "update", changes.draft.previous is true, and changes.draft.current is false. This is the exact event you want to capture so team members know review work can begin.

Case 2 is an ordinary update. The action might be "update", but changes.draft is absent, or changes.draft.previous and changes.draft.current share the same boolean value. This happens when a developer pushes a new commit to an active merge request, edits the description markdown, or modifies reviewers. If your goal is only a ready-for-review ping, your receiver should drop these payloads silently.

Case 3 is a merge request created directly as non-draft. Here, object_attributes.action equals "open" and object_attributes.draft is false. The item was never a draft, so changes.draft will not exist in the payload. If you want Slack alerts for brand-new, immediately reviewable work as well as un-drafted work, support both Case 1 and Case 3 and discard Case 2.

Treating Case 2 and Case 3 identically is the most common reason custom webhook scripts fail. Teams write a rule that checks whether object_attributes.draft is false, and that accidentally routes every Case 2 pipeline update and description edit into Slack as if it were newly ready.

A short defensive handler (illustrative custom automation): verify X-Gitlab-Token in constant time, check X-Gitlab-Event, validate fields, build your own Slack message

Below is an illustrative Node.js handler using Express. It shows the validation checks you need to accept GitLab webhooks securely and isolate ready-for-review events. This is custom automation you run on your own infrastructure, not an off-the-shelf app.

import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json());

const GITLAB_SECRET_TOKEN = process.env.GITLAB_WEBHOOK_SECRET;
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;

function verifySecret(headerToken) {
  if (!headerToken || !GITLAB_SECRET_TOKEN) return false;
  const headerBuffer = Buffer.from(headerToken);
  const secretBuffer = Buffer.from(GITLAB_SECRET_TOKEN);
  if (headerBuffer.length !== secretBuffer.length) return false;
  return crypto.timingSafeEqual(headerBuffer, secretBuffer);
}

app.post('/webhook/gitlab', async (req, res) => {
  const token = req.headers['x-gitlab-token'];
  if (!verifySecret(token)) {
    return res.status(401).send('Unauthorized');
  }

  const event = req.headers['x-gitlab-event'];
  if (event !== 'Merge Request Hook') {
    return res.status(200).send('Ignored: non-MR event');
  }

  const { object_attributes, changes, project, user } = req.body;
  if (!object_attributes) {
    return res.status(400).send('Bad Request: Missing attributes');
  }

  const isDraftToReady =
    object_attributes.action === 'update' &&
    changes?.draft?.previous === true &&
    changes?.draft?.current === false;

  const isDirectlyOpened =
    object_attributes.action === 'open' &&
    object_attributes.draft === false;

  if (!isDraftToReady && !isDirectlyOpened) {
    return res.status(200).send('Ignored: not a ready-for-review event');
  }

  const triggerReason = isDraftToReady ? 'Marked Ready for Review' : 'New Merge Request';
  const slackMessage = {
    text: `${triggerReason}: <${object_attributes.url}|${object_attributes.title}>`,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*${triggerReason}* in *${project.path_with_namespace}*\n<${object_attributes.url}|*${object_attributes.title}*>\nAuthor: ${user.name}`
        }
      }
    ]
  };

  await fetch(SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(slackMessage)
  });

  return res.status(200).send('Alert sent');
});

Note the timing-safe comparison. Standard string equality operators (== or ===) leak execution time differences based on how many characters match, which opens a potential side-channel vulnerability. Always use crypto.timingSafeEqual.

Deduplicating retries with Idempotency-Key and keeping webhook URLs and private payloads out of logs

GitLab treats a response timeout as a delivery failure, with a default timeout of 10 seconds that can be configured on self-managed instances. After a timeout, GitLab retries delivery automatically. If your handler sent the Slack notification before timing out on a database write, the retry will send a duplicate alert to your channel.

To prevent duplicates, inspect the delivery headers. GitLab includes an X-Gitlab-Event-UUID header with a unique ID for non-recursive webhooks. Store recent delivery IDs in an in-memory cache or a Redis key with a 300-second expiration. If your receiver sees a UUID it already processed, return HTTP 200 immediately without pinging Slack.

Logging needs discipline too. Webhook payloads contain internal repository names, commit messages, author details, and sometimes full git diffs. Never log the entire req.body to standard output or a cloud logging aggregator. If you have to debug webhook issues, log only a sanitized payload with the delivery UUID, the event type, the merge request IID, and the boolean values of changes.draft. Keep your Slack incoming webhook URL in a secrets manager and out of application stack traces.

Test fixtures for each case, and where built-in tools fit

Before pointing a live GitLab webhook at your custom receiver, send simulated payloads using curl.

To test Case 1 (draft to ready):

{
  "object_attributes": {
    "action": "update",
    "draft": false,
    "title": "Support PKCE in OAuth flow",
    "url": "https://gitlab.example.com/org/repo/-/merge_requests/42"
  },
  "changes": {
    "draft": { "previous": true, "current": false }
  },
  "project": { "path_with_namespace": "org/repo" },
  "user": { "name": "Alex Rivera" }
}

To test Case 2 (ordinary update), send changes without draft transitions, such as a description update, and verify the handler responds with HTTP 200 and suppresses the Slack post. If webhooks look correctly configured in your project settings but notifications don't arrive, follow our guide on GitLab Slack test works but merge request notifications missing to diagnose silent project configuration drops. You can also read how to control when PR/MR cards post to Slack across team channels.

Building your own microservice gives you full control over logic, but you own the server maintenance, TLS certificates, uptime, and payload format changes. The official GitLab Slack app handles basic event forwarding, but it posts individual messages per event instead of consolidating updates.

If you want unified PR and MR notifications without building custom handlers, MergeMe works out of the box. MergeMe delivers Slack notifications for GitHub pull requests and GitLab merge requests, shown as one updating card per PR/MR. The card updates in place through the lifecycle of the merge request. It works with GitHub.com, GitLab.com, and self-hosted GitLab instances. Pricing is simple: Hobby is free forever with 1 channel mapping and 5 user mappings, while the Team plan is £5 per dev seat monthly (with a 5 seat minimum). Teams that need on-premises data control can contact hello@mergeme.dev for self-hosting options.

Diagram: decision flow from webhook delivery to Slack post

To build your integration accurately, follow this sequential decision flow from initial HTTP ingress to the Slack API call:

+-------------------------------------------------------+
|             Incoming POST to Webhook URL              |
+-------------------------------------------------------+
                           |
                           v
              [ Valid X-Gitlab-Token? ]
                     /          \
                   No            Yes
                   /              \
           Reject 401              v
                     [ X-Gitlab-Event == MR Hook? ]
                                 /          \
                               No            Yes
                               /              \
                       Ignore 200              v
                                   [ Seen Delivery UUID? ]
                                           /         \
                                         Yes          No
                                         /             \
                                 Ignore 200             v
                                             [ Action == 'update' && ]
                                             [ changes.draft: T -> F ]
                                                   /         \
                                                 Yes          No
                                                 /             \
                                          Post Alert            v
                                                  [ Action == 'open' && ]
                                                  [ draft == false      ]
                                                          /         \
                                                        Yes          No
                                                        /             \
                                                 Post Alert       Ignore 200

Every branch that ends in "Ignore 200" keeps noise out of your engineering channel. Only the two explicitly identified paths send a message to Slack.

Conclusion

State checks instead of transition checks guarantee noisy Slack channels. When developers get notifications for draft work or routine commits, they stop reading alerts, and review pickup times stretch across the organization.

You can write and host your own webhook proxy to parse changes.draft and post custom messages. If your team doesn't want to maintain bespoke webhook infrastructure, tokens, and alert parsers across GitLab projects, look at MergeMe. MergeMe provides unified Slack notifications for GitHub pull requests and GitLab merge requests as one updating card per PR/MR, and supports GitLab.com and self-hosted GitLab alongside GitHub. Connect your repositories and keep review requests visible when work is genuinely ready.

FAQs

How do I configure GitLab webhooks to trigger Slack notifications on draft status changes?

Navigate to Project Settings > Webhooks in GitLab. Enable the Merge request events trigger and enter your listener URL. Your receiver must inspect the webhook JSON payload specifically for changes.draft showing previous: true and current: false to alert Slack only when a draft is marked ready.

Why does GitLab send duplicate merge request notifications to Slack?

Delivery issues often arise when your webhook endpoint exceeds the configured webhook timeout—10 seconds by default—causing GitLab to treat the delivery as failed. Duplicates also occur when integrations alert on every payload update rather than filtering for transition changes. Deduplicate requests using the event_id field in the payload to identify repeated events.

Can the official GitLab Slack integration notify only when a draft merge request becomes ready?

The native GitLab integration triggers alerts for broader merge request events, which often includes regular updates and comments. To alert exclusively on the draft-to-ready transition, teams build a custom webhook handler checking changes.draft, while tools like MergeMe display merge requests as one updating card.

What is the difference between object_attributes.draft and object_attributes.work_in_progress?

Work in progress (WIP) was the legacy naming convention in earlier GitLab versions. Modern GitLab instances use draft under object_attributes.draft and changes.draft, following an API change that replaced the work_in_progress field.