Slack Birthday Reminders with Google Sheets: A Daily-Check Guide
Use a private Google Sheet, Apps Script clock trigger, and Slack incoming webhook to announce opted-in birthdays, with explicit date, duplicate, and failure handling.
By Kelasa team ·
A Google Sheet can hold a small team's birthday roster, but it will not send Slack reminders by itself. A practical DIY approach is a daily Google Apps Script check: read opted-in month/day values, then send one message through a Slack incoming webhook when somebody matches. The example below is a starting point to test in your own workspace, not a deployed or hands-on-verified integration. For a simpler human-managed option, see tracking employee birthdays in Slack.
Set up a private birthday sheet
Create a spreadsheet with a tab named Birthdays. Put these headers in row 1; format the Birthday (MM-DD) column as Plain text before entering values, so the sheet does not convert 03-14 into a full date.
| Name | Birthday (MM-DD) | Slack User ID | Opted In | | --- | --- | --- | --- | | Example Employee | 03-14 | U012ABCDEF0 | Yes | | Sample Colleague | 11-01 | | No |
These are fictitious examples. Only people who have agreed to a public birthday greeting should have Yes in the last column. A blank Slack ID produces a plain-name greeting; a valid ID produces a mention, which can notify the person. Ask whether they want to be mentioned as well as announced. You do not need a birth year for a yearly month/day comparison. Restrict spreadsheet access to people administering celebrations, remove departing employees, and let people opt out. Our no-bot reminder guide covers the lower-maintenance manual alternative.
Connect Apps Script to a Slack channel
In your Slack app's Incoming Webhooks settings, activate incoming webhooks, add a webhook to the desired workspace, and choose the destination channel. Slack's official setup guide describes that flow and its channel-specific webhook URL. This requires permission to install/configure an app; a webhook is not a built-in Slack /remind command. Do not paste the URL into a public Sheet cell, article, or repository. Store it as a SLACK_WEBHOOK_URL Script Property in the sheet's Extensions → Apps Script → Project Settings instead; Google documents that settings interface. Limit access to the script project too: Script Properties are shared with the script, not an encrypted secret vault.
In Apps Script, paste this illustrative function into the sheet-bound script. It expects the exact tab and columns above, one scheduled run per day, and a valid spreadsheet time zone. The script uses the sheet's time zone for the local day; check File → Settings → Time zone in Sheets before enabling it.
function postBirthdayDigest() {
const sheet = SpreadsheetApp.getActiveSpreadsheet();
const zone = sheet.getSpreadsheetTimeZone();
const today = Utilities.formatDate(new Date(), zone, 'MM-dd');
const localDate = Utilities.formatDate(new Date(), zone, 'yyyy-MM-dd');
const rows = sheet.getSheetByName('Birthdays').getDataRange().getDisplayValues().slice(1);
const people = rows.filter(row =>
row[3].trim().toLowerCase() === 'yes' && row[1].trim() === today
);
if (!people.length) return;
// Claim the day before posting: uncertain sends require manual reconciliation.
const lock = LockService.getScriptLock();
lock.waitLock(30000);
try {
const props = PropertiesService.getScriptProperties();
const key = 'birthday-attempt-' + localDate;
if (props.getProperty(key)) return;
const webhook = props.getProperty('SLACK_WEBHOOK_URL');
if (!webhook) throw new Error('Missing SLACK_WEBHOOK_URL script property');
props.setProperty(key, 'attempted');
const names = people.map(row => {
const id = row[2].trim();
if (/^[UW][A-Z0-9]+$/.test(id)) return '<@' + id + '>';
return row[0].trim().replace(/&/g, '&')
.replace(/</g, '<').replace(/>/g, '>');
});
const response = UrlFetchApp.fetch(webhook, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ text: 'Happy birthday to ' + names.join(', ') + '! 🎉' }),
muteHttpExceptions: true,
});
if (response.getResponseCode() !== 200) {
throw new Error('Slack webhook returned HTTP ' + response.getResponseCode());
}
props.setProperty(key, 'accepted');
} finally {
lock.releaseLock();
}
}
This does not guarantee exactly-once delivery. The script lock prevents simultaneous executions of this script from claiming the same day; the pre-send attempted marker prevents a blind retry if the network response is lost. It also means an error after the claim can leave that day's announcement unsent. Review the Apps Script execution log and the Slack channel before deciding whether to clear a marker and rerun; clearing it without checking may produce a duplicate. Google's LockService reference, Properties guide, and UrlFetchApp JSON POST example document the underlying APIs. Slack says the webhook's chosen channel cannot be overridden by a message payload, and webhook messages cannot be deleted through the webhook itself.
Schedule and check the workflow
In the Apps Script editor, open Triggers → Add Trigger, choose postBirthdayDigest, select a time-driven trigger, and choose a daily window. Authorize the script's spreadsheet and external-request permissions. Google's installable-trigger guide notes that daily trigger times can be randomized within the selected hour; do not promise an exact minute. Avoid adding a second trigger for the same function.
Before using real employee data, use a private test channel and fictitious entries. Test a matching day, a non-matching day, an opted-out row, a missing Slack ID, and a second run on the same day. Inspect the message in Slack, not just the script log. Test a non-200 webhook response and inspect the attempted marker; verify who receives mentions. These are recommended tests, not tests performed for this article.
Decide in advance what happens to February 29 birthdays in non-leap years (this example posts only on February 29) and whether weekend or holiday birthdays should be celebrated on the day itself (this example does so). Changing those policies requires code changes. A daily trigger and webhook also require somebody to maintain consent, access, and delivery failures; the Sheet is not automatically synced with Slack profiles.
If that upkeep is more than your team wants, compare this DIY flow with the automated birthday-message approaches and Kelasa BirthdayBot's daily digest overview. The landing page describes a workspace channel, time zone, and schedule; it does not promise importing a Google Sheet. Choose the approach that fits your consent and maintenance needs.
Sources and editorial limits
- Slack Developer Docs: sending messages using incoming webhooks
- Google Apps Script: installable and time-driven triggers
- Google Apps Script: Properties service
- Google Apps Script: UrlFetchApp
- Google Apps Script: LockService
This is documentation-backed sample code, not a verified live Slack integration or a downloadable template. No original screenshot, hands-on Slack test, or guarantee of exact-once delivery is claimed.