Send Slack Alerts From a Google Sheet Cell with Custom Functions
Most spreadsheet-to-Slack tutorials show the same thing: an Apps Script function you run manually, or a trigger that fires daily. Both are useful, but neither is what spreadsheet-native people actually want. What they want is to type =NOTIFY_SLACK("#alerts", "Inventory low: " & A2) in a cell and have Slack get a message.
That pattern is possible, with caveats. This post is the working version - the right way to build it, the gotchas that Google's docs don't lead with, and the alternative pattern that you should usually use instead for anything that needs to be reliable.
The custom function version (formula-driven)
Apps Script lets you define custom functions that work like built-in spreadsheet functions. You write JavaScript, give it a name, and now =MY_FUNCTION(args) works in any cell of the sheet. Here's the Slack-posting version:
const SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/T000/B000/xxx';
/**
* Posts a message to a Slack channel from a cell.
* @param {string} channel Slack channel name, e.g. "#alerts"
* @param {string} message Message text
* @customfunction
*/
function NOTIFY_SLACK(channel, message) {
if (!channel || !message) return 'missing args';
const payload = { channel: channel, text: String(message) };
UrlFetchApp.fetch(SLACK_WEBHOOK_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
return 'sent ' + new Date().toLocaleTimeString();
}
Save the script. In a cell, type =NOTIFY_SLACK("#alerts", "Test"). Press Enter. A Slack message appears in #alerts. The cell returns the string "sent 14:32:11", so you can see when it fired.
Set up the Slack webhook URL in the Slack incoming webhooks page. Each webhook is bound to a single channel by default, but you can override it per request (as above) if your app is configured to allow it.
The huge gotcha nobody warns you about
Custom functions in Google Sheets cannot access services that require user authorization. Email, Drive, Calendar - all forbidden inside a custom function. The exceptions are limited: UrlFetchApp (which is what we use above) and a handful of utility services.
But there's a second, sneakier limitation: custom functions are recalculated automatically by Sheets, on its own schedule. Open the sheet, custom functions re-run. Edit a referenced cell, custom functions re-run. Sometimes Sheets just decides to re-run them.
If your formula is =NOTIFY_SLACK("#alerts", A2) and A2 changes once a day, you might think Slack gets one message per day. Wrong. Slack might get a message every time someone opens the sheet, every time A2 is recalculated as part of a larger formula chain, every time Sheets decides to refresh. We've watched a single cell formula fire 40 Slack messages in an afternoon. It is extremely embarrassing.
The right pattern: idempotent + last-sent tracking
The fix is to make the function deterministic about whether to actually fire. Use PropertiesService to remember the last message content per cell, and only fire if it's changed:
function NOTIFY_SLACK(channel, message, key) {
if (!key) return 'need unique key';
const props = PropertiesService.getDocumentProperties();
const lastSent = props.getProperty('slack_' + key);
const current = channel + '|' + message;
if (lastSent === current) return 'no change';
props.setProperty('slack_' + key, current);
UrlFetchApp.fetch(SLACK_WEBHOOK_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ channel: channel, text: String(message) })
});
return 'sent';
}
Now use it as =NOTIFY_SLACK("#alerts", "Inventory low: " & A2, "inv_a2"). The third argument is a unique key per logical alert. Repeated recalculations with the same channel+message combination are no-ops. Only a real change in the message text triggers a real Slack post.
This isn't perfect - in particular, two cells with the same key will fight - but it eliminates 99% of the duplicate-spam problem.
The trigger-based alternative (recommended for most cases)
Honestly, for anything important, skip the custom function entirely and use a time-based trigger that reads the sheet and posts alerts. The pattern looks like this:
function checkAndAlert() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Inventory');
const data = sheet.getRange('A2:C100').getValues();
const alerts = [];
data.forEach((row, i) => {
const [item, qty, threshold] = row;
if (item && qty < threshold) {
alerts.push(`• ${item}: ${qty} (threshold ${threshold})`);
}
});
if (alerts.length === 0) return;
const message = `*Low inventory:*\n` + alerts.join('\n');
UrlFetchApp.fetch(SLACK_WEBHOOK_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ text: message })
});
}
Schedule it to run every hour (or every 15 minutes) using a time-based trigger (see our triggers post). It's predictable, it batches alerts together so the channel doesn't get flooded, and it never fires from a sheet recalculation. For real production use, this is the right pattern about 90% of the time.
The cases where custom functions still win
| Use case | Cell formula | Time-based trigger | Why |
|---|---|---|---|
| Production low-inventory alerts | No | Yes | Predictable, batched, never fires on recalc |
| Daily / hourly summary digests | No | Yes | Trigger is the natural scheduler |
| Quick experiment / demo | Yes | Overkill | Type formula, see Slack message, done |
| Cell-as-trigger (user types a value) | Yes, with idempotency guard | Maybe | Requires PropertiesService key per cell |
| Pure computation (WORDCOUNT, slug, hash) | Yes - ideal use | No | No side effects, recalc is fine |
| One-off notification when something changes | No | Yes (or onEdit) | Idempotency in onEdit is much easier than in a custom function |
Where the =NOTIFY_SLACK() pattern is genuinely the right answer:
Quick experiments. You want to test "what if this triggered a Slack?" without setting up a trigger. Throw it in a cell, see the result, decide.
Demo or training contexts. Showing a colleague how something works, where the formula-as-action makes the demo feel magical.
Cells that are themselves the trigger. If you have a sheet where a user manually types a number into a specific cell and that should fire a notification, and you've tested that the custom function only fires on actual edits (not on every reopen), it can work. Use the idempotent pattern above to be safe.
Other custom functions worth building
The same pattern - JSDoc @customfunction tag, simple input → output, no side effects ideally - works for a bunch of useful things:
// Word count of a cell
function WORDCOUNT(text) {
if (!text) return 0;
return String(text).trim().split(/\s+/).length;
}
// Read time estimate (200 wpm)
function READTIME(text) {
if (!text) return '0 min';
const words = String(text).trim().split(/\s+/).length;
return Math.ceil(words / 200) + ' min';
}
// Slugify for URLs
function SLUGIFY(text) {
return String(text || '').toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
// HMAC-SHA256 hash (useful for signing or quick fingerprints)
function HMAC(text, key) {
const sig = Utilities.computeHmacSha256Signature(String(text), String(key));
return sig.map(b => (b & 0xff).toString(16).padStart(2, '0')).join('');
}
These are all pure functions - they have no side effects, just compute and return. That's the ideal shape for a custom function. The Slack-posting one violates that rule, which is why it's tricky; the four above are safe.
Quick limits to remember
- Custom functions have a 30-second execution timeout (much shorter than regular Apps Script's 6 minutes).
- Cannot access user-scoped services (Gmail, Drive personal data, Calendar).
- Cannot ask the user for authorization - they run with limited scopes only.
- Results are cached. Force a recalc with
Ctrl+Shift+F9(Windows) or insert a small change. - Custom function names must be ALL_CAPS to appear in autocomplete (technically not required, but conventional).
Summing up
Custom functions that post to Slack are technically possible, intuitively appealing, and a footgun if you don't handle the recalculation problem. The right path for most teams is:
- Use pure custom functions for computation (formatting, slugs, hashes, calculations).
- Use time-based triggers for alerts and notifications.
- Use the cell-firing custom function pattern only for demos, experiments, or with the idempotent guard.
Slack is a forgiving target for automation experiments because you can always mute the channel - but a flooded #alerts channel teaches the team to ignore alerts, which is the opposite of what you wanted. The boring trigger pattern is what gets used in real production.
Sources & Further Reading
- Apps Script: Custom functions in Google Sheets
- Slack Incoming Webhooks documentation
- PropertiesService reference
- Apps Script quotas and timeouts
Frequently asked questions
Can I really post to Slack from a Google Sheets cell formula?
Yes, technically - via a custom function with @customfunction JSDoc tag that calls UrlFetchApp.fetch to a Slack incoming webhook. Type =NOTIFY_SLACK("#alerts", "hi") and a Slack message appears. It works, but read the next answer before shipping it.
What's the huge gotcha with this pattern?
Custom functions are recalculated automatically by Sheets on its own schedule - on open, on referenced-cell change, sometimes for no apparent reason. We watched one cell formula fire 40 Slack messages in an afternoon. Without an idempotency guard, you will spam #alerts and the team will mute the channel.
How do I prevent duplicate Slack messages?
Add a third "key" argument and use PropertiesService to remember the last channel+message per key. If the current payload matches the last sent payload for that key, return early without fetching. Eliminates 99% of duplicate-spam at the cost of one extra argument per cell.
What should I use instead for production alerts?
A time-based trigger that reads the sheet on a schedule (hourly or every 15 minutes), aggregates conditions into one batched message, and posts to Slack. Predictable, never fires on recalculation, batches alerts so the channel doesn't flood. See our triggers guide. This is the right pattern about 90% of the time.
What other custom functions are worth building?
Pure functions that compute and return without side effects: WORDCOUNT, READTIME (200 wpm estimate), SLUGIFY (URL-safe slugs), HMAC (signing or fingerprint hashes). These are the ideal shape for custom functions - the Slack-posting one violates that rule, which is why it's tricky.
What are the hard limits on custom functions?
30-second execution timeout (vs 6 minutes for regular Apps Script), no access to user-scoped services (Gmail, Drive personal data, Calendar), cannot prompt for authorization, results are cached (force a recalc with Ctrl+Shift+F9). Names should be ALL_CAPS for autocomplete (conventional, not required).