How to Automate Gmail to Slack Notifications with n8n

By The LightCube Labs team

This guide covers real-time, per-message routing: every new Gmail message is checked, classified and posted to Slack within about a minute using the n8n Gmail Trigger. It is not a batch email digest that summarizes a day or an hour of mail into one post; if you want that, use the separate email digest workflow. Here each email gets its own Slack message in the channel that matches its Gmail label.

The Problem

Shared inboxes bury the messages that matter, and n8n can fix that. A client escalation sits next to a newsletter, a billing question waits until someone scrolls far enough, and the person on support never sees the thread that belongs to them. Forwarding rules in Gmail can only send mail to other inboxes, not to the Slack channels where the team actually works. Manual triage costs minutes per message and gets skipped on busy days. With n8n you can read the label, check the sender against a VIP list, and push a clean notification with a direct Gmail link to the right channel automatically.

What You Will Need

Step-by-Step Workflow Setup

1 Poll Gmail every minute with the Gmail Trigger

The workflow starts with the Gmail Trigger node. It polls the Gmail API on the schedule you pick and emits one n8n item for every new message that matches your filters. Polling every minute is the fastest cadence the trigger offers, which is what makes this n8n workflow feel real time.

Add a Gmail Trigger node and configure:

Node: Gmail Trigger
Poll Times: Every Minute
Event: Message Received
Simplify: Off
Filters:
  Label Names or IDs: INBOX
  Read Status: Unread
  Search: -label:notified
Credential: Gmail OAuth2 API

The Search filter uses normal Gmail search syntax. Excluding the notified label here is the first of two defenses against duplicate Slack posts; the second is the labeling step at the end of the workflow. Turn Simplify off so n8n returns the full message with labelIds, threadId and the parsed from object.

Sample:
{
  "id": "18c2f4a9d3b7e601",
  "threadId": "18c2f4a9d3b7e601",
  "labelIds": ["INBOX", "UNREAD", "Label_4821907"],
  "snippet": "Hi team, attached is the revised statement of work",
  "subject": "Revised SOW for Q4 rollout",
  "from": { "text": "Dana Ortiz <dana@acmecorp.com>",
            "value": [{ "address": "dana@acmecorp.com", "name": "Dana Ortiz" }] }
}

Custom Gmail labels appear as IDs such as Label_4821907, not as names. You will map those IDs to routes in the Code node, so copy them from the n8n Gmail node's label dropdown or from the Gmail API label list.

2 Store the VIP sender list in a Set node

VIP detection needs a list of addresses that always get the urgent channel. The simplest option is a Set node placed right after the trigger that holds the list as a field. A Google Sheets lookup works the same way if the list changes often, and is covered in the notes below.

Add a Set node named VIP List and configure:

Node: Set (Edit Fields)
Mode: Manual Mapping
Include Other Input Fields: On
Fields to Set:
  Name: vip
  Type: Array
  Value: ["ceo@bigclient.com","dana@acmecorp.com","legal@partnerfirm.io"]

Because Include Other Input Fields is on, the Gmail message passes through untouched and gains a vip array. If you prefer Google Sheets, replace this node with a Google Sheets node using Operation: Get Row(s) on a sheet with one address per row, then collect the column into an array inside the n8n Code node.

Sample:
{
  "id": "18c2f4a9d3b7e601",
  "subject": "Revised SOW for Q4 rollout",
  "vip": ["ceo@bigclient.com", "dana@acmecorp.com", "legal@partnerfirm.io"]
}

Keep addresses lower case. The Code node lower cases the sender before comparing, so a mixed case entry in the list would never match. Test the node with Execute Step so you can see the n8n output before moving on.

3 Extract the sender and build the Gmail deep link in a Code node

Next, a Code node reduces the raw Gmail payload to the handful of fields Slack needs, decides whether the sender is a VIP, picks a route, and builds a direct link to the message. Running the node in Run Once for Each Item mode keeps the logic simple. Name it Extract Sender and Route, because a later n8n expression refers to it by that name; the n8n Code node runs in a sandbox, so no extra packages are needed.

Node: Code
Language: JavaScript
Mode: Run Once for Each Item
const labelMap = {
  Label_4821907: 'clients',
  Label_4821908: 'billing',
  Label_4821909: 'support'
};
const sender = ($json.from.value[0].address || '').toLowerCase();
const isVip = $json.vip.map(v => v.toLowerCase()).includes(sender);
const hit = ($json.labelIds || []).find(id => labelMap[id]);
let route = hit ? labelMap[hit] : 'support';
if (isVip) route = 'vip';
return {
  json: {
    messageId: $json.id,
    sender,
    senderName: $json.from.value[0].name || sender,
    subject: $json.subject || '(no subject)',
    snippet: ($json.snippet || '').slice(0, 140),
    route,
    isVip,
    gmailLink: 'https://mail.google.com/mail/u/0/#inbox/' + $json.threadId
  }
};

The VIP check runs last on purpose. A message from a VIP that also carries the clients label should go to the urgent channel, not the clients channel, so the VIP assignment overwrites the label route.

Sample:
{
  "messageId": "18c2f4a9d3b7e601",
  "sender": "dana@acmecorp.com",
  "senderName": "Dana Ortiz",
  "subject": "Revised SOW for Q4 rollout",
  "snippet": "Hi team, attached is the revised statement of work",
  "route": "vip",
  "isVip": true,
  "gmailLink": "https://mail.google.com/mail/u/0/#inbox/18c2f4a9d3b7e601"
}

The deep link uses the thread ID because Gmail opens threads, not single messages. The u/0 segment assumes the first signed in Google account; change it to u/1 if your team uses a second account. n8n does not rewrite the URL for you.

