AI & Development
How to use AI to personalise app onboarding based on user goals, context, and behaviour - improving activation rates without building complex rule-based branching logic.
The first 60 seconds in an app determine whether a user becomes active or churns. Most apps waste that minute showing every user the same generic sequence of screens - feature highlights, permission requests, a tutorial nobody finishes. AI onboarding takes a different approach: it asks a small number of high-signal questions up front, uses those answers to understand what this specific user wants to accomplish, and routes them to the feature and content most likely to give them their first moment of value. The result is a first-use experience that feels like the app was built for them, not for a generic user.
AI in onboarding is not one thing - it is three distinct capabilities, each with different implementation requirements:
Most apps benefit most from goal interpretation - the other two can be implemented with simpler mechanisms once you have a clean goal signal.
The highest-signal onboarding question is the open goal field: "What do you want to use [app name] for?" It sounds simple, but free-form text input captures intent that multiple-choice questions cannot - the user's exact words, their level of specificity, and the emotional framing they bring to the task.
The practical implementation: show one open text field after account creation or app install, with a placeholder that models good responses ("e.g., improve my focus at work, prepare for an exam, keep my mind sharp as I get older"). Cap the response at 200 characters. This 30-second step produces a signal that drives personalisation for the entire first week.
Once you have the freeform goal, classify it into your app's goal taxonomy:
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
type UserGoal = 'focus' | 'exam_prep' | 'cognitive_aging' | 'general_training' | 'curiosity';
async function classifyUserGoal(rawGoal: string): Promise<UserGoal> {
const response = await client.messages.create({
model: 'claude-haiku-4-5-20251001', // Fast enough for onboarding
max_tokens: 50,
system: `Classify user goals into exactly one category. Return only the category name, nothing else.
Categories:
- focus: wants to improve concentration, attention, focus at work/study
- exam_prep: preparing for a specific test, exam, or certification
- cognitive_aging: concerned about memory or mental sharpness as they get older
- general_training: general brain training, keeping mind active
- curiosity: just exploring, no specific goal mentioned`,
messages: [{
role: 'user',
content: `User goal: "${rawGoal.slice(0, 200)}"`,
}],
});
const classification = response.content[0].text.trim() as UserGoal;
const validGoals: UserGoal[] = ['focus', 'exam_prep', 'cognitive_aging', 'general_training', 'curiosity'];
return validGoals.includes(classification) ? classification : 'general_training';
}
The strict output constraint ("Return only the category name, nothing else") with a validation fallback is important - Haiku is reliable for simple classification, but the fallback prevents any edge-case output from crashing the onboarding flow.
Once the goal is classified, generate onboarding copy that speaks to it directly. This single call changes the feel of the entire first-use experience from generic to targeted:
async function generateWelcomeCopy(
goal: UserGoal,
rawGoal: string,
userName: string
): Promise<{ headline: string; subheadline: string; ctaLabel: string }> {
const response = await client.messages.create({
model: 'claude-haiku-4-5-20251001',
max_tokens: 200,
system: `You write short UI copy for a mobile app's welcome screen.
Return ONLY valid JSON: {"headline": "...", "subheadline": "...", "ctaLabel": "..."}
headline: max 40 chars, speaks directly to the user's goal
subheadline: max 80 chars, one specific benefit
ctaLabel: max 20 chars, action verb + what happens`,
messages: [{
role: 'user',
content: `User: ${userName}
Goal category: ${goal}
Their exact words: "${rawGoal}"
Write welcome screen copy for this user.`,
}],
});
return JSON.parse(response.content[0].text.trim());
}
The difference between "Welcome to AppName. Start your journey." and "Your focus starts here, Alex. 15 minutes a day builds the attention span distraction has eroded." is not cosmetic - it is the difference between a user who feels understood and one who feels like another data point.
Goal classification drives feature routing. Map each goal to the feature or content path most likely to produce the user's first meaningful moment of value:
const goalRoutes: Record<UserGoal, OnboardingRoute> = {
focus: {
firstFeature: 'attention_training',
tutorialId: 'focus-intro',
suggestedDifficulty: 'beginner',
initialContent: 'focus-puzzle-set',
},
exam_prep: {
firstFeature: 'timed_practice',
tutorialId: 'timed-intro',
suggestedDifficulty: 'intermediate',
initialContent: 'speed-challenge-set',
},
cognitive_aging: {
firstFeature: 'memory_training',
tutorialId: 'memory-intro',
suggestedDifficulty: 'beginner',
initialContent: 'memory-starter-set',
},
general_training: {
firstFeature: 'daily_challenge',
tutorialId: 'general-intro',
suggestedDifficulty: 'beginner',
initialContent: 'mixed-starter-set',
},
curiosity: {
firstFeature: 'explore',
tutorialId: 'explore-intro',
suggestedDifficulty: 'beginner',
initialContent: 'variety-starter-set',
},
};
Rule-based routing after classification is the right approach here - it is deterministic, easily auditable, and easy to adjust without retraining a model. The AI's job is to understand the user's intent; your product's job is to know what features best serve each intent.
The metrics that tell you whether personalised onboarding is working:
The test worth running before full commitment: A/B test the personalised onboarding against your existing flow with a 50/50 split for two weeks. If personalised onboarding users show meaningfully better D7 retention, the LLM cost per new user is almost certainly worth it - at Haiku pricing, the cost is under $0.001 per onboarded user.