Skip to content
n8nGoogle SheetsSlackTutorial

How to Connect Google Sheets to Slack with n8n (Free Workflow Included)

Three nodes, about fifteen minutes, and most of that is credentials. Here's the full build — including the JSON you can paste straight onto your canvas — plus the two mistakes that break it for almost everyone.

Ritik Makhija
13 min read

TL;DR

Use the Google Sheets Trigger node set to "On Row Added" to poll your sheet, an IF node to filter out rows you don't care about, and the Slack node to post a formatted message to a channel. That's the whole workflow — three nodes, roughly fifteen minutes, most of it spent on OAuth. The two things that break it are a missing header row in the sheet and a Slack bot that was never invited to the channel.

Key takeaways

  • The Google Sheets Trigger polls, it doesn't push — expect up to a minute of delay, and don't build anything time-critical on it.
  • Row 1 must be a header row. The trigger turns those headers into field names, and without them your expressions reference nothing.
  • Slack fails with `not_in_channel` until you invite the bot with `/invite @your-app`. This is the single most common failure on this build.
  • Add the IF node on day one — an alert that fires on every row gets muted within a week, and a muted channel is worse than no automation.
  • Self-hosted n8n runs this for the cost of the server; per-task pricing elsewhere is why this specific workflow is the classic first migration.

A spreadsheet is where most small processes actually live — inbound leads, order forms, content calendars, expense claims. The problem is nobody watches a spreadsheet. Work sits in row 47 for two days because the person who needed to see it had no reason to open the tab.

Pushing new rows into Slack fixes that, and it's the single most common thing people build first in n8n. This is the whole build: what to click, what breaks, and a workflow JSON at the bottom you can import and edit rather than rebuild from scratch.

What you're building

Three nodes, in a straight line:

  1. 1Google Sheets Trigger — watches a specific tab of a specific spreadsheet and emits an item every time a row is added.
  2. 2IF — drops the rows that don't deserve a notification, so the channel stays worth reading.
  3. 3Slack — posts a formatted message to a channel.

Everything past that — routing to different channels, threading replies, writing back to the sheet — is a variation on the same three nodes. Get this working first.

