Sending technically-sound Slack notifications is only half the challenge. The other half is designing notifications that engage users and produce desired outcomes. Follow these best practices to send Slack notifications that feel helpful rather than annoying.
Message design
Keep messages concise, scannable, and actionable. You should avoid walls of text. Use Block Kit for visual structure, but always include fallback text. Use headers to establish hierarchy, sections for content, and context blocks for metadata.
For example, the following message template produces a header for immediate recognition, a section with the key details, fields showing severity and timing, and a context block with the error ID. Users can scan it in just two seconds and understand precisely what happened:
// Good: Structured, scannable, actionable
const message = MessageTemplates.errorAlert({
title: "Database Connection Failed",
description: "Unable to connect to primary database",
severity: "High",
errorId: "ERR-2024-001",
});When possible, use buttons that allow users to complete desired actions without leaving Slack. Make primary actions visually prominent with the style: 'primary' attribute. Dangerous actions get style: 'danger'.
Context and relevance
Do your best to only send notifications that add clear value. Ask yourself if the message requires immediate attention or if it can wait for a digest? Does the recipient care?
When you can, group related messages. Instead of sending "Server 1 restarted," "Server 2 restarted," and "Server 3 restarted" as separate messages, batch them into a single message: "3 servers restarted: server-1, server-2, server-3." One notification conveys the same information with less noise.
For messages that will have updates, post a parent message for the main event, then add follow-ups as threaded replies. The channel will see one message, and users who want details can expand the thread:
// Parent message
const parent = await makeSlackRequest("chat.postMessage", {
channel: channelId,
text: 'Deployment started',
blocks: [...]
});
// Updates in thread
await makeSlackRequest("chat.postMessage", {
channel: channelId,
thread_ts: parent.ts,
text: 'Running tests...'
});Finally, make sure to always filter notifications through user preferences before sending:
const shouldSend = preferenceManager.shouldNotify(
userId,
workspace,
notification,
);
if (!shouldSend) {
console.log("Notification filtered by preferences");
return;
}Preference design patterns
When you implement a new event that will generate notifications, don't automatically enable it for all users. Instead, sending one announcement with the option to click and enable these notifications respects users’ preferences. Users who opt in are less likely to mute or complain.
Additionally, make sure to include an unsubscribe link in every notification. Even for critical alerts, provide a path to adjust preferences. Make it one click from the message to the settings:
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: '<https://app.example.com/preferences|Manage notification preferences>'
}
]
}Multi-channel notification strategy
Never cross-post the same notification to multiple channels automatically. If an error affects both the engineering and ops channels, pick the primary channel and let humans cross-post if needed. Duplicate notifications train users to ignore your app.
Use ephemeral messages for user-specific responses in shared channels. When someone runs a slash command or clicks a button, respond with an ephemeral message visible only to them. This prevents cluttering the channel with responses that don't benefit the whole team:
await makeSlackRequest("chat.postEphemeral", {
channel: channelId,
user: userId,
text: 'Settings updated successfully',
blocks: [...]
});Delivery reliability patterns
Implement a circuit breaker when Slack's API fails repeatedly. Hammering a failing API wastes resources and delays recovery. If you've seen three consecutive failures, stop sending for 60 seconds:
if (this.consecutiveFailures >= 3) {
const timeSinceLastFailure = Date.now() - this.lastFailureTime;
if (timeSinceLastFailure < 60000) {
throw new Error("Circuit breaker open, Slack API unavailable");
}
}Use priority queues for different notification severities. Critical alerts bypass the queue and send immediately, whereas low-priority notifications wait and batch. This ensures urgent messages get through even when you're hitting rate limits on less important traffic.
Threading strategy and limits
Don't hide critical information in threads. Users who aren't following a thread won't see replies, even in channels they're actively monitoring. For urgent updates that affect the whole team, use reply_broadcast: true to surface thread replies in the main channel:
await makeSlackRequest("chat.postMessage", {
channel: channelId,
thread_ts: parent.ts,
reply_broadcast: true, // Shows in main channel AND thread
text: "RESOLVED: Database connection restored",
});Start a new parent message when threads exceed 20-30 replies. Long threads become difficult to follow and don't surface in channel search effectively. For ongoing incidents, post hourly parent messages with the current status and thread the details.
Security operations
Handle token revocation gracefully. When a user uninstalls your app or revokes permissions, Slack sends a tokens_revoked event. Clean up stored tokens immediately and stop attempting notifications to that workspace:
app.event("tokens_revoked", async ({ event }) => {
await database.deleteTokens({
teamId: event.team_id,
userIds: event.tokens.oauth,
});
console.log("Tokens revoked and cleaned up");
});Additionally, you should implement notification audit trails for compliance. This includes storing who sent what notification, when, to whom, and whether it was delivered, using append-only logs that can't be modified. Then, when customers ask if you notified them, or if regulators request proof, you'll have answers.
Link strategy and optimization
Deep link to the exact content users need, not generic landing pages. If notifying about a specific comment, link directly to that comment with an anchor. If the page requires authentication, use a magic link or session token in the URL so users land exactly where they need to be:
const deepLink = `https://app.example.com/tickets/${ticketId}/comments/${commentId}?token=${sessionToken}`;For security-sensitive content, use expiring links. Generate short-lived tokens (valid for 1-4 hours) embedded in notification links. After expiration, users re-authenticate to view the content. This prevents old notifications from becoming permanent access backdoors.
Observability
You should log all notification attempts with structured data. This includes tracking sent count, failed count, retry count, rate limit hits, and average delivery latency:
console.log("Notification sent:", {
channel,
ts: result.ts,
dedupe_key,
timestamp: Date.now(),
});
console.error("Failed to send:", {
channel,
error: error.data?.error,
attempts: retryCount,
});Then, you should export metrics to your monitoring system, so you can build dashboards that display notification volume over time, failure rates by error type, and filtering statistics. Setting up alerts when failure rates exceed certain thresholds will help you debug any messaging issues that may arise.
All of the above practices ensure technically-functional notifications are also helpful and effective, so users keep your app installed.