The Problem
On Monday you tested the 3 prompts in ChatGPT. You saw how user profiling → adaptive paths → progress tracking works. But here's the reality: you can't manually customize onboarding for 1,000 new users per day. One-size-fits-all flows get 20% activation. Personalized flows get 60%. The difference? Automation that actually understands user context.
See It Work
Watch the 3 prompts chain together automatically. This is what you'll build to personalize onboarding at scale.
The Code
Three levels: start simple, add intelligence, then scale to production. Pick where you are and what you need.
Level 1: Simple Sequential Flow
Good for: 0-100 users/day | Setup time: 30 minutes
// Simple Sequential Onboarding (0-100 users/day) import Anthropic from '@anthropic-ai/sdk'; import { SegmentClient } from '@segment/analytics-node'; interface OnboardingResult { userProfile: any; recommendedPath: any; nextSteps: any[]; } const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, }); const segment = new SegmentClient({ writeKey: process.env.SEGMENT_WRITE_KEY!, }); async function automateOnboarding( userId: string, signupData: any, behaviorData: any ): Promise<OnboardingResult> { // Step 1: Profile user from signup + behavior const profilePrompt = `Analyze this user signup and behavior data to create a user profile. Signup data: ${JSON.stringify(signupData, null, 2)} Behavior data: ${JSON.stringify(behaviorData, null, 2)} Extract: - User persona (PM, developer, marketer, etc) - Company segment (enterprise, SMB, startup) - Intent signals (what they're trying to accomplish) - Technical level (beginner, intermediate, advanced) - Activation risk (low, medium, high) Output as JSON only.`; const profileResponse = await anthropic.messages.create({ model: 'claude-3-5-sonnet-20241022', max_tokens: 2048, messages: [{ role: 'user', content: profilePrompt }], }); const profileContent = profileResponse.content[0]; if (profileContent.type !== 'text') throw new Error('Invalid response'); const userProfile = JSON.parse(profileContent.text); // Step 2: Generate personalized onboarding path const pathPrompt = `Based on this user profile, generate a personalized onboarding flow. User profile: ${JSON.stringify(userProfile, null, 2)} Create 3-5 onboarding steps that: - Address their specific intent signals - Match their technical level - Skip irrelevant generic steps - Prioritize activation drivers For each step include: - title: Clear action-oriented title - description: Why this step matters to THEM - cta: Button text - estimated_time: How long it takes - priority: critical/high/medium/low Output as JSON array.`; const pathResponse = await anthropic.messages.create({ model: 'claude-3-5-sonnet-20241022', max_tokens: 2048, messages: [{ role: 'user', content: pathPrompt }], }); const pathContent = pathResponse.content[0]; if (pathContent.type !== 'text') throw new Error('Invalid response'); const recommendedPath = JSON.parse(pathContent.text); // Step 3: Track initial state in Segment segment.track({ userId, event: 'Onboarding Started', properties: { persona: userProfile.persona, activation_risk: userProfile.activation_risk, flow_type: recommendedPath.flow_name, total_steps: recommendedPath.steps?.length || 0, }, }); return { userProfile, recommendedPath, nextSteps: recommendedPath.steps || [], }; } // Usage const result = await automateOnboarding( 'usr_abc123', { email: 'sarah@techstartup.com', company: 'TechCorp', role: 'Product Manager', signup_source: 'organic_search', }, { viewed_features: ['gantt_charts', 'integrations'], viewed_pricing: true, integration_interest: 'asana', } ); console.log(`Generated ${result.nextSteps.length}-step personalized flow`);
Level 2: With Progress Tracking & Nudges
Good for: 100-1,000 users/day | Setup time: 2 hours
# With Progress Tracking & Automated Nudges (100-1000 users/day) import anthropic import segment.analytics as analytics from datetime import datetime, timedelta import redis import json analytics.write_key = os.getenv('SEGMENT_WRITE_KEY') client = anthropic.Anthropic(api_key=os.getenv('ANTHROPIC_API_KEY')) redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True) class OnboardingTracker: def __init__(self, user_id: str): self.user_id = user_id self.state_key = f'onboarding:{user_id}' def get_state(self) -> dict: """Get current onboarding state from Redis""" state_json = redis_client.get(self.state_key) if state_json: return json.loads(state_json) return { 'current_step': 0, 'completed_steps': [], 'started_at': datetime.now().isoformat(), 'last_activity': datetime.now().isoformat(), 'stuck_count': 0 } def update_state(self, updates: dict): """Update onboarding state""" state = self.get_state() state.update(updates) state['last_activity'] = datetime.now().isoformat() redis_client.setex( self.state_key, timedelta(days=30), json.dumps(state) ) return state def check_stuck(self) -> bool: """Detect if user is stuck on current step""" state = self.get_state() last_activity = datetime.fromisoformat(state['last_activity']) time_since_activity = datetime.now() - last_activity # Stuck if no progress in 5 minutes if time_since_activity > timedelta(minutes=5): state['stuck_count'] += 1 self.update_state(state) return True return False def generate_nudge(user_profile: dict, current_step: dict, stuck_reason: str) -> dict: """Generate personalized intervention message""" nudge_prompt = f"""Generate a helpful intervention message for a stuck user. User profile: {json.dumps(user_profile, indent=2)} Current step they're stuck on: {json.dumps(current_step, indent=2)} Stuck reason: {stuck_reason} Generate: - A short, encouraging in-app message (title + body + 2 CTAs) - An email follow-up (subject + preview + body) Be specific to their situation. Don't be generic. Use their first name. Reference what they were trying to do. Output as JSON with 'in_app' and 'email' keys.""" response = client.messages.create( model='claude-3-5-sonnet-20241022', max_tokens=2048, messages=[{'role': 'user', 'content': nudge_prompt}] ) return json.loads(response.content[0].text) def automate_onboarding_with_tracking(user_id: str, event_type: str, event_data: dict): """Handle onboarding events with progress tracking""" tracker = OnboardingTracker(user_id) state = tracker.get_state() if event_type == 'step_viewed': # User viewed a step - check if stuck view_count = state.get(f"step_{event_data['step_number']}_views", 0) + 1 tracker.update_state({ f"step_{event_data['step_number']}_views": view_count }) # If viewed same step 3+ times, they're stuck if view_count >= 3 and tracker.check_stuck(): # Generate personalized nudge user_profile = json.loads(redis_client.get(f'profile:{user_id}')) current_step = event_data['step_data'] nudge = generate_nudge( user_profile, current_step, f"Viewed step {view_count} times without completing" ) # Send in-app message immediately analytics.track( user_id=user_id, event='Nudge Sent', properties={ 'channel': 'in_app', 'trigger': 'stuck_on_step', 'step': event_data['step_number'] } ) # Schedule email for 5 minutes if still stuck # (Use Celery or similar for delayed tasks) return {'action': 'nudge_sent', 'nudge': nudge} elif event_type == 'step_completed': # User completed a step - celebrate and move forward completed_steps = state['completed_steps'] completed_steps.append(event_data['step_number']) tracker.update_state({ 'completed_steps': completed_steps, 'current_step': event_data['step_number'] + 1, 'stuck_count': 0 # Reset stuck counter }) analytics.track( user_id=user_id, event='Onboarding Step Completed', properties={ 'step': event_data['step_number'], 'total_completed': len(completed_steps), 'time_to_complete': event_data.get('time_spent') } ) return {'action': 'step_completed', 'next_step': event_data['step_number'] + 1} return {'action': 'tracked', 'state': state} # Usage result = automate_onboarding_with_tracking( user_id='usr_abc123', event_type='step_viewed', event_data={ 'step_number': 1, 'step_data': {'title': 'Import Asana Projects', 'action': 'connect_integration'} } ) if result['action'] == 'nudge_sent': print(f"Sent nudge: {result['nudge']['in_app']['title']}")
Level 3: Production Multi-Channel System
Good for: 1,000+ users/day | Setup time: 1 day
// Production Multi-Channel Onboarding System (1000+ users/day) import Anthropic from '@anthropic-ai/sdk'; import { SegmentClient } from '@segment/analytics-node'; import { createClient } from 'redis'; import { Queue, Worker } from 'bullmq'; import Mixpanel from 'mixpanel'; interface OnboardingState { userId: string; currentStep: number; completedSteps: number[]; startedAt: Date; lastActivity: Date; stuckCount: number; persona: string; activationRisk: string; flowType: string; } interface NudgeConfig { trigger: string; channel: 'in_app' | 'email' | 'sms' | 'push'; delay: number; conditions: any; } class OnboardingOrchestrator { private anthropic: Anthropic; private segment: SegmentClient; private redis: ReturnType<typeof createClient>; private nudgeQueue: Queue; private mixpanel: any; constructor() { this.anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, }); this.segment = new SegmentClient({ writeKey: process.env.SEGMENT_WRITE_KEY!, }); this.redis = createClient({ url: process.env.REDIS_URL, }); this.nudgeQueue = new Queue('onboarding-nudges', { connection: { host: 'localhost', port: 6379 }, }); this.mixpanel = Mixpanel.init(process.env.MIXPANEL_TOKEN!); } async initialize() { await this.redis.connect(); this.startNudgeWorker(); } async profileAndRoute(userId: string, signupData: any, behaviorData: any) { // Step 1: Generate user profile with caching const cacheKey = `profile:${userId}`; let profile = await this.redis.get(cacheKey); if (!profile) { const profileResponse = await this.anthropic.messages.create({ model: 'claude-3-5-sonnet-20241022', max_tokens: 2048, messages: [ { role: 'user', content: `Profile this user: ${JSON.stringify({ signupData, behaviorData })}`, }, ], }); const content = profileResponse.content[0]; if (content.type !== 'text') throw new Error('Invalid response'); profile = content.text; // Cache for 30 days await this.redis.setEx(cacheKey, 2592000, profile); } const userProfile = JSON.parse(profile); // Step 2: Select optimal onboarding flow const flowType = this.selectFlow(userProfile); const onboardingSteps = await this.generateSteps(userProfile, flowType); // Step 3: Initialize state const state: OnboardingState = { userId, currentStep: 0, completedSteps: [], startedAt: new Date(), lastActivity: new Date(), stuckCount: 0, persona: userProfile.persona, activationRisk: userProfile.activation_risk, flowType, }; await this.redis.setEx( `onboarding:${userId}`, 2592000, JSON.stringify(state) ); // Step 4: Track in multiple analytics platforms this.segment.track({ userId, event: 'Onboarding Started', properties: { persona: userProfile.persona, flow_type: flowType, activation_risk: userProfile.activation_risk, }, }); this.mixpanel.track('Onboarding Started', { distinct_id: userId, persona: userProfile.persona, flow_type: flowType, }); // Step 5: Schedule proactive nudges await this.scheduleNudges(userId, state, onboardingSteps); return { profile: userProfile, flow: onboardingSteps, state }; } private selectFlow(profile: any): string { // Rule-based flow selection with ML scoring const flowScores = { competitor_migration: 0, power_user_fast_track: 0, beginner_guided: 0, team_admin_setup: 0, }; // Score based on signals if (profile.integration_interest) flowScores.competitor_migration += 0.4; if (profile.technical_level === 'advanced') flowScores.power_user_fast_track += 0.3; if (profile.role === 'admin') flowScores.team_admin_setup += 0.3; if (profile.activation_risk === 'high') flowScores.beginner_guided += 0.5; // Return highest scoring flow return Object.entries(flowScores).sort((a, b) => b[1] - a[1])[0][0]; } private async generateSteps(profile: any, flowType: string) { const response = await this.anthropic.messages.create({ model: 'claude-3-5-sonnet-20241022', max_tokens: 2048, messages: [ { role: 'user', content: `Generate ${flowType} onboarding steps for: ${JSON.stringify(profile)}`, }, ], }); const content = response.content[0]; if (content.type !== 'text') throw new Error('Invalid response'); return JSON.parse(content.text); } private async scheduleNudges( userId: string, state: OnboardingState, steps: any[] ) { // Schedule nudges for each step const nudgeConfigs: NudgeConfig[] = [ { trigger: 'no_progress_5min', channel: 'in_app', delay: 300000, // 5 minutes conditions: { current_step: state.currentStep }, }, { trigger: 'no_progress_1hour', channel: 'email', delay: 3600000, // 1 hour conditions: { completion_rate: { $lt: 0.5 } }, }, { trigger: 'abandoned_24hours', channel: 'email', delay: 86400000, // 24 hours conditions: { completed_steps: { $eq: 0 } }, }, ]; for (const config of nudgeConfigs) { await this.nudgeQueue.add( config.trigger, { userId, state, steps, config }, { delay: config.delay } ); } } private startNudgeWorker() { const worker = new Worker( 'onboarding-nudges', async (job) => { const { userId, state, steps, config } = job.data; // Check if conditions still met const currentState = await this.getState(userId); if (!this.checkNudgeConditions(currentState, config.conditions)) { return { skipped: true, reason: 'conditions_not_met' }; } // Generate personalized nudge const nudge = await this.generateNudge( currentState, steps[currentState.currentStep], config.trigger ); // Send via appropriate channel await this.sendNudge(userId, config.channel, nudge); return { sent: true, channel: config.channel }; }, { connection: { host: 'localhost', port: 6379 } } ); } private async getState(userId: string): Promise<OnboardingState> { const stateJson = await this.redis.get(`onboarding:${userId}`); return stateJson ? JSON.parse(stateJson) : null; } private checkNudgeConditions(state: OnboardingState, conditions: any): boolean { // Implement condition checking logic if (conditions.current_step !== undefined) { return state.currentStep === conditions.current_step; } if (conditions.completion_rate) { const rate = state.completedSteps.length / 4; // Assuming 4 total steps return rate < conditions.completion_rate.$lt; } return true; } private async generateNudge( state: OnboardingState, currentStep: any, trigger: string ) { const response = await this.anthropic.messages.create({ model: 'claude-3-5-sonnet-20241022', max_tokens: 1024, messages: [ { role: 'user', content: `Generate nudge for trigger '${trigger}': ${JSON.stringify({ state, currentStep })}`, }, ], }); const content = response.content[0]; if (content.type !== 'text') throw new Error('Invalid response'); return JSON.parse(content.text); } private async sendNudge(userId: string, channel: string, nudge: any) { // Send via appropriate channel (Intercom, Customer.io, Twilio, etc) this.segment.track({ userId, event: 'Nudge Sent', properties: { channel, trigger: nudge.trigger }, }); } } // Usage const orchestrator = new OnboardingOrchestrator(); await orchestrator.initialize(); const result = await orchestrator.profileAndRoute( 'usr_abc123', { email: 'sarah@techstartup.com', role: 'Product Manager' }, { viewed_features: ['gantt_charts'], integration_interest: 'asana' } ); console.log(`Started ${result.flow.flow_type} with ${result.flow.steps.length} steps`);
When to Level Up
Start: Simple Sequential Flow
0-100 users/day
- Basic user profiling with LLM
- Static onboarding paths (3-4 pre-defined flows)
- Manual progress tracking in Segment
- Basic email follow-ups (scheduled sends)
Scale: Add Intelligence & Tracking
100-1,000 users/day
- Real-time progress tracking in Redis
- Automated stuck detection (view counts, time thresholds)
- Dynamic nudge generation based on user context
- Multi-channel interventions (in-app + email)
- A/B testing different onboarding paths
Production: Multi-Channel Orchestration
1,000-5,000 users/day
- Rule-based + ML flow selection
- Queue-based nudge scheduling (BullMQ)
- Multi-platform analytics (Segment + Mixpanel + Amplitude)
- Conditional routing (if stuck → intervention, if progressing → celebrate)
- Integration with product analytics for behavior signals
Enterprise: Predictive Activation System
5,000+ users/day
- ML-based activation prediction (LightGBM/XGBoost on behavior signals)
- Real-time personalization engine (feature flags, dynamic content)
- Multi-agent system (profiling agent, routing agent, intervention agent)
- Cross-platform state sync (web + mobile + email)
- Advanced experimentation platform (multi-armed bandits for flow optimization)
Product-Specific Gotchas
The code examples work. But product onboarding has unique challenges you need to handle.
Behavior Signal Lag in Product Analytics
Segment/Mixpanel can have 30-60 second delays. If you base nudges on stale data, you'll send interventions after users already progressed. Use Redis for real-time state, sync to analytics async.
// Real-time state in Redis, async sync to analytics class StateManager { async updateProgress(userId: string, step: number) { // 1. Update Redis immediately (< 10ms) await redis.set( `onboarding:${userId}:step`, step, { EX: 2592000 } // 30 days ); // 2. Queue analytics update (non-blocking) await analyticsQueue.add('track', { userId, event: 'Step Completed', properties: { step }, }); // 3. Check for nudge triggers on fresh data const state = await this.getRealtimeState(userId); if (this.shouldNudge(state)) { await this.sendNudge(userId, state); } } }
Mobile vs Web State Synchronization
Users start onboarding on web, continue on mobile (or vice versa). If state isn't synced across platforms, you'll show step 1 on mobile after they completed step 3 on web. Use shared state store (Redis/DynamoDB) with platform-agnostic keys.
# Platform-agnostic state management import redis import json class CrossPlatformState: def __init__(self, user_id: str): self.user_id = user_id self.redis = redis.Redis() self.state_key = f'onboarding:{user_id}' # No platform suffix def get_state(self, platform: str = None) -> dict: """Get state regardless of platform""" state_json = self.redis.get(self.state_key) if not state_json: return self.initialize_state() state = json.loads(state_json) # Add platform context for logging only state['last_platform'] = platform return state def update_state(self, updates: dict, platform: str): """Update from any platform""" state = self.get_state() state.update(updates) state['last_platform'] = platform state['last_updated'] = datetime.now().isoformat() # Atomic update with optimistic locking self.redis.set(self.state_key, json.dumps(state)) # Broadcast to other platforms via pub/sub self.redis.publish( f'onboarding:updates:{self.user_id}', json.dumps({'platform': platform, 'updates': updates}) ) # Usage across platforms state_manager = CrossPlatformState('usr_abc123') # Web completes step 3 state_manager.update_state({'current_step': 3}, platform='web') # Mobile immediately sees step 3 mobile_state = state_manager.get_state(platform='mobile') print(mobile_state['current_step']) # 3, not 0
Feature Flag Integration for Dynamic Flows
Onboarding flows change as you ship features. Hardcoded flows break when features are behind flags. Integrate with LaunchDarkly/Split.io to show steps only if user has access to that feature.
// Dynamic onboarding based on feature flags import { LDClient } from 'launchdarkly-node-server-sdk'; const ldClient = LDClient.init(process.env.LAUNCHDARKLY_SDK_KEY!); interface OnboardingStep { id: string; title: string; feature_flag?: string; fallback_step?: string; } async function filterStepsByFlags( userId: string, userContext: any, steps: OnboardingStep[] ): Promise<OnboardingStep[]> { const availableSteps: OnboardingStep[] = []; for (const step of steps) { if (!step.feature_flag) { // No flag = always show availableSteps.push(step); continue; } // Check if user has access to feature const hasAccess = await ldClient.variation( step.feature_flag, userContext, false // default = hidden ); if (hasAccess) { availableSteps.push(step); } else if (step.fallback_step) { // Show fallback if feature not available const fallback = steps.find(s => s.id === step.fallback_step); if (fallback) availableSteps.push(fallback); } } return availableSteps; } // Usage const allSteps = [ { id: 'import_data', title: 'Import Data' }, { id: 'setup_ai_assistant', title: 'Set Up AI Assistant', feature_flag: 'ai_assistant_enabled', fallback_step: 'manual_setup', }, { id: 'manual_setup', title: 'Manual Setup' }, ]; const userSteps = await filterStepsByFlags( 'usr_abc123', { key: 'usr_abc123', plan: 'enterprise' }, allSteps ); // Enterprise user sees AI assistant step // Free user sees manual setup instead
Timezone-Aware Nudge Scheduling
Sending email nudges at 3 AM user local time kills engagement. Store user timezone (from signup IP or explicit selection), schedule nudges for optimal hours (9 AM - 8 PM local time).
# Timezone-aware nudge scheduling from datetime import datetime, timedelta import pytz from timezonefinder import TimezoneFinder import geoip2.database class TimezoneAwareScheduler: def __init__(self): self.tf = TimezoneFinder() self.geoip = geoip2.database.Reader('GeoLite2-City.mmdb') def get_user_timezone(self, user_id: str, signup_ip: str) -> str: """Determine user timezone from IP or stored preference""" # Check if user set timezone explicitly stored_tz = redis.get(f'user:{user_id}:timezone') if stored_tz: return stored_tz.decode() # Fallback to IP geolocation try: response = self.geoip.city(signup_ip) lat, lng = response.location.latitude, response.location.longitude timezone = self.tf.timezone_at(lat=lat, lng=lng) # Cache for future use redis.set(f'user:{user_id}:timezone', timezone) return timezone except: return 'America/New_York' # Default fallback def schedule_nudge( self, user_id: str, signup_ip: str, nudge_type: str, delay_hours: int ): """Schedule nudge for optimal local time""" user_tz = self.get_user_timezone(user_id, signup_ip) user_time = datetime.now(pytz.timezone(user_tz)) # Calculate target time (delay_hours from now) target_time = user_time + timedelta(hours=delay_hours) # Adjust to business hours (9 AM - 8 PM) if target_time.hour < 9: target_time = target_time.replace(hour=9, minute=0) elif target_time.hour >= 20: # Push to next day 9 AM target_time = (target_time + timedelta(days=1)).replace(hour=9, minute=0) # Convert to UTC for queue target_utc = target_time.astimezone(pytz.UTC) delay_seconds = int((target_utc - datetime.now(pytz.UTC)).total_seconds()) # Schedule in queue nudge_queue.add( nudge_type, {'user_id': user_id}, delay=delay_seconds * 1000 # BullMQ uses milliseconds ) return target_time.isoformat() # Usage scheduler = TimezoneAwareScheduler() # Schedule "no progress" email for 1 hour from now # But adjust to user's business hours scheduled_time = scheduler.schedule_nudge( user_id='usr_abc123', signup_ip='203.0.113.42', nudge_type='no_progress_1hour', delay_hours=1 ) print(f"Nudge scheduled for {scheduled_time} user local time")
Preventing Nudge Fatigue with Frequency Caps
Sending 5 nudges in 1 hour annoys users. Implement frequency caps: max 2 in-app messages per hour, 1 email per day, 1 SMS per week. Track across all channels.
// Frequency cap enforcement import { Redis } from 'ioredis'; interface FrequencyCap { channel: string; max_count: number; window_seconds: number; } const FREQUENCY_CAPS: FrequencyCap[] = [ { channel: 'in_app', max_count: 2, window_seconds: 3600 }, // 2 per hour { channel: 'email', max_count: 1, window_seconds: 86400 }, // 1 per day { channel: 'sms', max_count: 1, window_seconds: 604800 }, // 1 per week { channel: 'push', max_count: 3, window_seconds: 86400 }, // 3 per day ]; class NudgeFrequencyManager { private redis: Redis; constructor() { this.redis = new Redis(process.env.REDIS_URL); } async canSendNudge( userId: string, channel: string ): Promise<{ allowed: boolean; reason?: string; retry_after?: number }> { const cap = FREQUENCY_CAPS.find((c) => c.channel === channel); if (!cap) return { allowed: true }; // No cap = always allow const key = `nudge_freq:${userId}:${channel}`; const count = await this.redis.get(key); if (!count) { // First nudge in window return { allowed: true }; } const currentCount = parseInt(count, 10); if (currentCount >= cap.max_count) { const ttl = await this.redis.ttl(key); return { allowed: false, reason: `Frequency cap reached: ${currentCount}/${cap.max_count} in ${cap.window_seconds}s`, retry_after: ttl, }; } return { allowed: true }; } async recordNudge(userId: string, channel: string): Promise<void> { const cap = FREQUENCY_CAPS.find((c) => c.channel === channel); if (!cap) return; const key = `nudge_freq:${userId}:${channel}`; const current = await this.redis.get(key); if (!current) { // First nudge - set with expiry await this.redis.setex(key, cap.window_seconds, '1'); } else { // Increment existing counter await this.redis.incr(key); } } async getNudgeHistory( userId: string, channel?: string ): Promise<{ channel: string; count: number; window_remaining: number }[]> { const pattern = channel ? `nudge_freq:${userId}:${channel}` : `nudge_freq:${userId}:*`; const keys = await this.redis.keys(pattern); const history = []; for (const key of keys) { const [, , , channelName] = key.split(':'); const count = parseInt((await this.redis.get(key)) || '0', 10); const ttl = await this.redis.ttl(key); history.push({ channel: channelName, count, window_remaining: ttl, }); } return history; } } // Usage const freqManager = new NudgeFrequencyManager(); // Before sending nudge const canSend = await freqManager.canSendNudge('usr_abc123', 'email'); if (canSend.allowed) { // Send the nudge await sendEmailNudge('usr_abc123', nudgeContent); // Record that we sent it await freqManager.recordNudge('usr_abc123', 'email'); } else { console.log(`Skipped nudge: ${canSend.reason}`); console.log(`Retry after ${canSend.retry_after} seconds`); } // Check user's nudge history const history = await freqManager.getNudgeHistory('usr_abc123'); console.log('Recent nudges:', history); // Output: [{ channel: 'email', count: 1, window_remaining: 82341 }]
Cost Calculator
Manual Onboarding Intervention
Limitations:
- • Can't scale beyond 100-200 users/day
- • Inconsistent experience (depends on who responds)
- • Slow response time (hours to days)
- • No data-driven optimization
Automated Onboarding Platform
Benefits:
- ✓ Scales to 10,000+ users/day with same infrastructure
- ✓ Consistent personalized experience for every user
- ✓ Real-time interventions (seconds vs hours)
- ✓ Data-driven continuous optimization
- ✓ 60% activation rate (vs 20% manual)