Before you start

  • An n8n instance — Cloud or self-hosted, either works. Self-hosted matters if you plan to run this thousands of times a month.
  • A Google account with the spreadsheet in it, and permission to create OAuth credentials (or an admin who'll do it once).
  • A Slack workspace where you can install an app. On many workspaces this needs admin approval, so start that request now rather than at step five.

Step 1 — Set up the sheet

This step decides whether the rest works, so don't skim it. Your first row must be a header row, and the headers become the field names you reference later. A sheet like this:

NameEmailCompanySourceStatus
Jane Okaforjane@acme.ioAcmeWebsite formNew
Tom Reyestom@northline.coNorthlineReferralNew

…gives you {{ $json.Name }}, {{ $json.Email }} and so on inside n8n. Two rules worth following: keep the headers simple (one word where you can, no leading spaces, no duplicates), and never insert a column in the middle once the workflow is live — add new columns at the end.

Step 2 — Connect Google Sheets

In n8n, add a Google Sheets Trigger node and create a new credential. n8n Cloud has a one-click OAuth option that handles this in about twenty seconds. On self-hosted you do it manually, and it's a five-minute detour:

  1. 1In Google Cloud Console, create a project (or reuse one) and enable both the Google Sheets API and the Google Drive API. The Drive API is not optional — without it the spreadsheet dropdown in n8n comes back empty.
  2. 2Under APIs & Services → OAuth consent screen, configure the app and add your own Google account as a test user.
  3. 3Under Credentials, create an OAuth client ID of type *Web application*, and paste n8n's OAuth Redirect URL (shown in the credential dialog) into *Authorised redirect URIs*.
  4. 4Copy the client ID and secret back into n8n and click Sign in with Google.

Note that the trigger uses its own credential type — *Google Sheets Trigger OAuth2* — separate from the regular Google Sheets node credential. If you already have one for the action node, you still create a second one here. This trips people up constantly.

Step 3 — Configure the trigger

With the credential connected, fill in four fields:

FieldSet it toWhy
Poll TimesEvery MinuteThe fastest the trigger runs. There is no push option — Google doesn't offer one for Sheets.
DocumentYour spreadsheet, from the listPicking from the dropdown confirms your OAuth scopes actually work.
SheetThe specific tabNot the file — the tab. A second tab needs a second trigger.
Trigger OnRow Added"Row Added or Updated" re-fires every time someone edits a cell. Rarely what you want.

Now click Fetch Test Event. You should see one row of your data come back as JSON. If you see an empty array, add a fresh row to the sheet and try again — the trigger only reports what's new since it last looked.

Step 4 — Filter before you notify

Add an IF node between the trigger and Slack. This is the step people skip, and it's the reason so many automation channels end up muted. A notification that arrives for everything carries no information.

Two conditions cover most cases: the row is in the state you care about, and the row is actually complete.

  • {{ $json.Status }} is equal to New — ignores rows added in other states.
  • {{ $json.Email }} is not empty — a half-typed row shouldn't page anyone.

Leave the false branch unconnected. In n8n, an unconnected branch simply ends the execution for those items — no error, no message.

Step 5 — Connect Slack

You have two options for the credential. OAuth2 is nicer for a shared workspace; an Access Token from your own Slack app is faster and easier to reason about. For a bot that posts to one channel, use the access token:

  1. 1Go to api.slack.com/apps and create a new app From scratch, pointed at your workspace.
  2. 2Under OAuth & Permissions, add the bot token scope chat:write. Add chat:write.public too if you want it to post without being invited anywhere, and files:write only if you'll upload files later.
  3. 3Click Install to Workspace and copy the Bot User OAuth Token — it starts with xoxb-.
  4. 4In n8n, create a Slack API credential and paste that token.
  5. 5In Slack, open the destination channel and run /invite @your-app-name.

Step 6 — Write a message worth reading

On the Slack node, set Resource to *Message*, Operation to *Send*, Send Message To to *Channel*, and pick the channel. Then build the text with expressions:

Slack node — Message Text
:zap: *New lead — {{ $json.Name }}*
*Company:* {{ $json.Company || 'Not given' }}
*Email:* {{ $json.Email }}
*Source:* {{ $json.Source || 'Unknown' }}

Slack's markup is its own dialect: single asterisks for bold, underscores for italic, and links in the form <https://example.com|label>. Standard markdown double asterisks will render literally, which looks broken.

The || 'Not given' fallbacks matter more than they look. An empty expression renders as an empty string and produces a message with a dangling label, which reads like a bug in your automation even when the data is simply missing.

One last thing on this node: under Options, turn off *Include Link to Workflow*. It's on by default and appends an n8n URL to every message, which nobody in a leads channel needs to see.

Step 7 — Test, then activate

Run the workflow manually first and add a row while it's listening. Check the message renders the way you expect in a test channel, not the real one.

Then flip the Active toggle. This is the step that catches everyone at least once: a manual test execution is not the live workflow. Until that toggle is on, the trigger isn't polling and nothing happens no matter how many rows you add.

The full workflow

Copy this, then in n8n use Workflows → Import from Clipboard. The credential IDs and document IDs are placeholders — after importing, open each node and re-select your own credential and spreadsheet from the dropdowns.

sheets-to-slack.json
{
  "name": "Google Sheets to Slack",
  "nodes": [
    {
      "parameters": {
        "pollTimes": { "item": [{ "mode": "everyMinute" }] },
        "documentId": { "__rl": true, "value": "YOUR_SPREADSHEET_ID", "mode": "id" },
        "sheetName": { "__rl": true, "value": "gid=0", "mode": "list", "cachedResultName": "Leads" },
        "event": "rowAdded",
        "options": {}
      },
      "id": "a1b2c3d4-sheets-trigger",
      "name": "New row in Leads",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "typeVersion": 1,
      "position": [-220, 0],
      "credentials": {
        "googleSheetsTriggerOAuth2Api": { "id": "REPLACE_ME", "name": "Google Sheets Trigger account" }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": false, "version": 2 },
          "conditions": [
            {
              "id": "status-is-new",
              "leftValue": "={{ $json.Status }}",
              "rightValue": "New",
              "operator": { "type": "string", "operation": "equals" }
            },
            {
              "id": "email-present",
              "leftValue": "={{ $json.Email }}",
              "rightValue": "",
              "operator": { "type": "string", "operation": "notEmpty", "singleValue": true }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b2c3d4e5-filter",
      "name": "Only new, complete rows",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [20, 0]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": { "__rl": true, "value": "C01234ABCDE", "mode": "id" },
        "text": "=:zap: *New lead — {{ $json.Name }}*\n*Company:* {{ $json.Company || 'Not given' }}\n*Email:* {{ $json.Email }}\n*Source:* {{ $json.Source || 'Unknown' }}",
        "otherOptions": { "includeLinkToWorkflow": false, "mrkdwn": true }
      },
      "id": "c3d4e5f6-slack",
      "name": "Post to #leads",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [260, -60],
      "credentials": { "slackApi": { "id": "REPLACE_ME", "name": "Slack account" } }
    }
  ],
  "connections": {
    "New row in Leads": {
      "main": [[{ "node": "Only new, complete rows", "type": "main", "index": 0 }]]
    },
    "Only new, complete rows": {
      "main": [[{ "node": "Post to #leads", "type": "main", "index": 0 }]]
    }
  },
  "settings": { "executionOrder": "v1" },
  "pinData": {}
}

What breaks, and why

SymptomCauseFix
not_in_channel from SlackBot was installed but never invitedRun /invite @your-app in the channel
Trigger never firesWorkflow isn't active, or row 1 isn't a header rowToggle Active; add proper headers
Message posts with blank fieldsHeader name doesn't match the expressionCheck spelling and use bracket syntax for headers with spaces
Duplicate notificationsTrigger set to "Row Added or Updated"Switch to "Row Added"
Ten messages at onceRows pasted in bulk arrive as one execution with many itemsAdd a Limit node, or aggregate into a single message
429 from GoogleToo many workflows polling on one credentialSheets allows 60 read requests/minute/user — stagger poll times

Worth doing next

  • Route by value. A Switch node after the IF sends deals over a threshold to a different channel, or @-mentions an owner.
  • Write back to the sheet. Add a Google Sheets *Update Row* node after Slack that stamps Notified into a column — now the sheet shows what's been actioned, and you have a rerun-safe record.
  • Thread the updates. Store the Slack message ts from the response, and post status changes as replies rather than new messages.
  • Add an error workflow. In workflow Settings → Error Workflow, point at a workflow that DMs you on failure. Silent automations fail silently.

One judgement call before you scale this up: it's a deterministic workflow, and it should stay one. There's a temptation to drop a model in to "summarise the lead" — for a five-field row, that's paying tokens to reformat text you already have. The line between automation and an actual agent is worth being deliberate about.

If you'd rather have this built properly — with error handling, retries and monitoring that tells you when it stops — that's what I do. Or send me the sheet and I'll tell you whether it's a fifteen-minute job or a real one.

Frequently asked questions

Add a Google Sheets Trigger node set to "On Row Added", connect it to an IF node that filters out rows you don't want to be notified about, and end with a Slack node using the Send Message operation. Connect a Google Sheets Trigger OAuth2 credential and a Slack API credential with the chat:write scope, invite the Slack bot to the destination channel, then activate the workflow.

About the author

Ritik Makhija

Ritik Makhija

Founder & Product Lead · AI Kaptan

I build AI agents and automation that run in production — and I've open-sourced 5,000+ workflows so you can read the work rather than take my word for it. I run outreach infrastructure sending 6,000 emails a day on this stack, and I've mentored 700+ builders 1:1.

Got a process you're trying to automate?

Tell me what it is and I'll say straight whether it needs an agent, a plain workflow, or nothing at all. Free 30 minutes, no pitch.

Send a message