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 MakhijaTL;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:
- 1Google Sheets Trigger — watches a specific tab of a specific spreadsheet and emits an item every time a row is added.
- 2IF — drops the rows that don't deserve a notification, so the channel stays worth reading.
- 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:
| Name | Company | Source | Status | |
|---|---|---|---|---|
| Jane Okafor | jane@acme.io | Acme | Website form | New |
| Tom Reyes | tom@northline.co | Northline | Referral | New |
…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:
- 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.
- 2Under APIs & Services → OAuth consent screen, configure the app and add your own Google account as a test user.
- 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*.
- 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:
| Field | Set it to | Why |
|---|---|---|
| Poll Times | Every Minute | The fastest the trigger runs. There is no push option — Google doesn't offer one for Sheets. |
| Document | Your spreadsheet, from the list | Picking from the dropdown confirms your OAuth scopes actually work. |
| Sheet | The specific tab | Not the file — the tab. A second tab needs a second trigger. |
| Trigger On | Row 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 toNew— 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:
- 1Go to api.slack.com/apps and create a new app From scratch, pointed at your workspace.
- 2Under OAuth & Permissions, add the bot token scope
chat:write. Addchat:write.publictoo if you want it to post without being invited anywhere, andfiles:writeonly if you'll upload files later. - 3Click Install to Workspace and copy the Bot User OAuth Token — it starts with
xoxb-. - 4In n8n, create a Slack API credential and paste that token.
- 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:
: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.
{
"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
| Symptom | Cause | Fix |
|---|---|---|
not_in_channel from Slack | Bot was installed but never invited | Run /invite @your-app in the channel |
| Trigger never fires | Workflow isn't active, or row 1 isn't a header row | Toggle Active; add proper headers |
| Message posts with blank fields | Header name doesn't match the expression | Check spelling and use bracket syntax for headers with spaces |
| Duplicate notifications | Trigger set to "Row Added or Updated" | Switch to "Row Added" |
| Ten messages at once | Rows pasted in bulk arrive as one execution with many items | Add a Limit node, or aggregate into a single message |
| 429 from Google | Too many workflows polling on one credential | Sheets 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
Notifiedinto a column — now the sheet shows what's been actioned, and you have a rerun-safe record. - Thread the updates. Store the Slack message
tsfrom 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.
Because the Slack bot is not a member of the channel it's posting to. Installing the app to your workspace does not join it to any channel. Open the channel in Slack and run /invite @your-app-name. Alternatively, add the chat:write.public scope, which lets the bot post to public channels without joining — private channels always require the invite.
Up to one minute. The trigger polls the Sheets API rather than receiving a push, and every minute is the fastest available poll interval. Google offers no real-time push for Sheets, so if you need instant delivery you need a different entry point — an Apps Script on-edit trigger calling an n8n Webhook node, or moving the data capture to a form that posts to a webhook directly.
No. It runs on self-hosted n8n, which is free under the fair-code license, and on n8n Cloud's entry tier. The relevant difference is pricing model rather than capability: n8n bills per workflow execution rather than per task, and self-hosting has no per-run cost at all, which is why this particular workflow is such a common migration from per-task tools.
Not with a single trigger — each Google Sheets Trigger node watches one tab of one spreadsheet. To cover several tabs, add one trigger node per tab in the same workflow and connect them all to the same downstream nodes. Use a Set node after each trigger to tag which sheet the item came from, so the Slack message can say so.
About the author

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.