4 Route by label with a Switch node

The n8n Switch node reads the route field and sends each item down exactly one of four outputs. This is where label based channel routing happens, and it is the main difference between this n8n workflow and a batch digest.

Node: Switch
Mode: Rules
Data Type: String
Value 1: {{ $json.route }}
Routing Rules:
  Output 0: Equal to  clients
  Output 1: Equal to  billing
  Output 2: Equal to  support
  Output 3: Equal to  vip
Options > Fallback Output: Extra Output

Rename each output so the canvas stays readable: slack-clients, slack-finance, slack-support and slack-urgent. The Fallback Output catches anything that slips through, for example a route value added to the Code node but not yet to the Switch.

Sample:
{
  "route": "billing",
  "matchedOutput": 1,
  "outputName": "slack-finance"
}

If you add a fifth label later, add the map entry in the Code node and a fifth rule here. Nothing else in the n8n workflow changes, and the Switch node keeps its own execution data for debugging.

5 Post to the mapped Slack channel

Each Switch output connects to its own Slack node, all configured the same way except for the channel. Using four nodes instead of one expression driven node keeps the channel visible on the n8n canvas and lets you give the urgent channel a different message style.

Add an n8n Slack node to the first output and configure:

Node: Slack
Resource: Message
Operation: Send
Send Message To: Channel
Channel: slack-clients
Message Type: Simple Text Message
Text: *{{ $json.senderName }}* ({{ $json.sender }})
Subject: {{ $json.subject }}
> {{ $json.snippet }}
<{{ $json.gmailLink }}|Open in Gmail>
Options > Include Link to Workflow: Off

For the urgent output, prefix the text with URGENT VIP:. Slack renders <url|label> as a clickable link, which is how the Gmail deep link becomes a one click action. Link the Slack credential once in n8n and reuse it on all four nodes.

Sample:
{
  "ok": true,
  "channel": "C07URGENT01",
  "ts": "1730736012.004200",
  "message": { "text": "*Dana Ortiz* (dana@acmecorp.com) ..." }
}

Subjects and snippets can contain ampersands or angle brackets, which Slack treats as control characters. If you see broken formatting, escape them in the n8n Code node before building the message text.

6 Add the notified label to prevent duplicates

After Slack succeeds, an n8n Gmail node tags the original message with a notified label. Connect all four Slack outputs to this one node; only one branch runs per item, so the label is added once.

Node: Gmail
Resource: Message
Operation: Add Label
Message ID: {{ $('Extract Sender and Route').item.json.messageId }}
Label Names or IDs: notified

Create the notified label in Gmail before running the n8n workflow. The expression pulls the ID from the Code node by name because the Slack node output replaces $json with its own response. This label, combined with the -label:notified search in step 1, means a message is processed one time even if the workflow restarts or the poll window overlaps.

Sample:
{
  "id": "18c2f4a9d3b7e601",
  "threadId": "18c2f4a9d3b7e601",
  "labelIds": ["INBOX", "UNREAD", "Label_4821907", "Label_5000112"]
}

7 Test with real messages and activate

Click Execute Workflow and send yourself four test emails, one for each label plus one from a VIP address. Confirm each lands in the right Slack channel, that the Gmail link opens the thread, and that the notified label appears in Gmail. Then toggle the workflow to Active so n8n begins polling every minute.

Watch the first hour of executions in the n8n execution list, and open any failed n8n run to see which node stopped. A healthy run shows one item per new message and no repeated message IDs.

Troubleshooting

n8n posts to Slack twice for the same email

This almost always means the notified label was never applied. Check that the label exists with that exact name, that the Gmail node uses Add Label and not Remove Label, and that the Search filter in the Gmail Trigger contains -label:notified. If the Slack node fails, the label node never runs, so fix the Slack error first in n8n by opening the failed execution.

Every message goes to the support channel in n8n

The Code node falls back to support when no label ID matches. Gmail returns custom labels as IDs such as Label_4821907. Open the n8n trigger output once, copy the real IDs from labelIds, and paste them into labelMap. Remember that the trigger only sees labels already applied by Gmail filters when the message arrives.

VIP senders never reach the urgent channel

Compare the lower cased address from from.value[0].address with the list. Hidden spaces, mixed case entries, or an alias address are the usual causes. If the sender uses a display name only in the text field, make sure Simplify is off so the structured value array exists.

Free n8n Workflow Resources

Get started with the free workflow pack on the LightCube Labs templates page.

Production-Ready Workflow Packs

Get this workflow and more, with setup guides, editable configurations, and a shared failure-alert system.

View the Starter ($29) and Pro ($99) Packs

Frequently Asked Questions

How is this different from the email digest workflow?

The digest n8n workflow collects many messages on a schedule and posts one summary. This n8n workflow runs per message, polls every minute, and sends each email to a channel chosen by its Gmail label. Use this one when response time matters and the digest when volume matters.

Can n8n keep the VIP list in Google Sheets instead of a Set node?

Yes. Add a Google Sheets node with Get Row(s) before the Code node, then collect the address column into an array. Sheets is the better choice when non technical teammates maintain the list, because changes apply on the next poll without editing the n8n workflow.

What happens in n8n when a message has several labels?

The n8n Code node picks the first label ID that exists in your map, then lets the VIP check overwrite it. If you need a different priority, reorder the logic or sort the label IDs before the find call.

Is polling every minute in n8n within Gmail API limits?

One poll per minute for a single mailbox is well inside normal Gmail API quotas. If you run many mailboxes on one Google Cloud project, watch the quota page and stagger the poll times in each n8n Gmail Trigger node.