Blog / Product Brand Operations

Low-Stock Alerts from a Google Sheet (Email or Slack)

By Keyona Meeks, Founder, ReRev Labs · Oct 31, 2026 · 8 min read

The short answer: Keep one row per item with its current count and a reorder point. A short Google Apps Script runs each morning, finds items at or below their reorder point, and sends you one email, one Slack message, or both. It alerts once per dip. When you restock above the line, it resets, so the next dip alerts again.

By the end of this post, you'll be able to:

  • Set up an inventory sheet with the columns the alert needs.
  • Highlight low items in the sheet itself.
  • Install a script that sends one email or Slack message per low item.
  • Work out a first reorder point and know what to check before you trust it.

You find out you are out of a best seller when a customer asks for it. The count was in a sheet the whole time. Nobody was looking at it that day.

A sheet cannot tap you on the shoulder. A script can. This post builds the sheet and the script. It uses tools you already have, and it covers the part that fails most often: the alert nobody sees, or the alert that fires every day until you scream.

What columns does the inventory sheet need?

Make a tab called Inventory. Put these headers in row 1:

ColumnHeaderWhat goes in it
AItemThe name you use
BSKUYour code for it
COn handA number. The count right now
DReorder pointA number. The count at or below which you reorder
ESupplierWho you buy it from
FLast alertThe script fills this in. Leave it empty

Keep the header names exactly as written. The script finds columns by header text. On hand and Reorder point must be plain numbers, with no words or units in the cell.

Keeping On hand current is your job, or your other tools' job. The script does not count anything. It reads the number you give it.

How do you highlight low items in the sheet?

  1. Select the rows of data, for example A2:F500.
  2. Click Format, then Conditional formatting.
  3. Under Format cells if, choose Custom formula is.
  4. Enter =AND(ISNUMBER($C2),ISNUMBER($D2),$C2<=$D2).
  5. Pick a fill color and click Done.

Now any item at or below its reorder point turns that color. Google explains the steps in its conditional formatting help, checked October 30, 2026.

How do you pick a reorder point?

This is your call, and the sheet cannot make it for you. One common way to think about it is:

reorder point = (units you sell per day × days your supplier takes to deliver) + a safety cushion

If you sell 4 a day and a delivery takes 7 days, you use about 28 units while you wait. The cushion covers a slow delivery or a busy week. How big it is depends on how costly a stock-out is for you. Start with a number you can defend, then adjust it after your first few reorders. I do not give a default cushion, because I do not know your products.

How do you get alerts by email or Slack?

The script reads your sheet each morning. It sends one message listing the new low items. It then writes the date in Last alert for each, so the same item does not alert again tomorrow.

It uses Google Apps Script, which lives inside your sheet. If you have not used it before, read what Google Apps Script is first.

Add a Settings tab

Make a tab called Settings. Put the email address that should get alerts in B1. If you want Slack, keep the webhook URL out of the sheet. In the Apps Script editor, open Project Settings, scroll to Script properties, and add a property named SLACK_WEBHOOK_URL with your webhook URL as the value. Leave that property out to skip Slack. Leave B1 empty to skip email.

Get a Slack webhook (optional)

Slack's docs describe the steps. In short: create or open an app at api.slack.com/apps, turn on Incoming Webhooks, click Add New Webhook to Workspace, choose a channel and authorize. Slack then gives you a URL that starts with https://hooks.slack.com/services/. Slack's docs say the URL contains a secret, so keep it private. Source: Slack's incoming webhooks guide, checked October 30, 2026.

The webhook URL lives in the script's properties, not in the sheet, so nobody with a link to the sheet can read it.

Install the script

  1. In the sheet, click Extensions, then Apps Script.
  2. Delete what is in the editor and paste the code below.
  3. Click Save.
  4. In the function list, choose setupLowStockTrigger and click Run. Google asks you to allow the script. Read what it asks for and approve it if you agree.
// Low-stock alerts for a Google Sheet. Paste into Extensions > Apps Script.
const CONFIG = {
  SHEET: 'Inventory',
  SETTINGS_SHEET: 'Settings'   // B1 = email to alert. Slack URL: script property SLACK_WEBHOOK_URL (optional)
};

