Customers always noticed the stockout first. By the time the buyer opened the sheet to check, orders had already stopped, and the rush reorder cost us margin.
The inventory data was already in Google Sheets. What was missing was someone to sweep it at the same time every day. We replaced that someone with a workflow. On a schedule, it reads the sheet, filters for SKUs below threshold, and reports them to Slack. Healthy stock is never touched, so there's no alert noise.
How it works
- A
Schedule Triggernode starts the workflow on a cron at 9:00 AM KST every day. - A
Google Sheets Readnode reads the entire sheet, schemaproduct_id | name | stock | reorder_threshold | supplier_email. - A
Codenode checksstock < reorder_thresholdfor each row and formats the low-stock list into a single Slack message. If no SKU is below threshold, it returns an empty array and the workflow quietly ends. - A
Slack Send Messagenode posts the low-stock summary to#inventory-alerts. Each line includesproduct_id, name, current stock / threshold, and supplier email.
What you need
- n8n 2.29.9 or later (self-hosted or Cloud). Verified on self-hosted Docker (
docker.n8n.io/n8nio/n8n). - Google Sheets OAuth2 API credential — registered in n8n Credentials as
Google Sheets OAuth2 API. Enable the Sheets API in a Google Cloud project, then create an OAuth 2.0 Client ID. - Slack Bot User OAuth Token — in the Slack app admin page, create an app, grant the
chat:writeandchannels:readscopes, install it in the workspace. Register the token (starts withxoxb-) in n8n Credentials asSlack API. - The inventory sheet — one sheet in Google Drive. Row 1 is the header (
product_id | name | stock | reorder_threshold | supplier_email); data starts on row 2.stockandreorder_thresholdmust be numbers (strings break the filter). - A Slack channel to receive the alerts. You must
/invite @your-bot-namein Slack or the post will fail.
Sheet schema (row 1 = header)
product_id | name | stock | reorder_threshold | supplier_email
SKU-001 | Premium beans 1kg | 8 | 20 | supplier1@example.com
SKU-002 | Tumbler 350ml | 45 | 15 | supplier2@example.com
SKU-003 | Drip bag set | 3 | 10 | supplier1@example.com
SKU-004 | Hand grinder | 22 | 5 | supplier3@example.com
SKU-005 | Filters 100-pack | 2 | 30 | supplier1@example.comSetup order (10 minutes)
- Google Sheets credential — In Google Cloud Console → APIs & Services → Library, enable
Google Sheets API. On the Credentials tab, create an OAuth 2.0 Client ID. In n8n → Credentials →Google Sheets OAuth2 API, paste the client_id/secret and Sign in with Google. - Slack credential — At https://api.slack.com/apps, create a new app → OAuth & Permissions → add
chat:writeandchannels:readto Bot Token Scopes → install to workspace → paste the Bot User OAuth Token (starts withxoxb-) into n8n → Credentials →Slack API. - Import the workflow — Save the JSON via the download button above, then n8n → Workflows → Import from File.
- Swap in the Sheet ID — In the
Read Inventory Sheetnode, replaceREPLACE_WITH_YOUR_SHEET_IDwith the string between/d/and/editin the sheet URL. - Swap in the channel name — In the
Post to #inventory-alertsnode, replaceREPLACE_WITH_YOUR_CHANNEL_NAME_OR_IDwith your channel name (e.g.inventory-alerts). Don't forget/invite @your-botin the Slack channel. - Wire up the credentials — In both nodes, pick the credential you just created from the Credential dropdown. Save, then flip the Activate toggle.
What happens in edge cases
- All stock above threshold — the Code node returns an empty array and the Slack node never fires. No noise.
- Empty sheet — same graceful exit. No error is raised, so it's safe during sheet migrations or when a new category is being staged.
- Text in `stock` or `reorder_threshold` — those rows are filtered out (via
Number.isFinite). Healthy rows still process, but consider adding sheet-side validation rules. - Slack bot not invited to the channel — the Slack API returns
not_in_channeland the workflow ends in error. Immediately visible in the n8n Executions tab.
We find out about stockouts before the customer does. It should be obvious — but it used to be the other way around.— Purchasing lead