Notification Personalization: Segment Users for Better Engagement
How to move beyond "Dear Customer" and create notifications users actually want to receive
Generic notifications are ignored. According to Epsilon research, 80% of consumers are more likely to engage with personalized messages. Meanwhile, Campaign Monitor data shows personalized email subject lines increase open rates by 26%.
This guide covers practical personalization strategies you can implement today, from basic variable substitution to advanced behavioral segmentation.
What you will learn:
- Implement dynamic template variables
- Build user segments based on behavior
- Create personalized notification workflows
- Test and optimize personalization strategies
- Handle edge cases and fallbacks
The Personalization Spectrum
Level 1: Basic Variable Substitution
The simplest form of personalization:
// Before: Generic
const message = "Your order has shipped!"
// After: Personalized
const message = `Hi ${user.firstName}, your order #${order.id} has shipped!`Impact: 10-15% improvement in engagement
Level 2: Content Personalization
Tailor the actual content based on user context:
const getShippingMessage = (user: User, order: Order) => {
const estimate = getDeliveryEstimate(order, user.address)
const trackingUrl = `https://track.example.com/${order.trackingNumber}`
return {
subject: `${user.firstName}, your ${order.items[0].name} is on the way!`,
body: `
Great news! Your order is shipping to ${user.address.city}.
Estimated delivery: ${estimate.date}
${estimate.isExpress ? '🚀 Express shipping - arriving faster!' : ''}
Track your package: ${trackingUrl}
`
}
}Impact: 25-40% improvement in engagement
Level 3: Behavioral Personalization
Adjust messaging based on user behavior patterns:
const getReEngagementMessage = (user: User) => {
const daysSinceLastVisit = getDaysSince(user.lastActiveAt)
const favoriteCategory = user.mostViewedCategory
if (daysSinceLastVisit > 30) {
return {
subject: `We miss you, ${user.firstName}!`,
body: `It's been a while. Check out what's new in ${favoriteCategory}.`,
incentive: '20% off your next order'
}
} else if (daysSinceLastVisit > 14) {
return {
subject: `New arrivals in ${favoriteCategory}`,
body: `Based on your interests, we think you'll love these.`,
incentive: null
}
}
return null // Don't send to active users
}Impact: 50-100% improvement in engagement
User Segmentation Strategies
Behavioral Segments
Segment users based on their actions:
| Segment | Definition | Notification Strategy |
|---|---|---|
| Power Users | 10+ sessions/month | Feature announcements, beta access |
| Regular Users | 2-9 sessions/month | Engagement content, tips |
| At-Risk | No activity 14+ days | Re-engagement, incentives |
| New Users | Signed up < 7 days | Onboarding, tutorials |
| Churned | No activity 60+ days | Win-back campaigns |
Implementation with NotiGrid
import { NotiGrid } from '@notigrid/node'
const notigrid = new NotiGrid({ apiKey: process.env.NOTIGRID_API_KEY })
// Define segments
const segments = {
powerUsers: (user: User) => user.sessionsThisMonth >= 10,
atRisk: (user: User) => daysSince(user.lastActiveAt) >= 14,
newUsers: (user: User) => daysSince(user.createdAt) <= 7,
}
// Send segmented notification
async function sendFeatureAnnouncement(feature: Feature) {
const powerUsers = await db.users.findMany({
where: { sessionsThisMonth: { gte: 10 } }
})
for (const user of powerUsers) {
await notigrid.notify({
channel: 'feature-announcement',
recipient: { userId: user.id, email: user.email },
variables: {
user_name: user.firstName,
feature_name: feature.name,
feature_description: feature.description,
beta_access_url: `https://app.example.com/beta/${feature.slug}`,
}
})
}
}RFM Segmentation
Segment by Recency, Frequency, and Monetary value:
interface RFMScore {
recency: 1 | 2 | 3 | 4 | 5 // Days since last purchase
frequency: 1 | 2 | 3 | 4 | 5 // Number of purchases
monetary: 1 | 2 | 3 | 4 | 5 // Total spend
}
function calculateRFM(user: User): RFMScore {
const daysSinceLastPurchase = getDaysSince(user.lastPurchaseAt)
const purchaseCount = user.totalOrders
const totalSpend = user.lifetimeValue
return {
recency: scoreRecency(daysSinceLastPurchase),
frequency: scoreFrequency(purchaseCount),
monetary: scoreMonetary(totalSpend),
}
}
function getSegmentFromRFM(rfm: RFMScore): string {
const score = rfm.recency + rfm.frequency + rfm.monetary
if (score >= 12) return 'champions' // Best customers
if (score >= 9) return 'loyal' // Regular buyers
if (score >= 6) return 'potential' // Could be more engaged
if (rfm.recency <= 2) return 'at_risk' // Recently inactive
return 'hibernating' // Need reactivation
}Dynamic Template Strategies
Conditional Content Blocks
Show different content based on user attributes:
Hi {{user_name}},
{{#if is_premium_user}}
As a Premium member, you get early access to our new collection!
{{else}}
Check out our new collection - Premium members got first access yesterday.
{{/if}}
{{#if has_items_in_cart}}
Don't forget - you have {{cart_item_count}} items waiting in your cart.
{{/if}}
{{#if preferred_language == "es"}}
¿Prefieres leer esto en español? [Cambiar idioma]
{{/if}}Product Recommendations
Include personalized recommendations:
async function getPersonalizedRecommendations(user: User) {
const viewedCategories = user.recentlyViewedCategories
const purchasedProducts = user.purchasedProductIds
// Get products from viewed categories, excluding purchased
const recommendations = await db.products.findMany({
where: {
categoryId: { in: viewedCategories },
id: { notIn: purchasedProducts },
},
orderBy: { popularity: 'desc' },
take: 3,
})
return recommendations.map(p => ({
name: p.name,
price: formatCurrency(p.price, user.currency),
image_url: p.imageUrl,
product_url: `https://shop.example.com/p/${p.slug}`,
}))
}
// Use in notification
await notigrid.notify({
channel: 'weekly-recommendations',
recipient: { userId: user.id, email: user.email },
variables: {
user_name: user.firstName,
recommendations: await getPersonalizedRecommendations(user),
}
})Time-Based Personalization
Adjust content based on time context:
function getTimeBasedGreeting(user: User): string {
const userTime = getUserLocalTime(user.timezone)
const hour = userTime.getHours()
if (hour < 12) return `Good morning, ${user.firstName}`
if (hour < 17) return `Good afternoon, ${user.firstName}`
return `Good evening, ${user.firstName}`
}
function getUrgencyMessage(deadline: Date, user: User): string {
const userTime = getUserLocalTime(user.timezone)
const hoursUntilDeadline = differenceInHours(deadline, userTime)
if (hoursUntilDeadline <= 2) return '⚡ Ending very soon!'
if (hoursUntilDeadline <= 24) return `⏰ Only ${hoursUntilDeadline} hours left`
if (hoursUntilDeadline <= 72) return `Ends in ${Math.ceil(hoursUntilDeadline / 24)} days`
return ''
}Channel Personalization
Preferred Channel Selection
Respect user channel preferences:
interface UserPreferences {
preferredChannel: 'email' | 'sms' | 'push' | 'slack'
emailEnabled: boolean
smsEnabled: boolean
pushEnabled: boolean
quietHoursStart: number // 0-23
quietHoursEnd: number
}
async function sendWithPreferences(
user: User,
notification: Notification
) {
const prefs = await getUserPreferences(user.id)
// Respect quiet hours for non-critical notifications
if (!notification.critical && isQuietHours(prefs, user.timezone)) {
await scheduleForLater(user, notification, prefs.quietHoursEnd)
return
}
// Use preferred channel if enabled
const channel = selectChannel(prefs, notification.priority)
await notigrid.notify({
channel: `${notification.type}-${channel}`,
recipient: { userId: user.id },
variables: notification.variables,
})
}
function selectChannel(
prefs: UserPreferences,
priority: 'critical' | 'high' | 'normal' | 'low'
): string {
// Critical notifications use SMS if available
if (priority === 'critical' && prefs.smsEnabled) {
return 'sms'
}
// Otherwise use preferred channel
if (prefs.preferredChannel === 'email' && prefs.emailEnabled) {
return 'email'
}
if (prefs.preferredChannel === 'sms' && prefs.smsEnabled) {
return 'sms'
}
if (prefs.preferredChannel === 'push' && prefs.pushEnabled) {
return 'push'
}
// Fallback to email
return 'email'
}Multi-Channel Orchestration
Send across channels based on engagement:
async function sendWithEscalation(user: User, notification: Notification) {
// Start with preferred channel
const result = await notigrid.notify({
channel: `${notification.type}-${user.preferredChannel}`,
recipient: { userId: user.id },
variables: notification.variables,
})
// If high priority and not opened within 1 hour, escalate
if (notification.priority === 'high') {
await scheduleEscalation({
notificationId: result.id,
userId: user.id,
escalateAfterMinutes: 60,
escalateToChannel: 'sms',
})
}
}Handling Edge Cases
Missing Data Fallbacks
Always have fallbacks for missing personalization data:
function buildNotificationVariables(user: User, order: Order) {
return {
// Use fallback for missing name
user_name: user.firstName || user.email.split('@')[0] || 'there',
// Format with fallback currency
order_total: formatCurrency(
order.total,
user.currency || order.currency || 'USD'
),
// Default to first item if multiple
product_name: order.items[0]?.name || 'your order',
// Safe navigation for optional fields
loyalty_points: user.loyaltyPoints?.toLocaleString() || '0',
// Timezone-safe date formatting
delivery_date: formatDate(
order.estimatedDelivery,
user.timezone || 'UTC',
user.locale || 'en-US'
),
}
}Localization
Support multiple languages:
const translations = {
en: {
orderConfirmation: {
subject: 'Order {{order_id}} confirmed!',
greeting: 'Hi {{user_name}},',
body: 'Thank you for your order.',
}
},
es: {
orderConfirmation: {
subject: '¡Pedido {{order_id}} confirmado!',
greeting: 'Hola {{user_name}},',
body: 'Gracias por tu pedido.',
}
},
fr: {
orderConfirmation: {
subject: 'Commande {{order_id}} confirmée !',
greeting: 'Bonjour {{user_name}},',
body: 'Merci pour votre commande.',
}
}
}
function getLocalizedTemplate(
templateKey: string,
locale: string
): NotificationTemplate {
const lang = locale.split('-')[0] // 'en-US' -> 'en'
return translations[lang]?.[templateKey] || translations.en[templateKey]
}Measuring Personalization Impact
Key Metrics to Track
| Metric | Baseline (Generic) | Target (Personalized) |
|---|---|---|
| Email Open Rate | 15-20% | 25-35% |
| Click-Through Rate | 2-3% | 5-8% |
| Conversion Rate | 1-2% | 3-5% |
| Unsubscribe Rate | 0.5% | < 0.2% |
| SMS Response Rate | 10% | 20-30% |
A/B Testing Framework
async function sendWithABTest(
users: User[],
notification: Notification,
variants: NotificationVariant[]
) {
// Randomly assign users to variants
const assignments = users.map(user => ({
user,
variant: variants[Math.floor(Math.random() * variants.length)],
}))
// Send notifications
for (const { user, variant } of assignments) {
await notigrid.notify({
channel: notification.channel,
recipient: { userId: user.id },
variables: {
...notification.variables,
...variant.variables,
},
metadata: {
abTestId: notification.testId,
variantId: variant.id,
}
})
}
// Track results after 24 hours
await scheduleAnalysis(notification.testId, 24 * 60 * 60 * 1000)
}Getting Started with NotiGrid
NotiGrid makes personalization easy with:
- Dynamic templates with Handlebars syntax
- User segments defined in code or dashboard
- Channel preferences respected automatically
- A/B testing built into the platform
- Analytics to measure personalization impact
import { NotiGrid } from '@notigrid/node'
const notigrid = new NotiGrid({ apiKey: process.env.NOTIGRID_API_KEY })
// Personalized notification with all the features
await notigrid.notify({
channel: 'order-confirmation',
recipient: {
userId: user.id,
email: user.email,
locale: user.locale,
timezone: user.timezone,
},
variables: {
user_name: user.firstName,
order_id: order.id,
order_total: formatCurrency(order.total, user.currency),
items: order.items.map(formatItem),
delivery_estimate: order.estimatedDelivery,
recommendations: await getRecommendations(user),
},
preferences: {
respectQuietHours: true,
preferredChannel: user.preferredChannel,
}
})Start personalizing your notifications today with a free NotiGrid account.
Key Takeaways
- Start simple - Basic name personalization still beats generic messages
- Segment thoughtfully - 3-5 well-defined segments beat 20 poorly defined ones
- Test everything - A/B test personalization strategies with proper sample sizes
- Have fallbacks - Always handle missing data gracefully
- Respect preferences - Personalization includes letting users control their experience
- Measure impact - Track engagement metrics to prove ROI
Personalized notifications respect your users' attention while delivering more value. The investment in personalization infrastructure pays dividends in engagement, retention, and customer satisfaction.
Ready to send your first notification?
Get started with NotiGrid today and send notifications across email, SMS, Slack, and more.