function checkLowStock() {
  const ss = SpreadsheetApp.getActive();
  const sheet = ss.getSheetByName(CONFIG.SHEET);
  const settings = ss.getSheetByName(CONFIG.SETTINGS_SHEET);
  const email = String(settings.getRange('B1').getValue()).trim();
  const slackUrl = PropertiesService.getScriptProperties().getProperty('SLACK_WEBHOOK_URL') || '';
  const data = sheet.getDataRange().getValues();
  const col = headerMap_(data[0]);
  const alerts = [];

  for (let r = 1; r < data.length; r++) {
    const row = data[r];
    const onHand = row[col['On hand']];
    const point = row[col['Reorder point']];
    if (typeof onHand !== 'number' || typeof point !== 'number') continue;
    const lastCell = sheet.getRange(r + 1, col['Last alert'] + 1);

    if (onHand > point) {
      // Back above the line. Clear the flag so the next dip alerts again.
      if (row[col['Last alert']] !== '') lastCell.setValue('');
      continue;
    }
    if (row[col['Last alert']] !== '') continue;   // already alerted for this dip

    alerts.push({
      item: row[col['Item']],
      sku: row[col['SKU']],
      onHand: onHand,
      point: point,
      supplier: String(row[col['Supplier']]).trim(),
      cell: lastCell
    });
  }
  if (alerts.length === 0) return;

  const text = alerts.map(function (a) {
    return a.item + ' (' + a.sku + '): ' + a.onHand + ' on hand, reorder at ' + a.point +
      (a.supplier ? ', supplier ' + a.supplier : '');
  }).join('\n');

  if (email) {
    MailApp.sendEmail({
      to: email,
      subject: 'Low stock: ' + alerts.length + ' item(s)',
      body: text
    });
  }
  if (slackUrl) {
    UrlFetchApp.fetch(slackUrl, {
      method: 'post',
      contentType: 'application/json',
      payload: JSON.stringify({ text: 'Low stock:\n' + text }),
      muteHttpExceptions: true
    });
  }
  const stamp = new Date();
  alerts.forEach(function (a) { a.cell.setValue(stamp); });
}

function setupLowStockTrigger() {
  ScriptApp.getProjectTriggers().forEach(function (t) { ScriptApp.deleteTrigger(t); });
  ScriptApp.newTrigger('checkLowStock').timeBased().everyDays(1).atHour(7).create();
}

function headerMap_(headers) {
  const map = {};
  headers.forEach(function (h, i) { map[String(h).trim()] = i; });
  return map;
}

Run setupLowStockTrigger once. It sets the check to run each morning. Google runs a time trigger at some point inside the hour you pick, not at the exact minute.

What the script does

  • It skips any row where On hand or Reorder point is not a number.
  • It treats an item as low when On hand is at or below Reorder point.
  • It sends one email, one Slack message, or both, with every new low item in a single list.
  • It writes the date in Last alert. While that cell has a date, the item does not alert again.
  • When On hand goes back above Reorder point, it clears Last alert. The next dip alerts again.

Test it before you trust it

I tested the script with made-up rows in a mock harness and a syntax check. I did not run it in a live Google account, and I did not send a real Slack message. Test it on a copy of your sheet:

  1. Add three items: one below its reorder point, one exactly at it, one above.
  2. Run checkLowStock by hand.
  3. Check that the first two appear in the message, the third does not, and Last alert has a date on the first two.
  4. Run it again. Nothing should send.
  5. Raise On hand on one low item above its reorder point and run it. The Last alert cell should clear.
  6. If you use Slack, check that the message lands in the channel you picked.

Where does an LLM fit in this?

Not in the alert. A count and a threshold are a rule, and plain code does it the same way every time. An LLM can help in one place: reading a messy supplier email or a photo of a packing slip and suggesting the counts to add. A person should check those numbers before they go into the sheet, because the whole system trusts that column.

Where this has limits

  • The sheet is only as right as the count. If nobody updates On hand, the alert is silent or wrong.
  • One daily check. An item can run out between checks. If you sell fast, run it more often, and watch the quotas.
  • It does not reorder. It tells you. Someone still places the order.
  • Email and fetch quotas apply. Apps Script limits how many emails a script can send and how many outside requests it can make each day. See Google's quotas page for the current numbers.
  • Slack is untested here. I followed Slack's documented request format, and I did not send a live message.
  • Many products or many locations get messy. A sheet suits a small catalog in one place. Software built for inventory is the right size when you track lots, batches or several warehouses.
  • One owner. If the script fails, alerts stop. Check Executions in Apps Script now and then, and name one person who owns it.

If your stock counts, orders and reorders live in sheets and memory, see our AI-powered operations work or apply on /letstalk. Bring the workflow that eats your week and a sample of how it runs today. Do not send passwords, supplier logins or webhook URLs.

To decide what to fix first, read what to automate first in a small business.

Frequently asked questions

Can Google Sheets send an alert when stock is low?

Yes, with a short Apps Script on a daily timer, as above. A sheet on its own can color a cell, but it cannot send a message.

Can I get the alert in Slack instead of email?

Yes. Add your Slack incoming webhook URL as a script property named SLACK_WEBHOOK_URL and leave B1 empty if you only want Slack. Keep the URL private.

Why did one item alert only once?

By design. The script alerts once per dip, so a low item does not fill your inbox every day. It resets when you restock above the reorder point.

What if I restock but forget to update the count?

Then the alert stays quiet, and the sheet is wrong. Updating On hand is the job that keeps this working.

How often can it run?

You can change the trigger in setupLowStockTrigger. More runs use more of your daily quotas, so check Google's quotas page first.

Can it work with Shopify or another store?

The sheet and the script do not read your store. You would have to bring the counts into the sheet some other way. That is a larger job, and a place to ask for help.

Read next: The reorder point formula sets the threshold these alerts fire on, and operations automation for product brands maps what to automate next once stock alerts are running.