Onboarding Flows
Complete guide to implementing custom onboarding experiences and integrating them with ValkyrAI workflows.
Overview
The Valhalla Suite onboarding system provides:
- Signup Wizard: Multi-step user registration with customizable steps
- Workflow Integration: Automatic post-signup workflows (welcome email, data collection, etc.)
- Personalization: Step-specific configurations, conditional steps, custom branding
- Analytics: Track completion rates, drop-off points, user engagement
Architecture:
User Starts Signup
↓
OnboardingWizard Component
├─ Step 1: Account Creation
├─ Step 2: Profile Setup
├─ Step 3: Company Details
├─ Step 4: Preferences
└─ Step 5: Confirmation
↓
Account Created (Database)
↓
Post-Signup Workflows Triggered
├─ Send Welcome Email
├─ Initialize Default Workspace
├─ Create Starter Templates
└─ Schedule Onboarding Reminders
Frontend: Signup Wizard Component
Basic Setup
import React, { useState } from 'react';
import { OnboardingWizard } from '@valkyr/components/onboarding/OnboardingWizard';
import { useCreateUserMutation } from './backend/api/UserService';
export const SignupPage = () => {
const [createUser] = useCreateUserMutation();
const [completed, setCompleted] = useState(false);
const handleComplete = async (formData: SignupFormData) => {
try {
// Create user account
const user = await createUser({
email: formData.email,
password: formData.password,
firstName: formData.firstName,
lastName: formData.lastName,
company: formData.companyName,
}).unwrap();
setCompleted(true);
// Redirect after 2 seconds
setTimeout(() => {
window.location.href = '/dashboard';
}, 2000);
} catch (error) {
toast.error('Signup failed: ' + error.message);
}
};
return (
<div className="signup-container">
<OnboardingWizard
title="Welcome to Valkyr"
onComplete={handleComplete}
showProgress={true}
/>
</div>
);
};
Wizard Configuration
interface WizardStep {
id: string;
label: string;
title: string;
description: string;
fields: FormField[];
conditional?: (formData: any) => boolean; // Show step if true
optional?: boolean;
helpText?: string;
}
interface FormField {
name: string;
label: string;
type: 'text' | 'email' | 'password' | 'select' | 'checkbox' | 'textarea';
required?: boolean;
placeholder?: string;
options?: { label: string; value: string }[];
validation?: (value: any) => string | null; // Error message or null
helpText?: string;
}
// Example: Custom configuration
const customSteps: WizardStep[] = [
{
id: 'account',
label: 'Account',
title: 'Create Your Account',
description: 'Let\'s get started with your account setup',
fields: [
{
name: 'email',
label: 'Email Address',
type: 'email',
required: true,
placeholder: 'you@company.com',
validation: (value) => {
if (!value.includes('@')) return 'Invalid email';
return null;
},
},
{
name: 'password',
label: 'Password',
type: 'password',
required: true,
validation: (value) => {
if (value.length < 8) return 'Min 8 characters';
if (!/[A-Z]/.test(value)) return 'Need uppercase letter';
if (!/[0-9]/.test(value)) return 'Need number';
return null;
},
},
],
},
{
id: 'profile',
label: 'Profile',
title: 'Tell Us About Yourself',
fields: [
{
name: 'firstName',
label: 'First Name',
type: 'text',
required: true,
},
{
name: 'lastName',
label: 'Last Name',
type: 'text',
required: true,
},
],
},
{
id: 'company',
label: 'Company',
title: 'Company Information',
conditional: (data) => data.accountType === 'business', // Only for business
fields: [
{
name: 'companyName',
label: 'Company Name',
type: 'text',
required: true,
},
{
name: 'industry',
label: 'Industry',
type: 'select',
required: true,
options: [
{ label: 'Technology', value: 'tech' },
{ label: 'Finance', value: 'finance' },
{ label: 'Healthcare', value: 'healthcare' },
// ...
],
},
],
},
];
// Use custom steps
<OnboardingWizard steps={customSteps} onComplete={handleComplete} />
Error Handling & Validation
const handleStepError = (error: any) => {
// Show field-level errors
if (error.fieldErrors) {
Object.entries(error.fieldErrors).forEach(([field, message]) => {
setFieldError(field, message);
});
}
// Show general error
if (error.message) {
toast.error(error.message);
}
// Prevent step advance
return false;
};
const validateStep = async (stepId: string, formData: any) => {
try {
// Call backend validation
const validation = await validateSignupStep(stepId, formData);
if (!validation.isValid) {
setFieldErrors(validation.errors);
return false;
}
return true;
} catch (error) {
return handleStepError(error);
}
};
<OnboardingWizard
steps={customSteps}
onStepChange={validateStep}
onComplete={handleComplete}
/>
Backend: Post-Signup Workflows
Setup Workflow Triggers
@Service
public class OnboardingService {
@Autowired
private WorkflowService workflowService;
@Autowired
private UserRepository userRepo;
/**
* Triggered after user completes signup
*/
@Transactional
@EventListener(condition = "#event.eventType == 'user.registered'")
public void onUserRegistered(UserRegisteredEvent event) {
User user = event.getUser();
logger.info("User registered: {}", user.getEmail());
// Queue post-signup workflows
executePostSignupWorkflows(user);
}
private void executePostSignupWorkflows(User user) {
// Workflow 1: Send welcome email
triggerWorkflow("welcome-email", user);
// Workflow 2: Initialize workspace
triggerWorkflow("init-workspace", user);
// Workflow 3: Send onboarding sequence
triggerWorkflow("onboarding-sequence", user);
}
private void triggerWorkflow(String workflowId, User user) {
Workflow workflow = workflowService.getWorkflow(workflowId);
// Set initial state with user info
Map<String, Object> initialState = Map.of(
"userId", user.getId(),
"userEmail", user.getEmail(),
"userName", user.getFirstName() + " " + user.getLastName(),
"companyId", user.getCompany().getId()
);
// Execute workflow
workflowService.executeWorkflow(workflow, initialState);
}
}
Welcome Email Workflow Example
workflows:
- id: welcome-email
name: Welcome Email
description: Send welcome email to new users
tasks:
# Task 1: Prepare email content
- id: prepare-email
moduleId: transformModule
inputMapping:
userName: workflow.state.userName
userEmail: workflow.state.userEmail
outputMapping:
emailBody: workflow.state.emailContent
# Task 2: Send email
- id: send-email
moduleId: emailModule
inputMapping:
to: workflow.state.userEmail
subject: "Welcome to Valkyr!"
body: workflow.state.emailContent
html: true
outputMapping:
emailId: workflow.state.sentEmailId
status: workflow.state.emailStatus
# Task 3: Update user profile
- id: update-profile
moduleId: updateUserModule
inputMapping:
userId: workflow.state.userId
onboardingStep: "welcome_sent"
welcomeEmailSentAt: workflow.state.timestamp
outputMapping:
success: workflow.state.profileUpdated
ExecModule: Update User Onboarding Status
@Component("updateUserModule")
@VModuleAware
public class UpdateUserModule extends VModule {
@Autowired
private UserRepository userRepo;
@Override
public Map<String, Object> execute(
Workflow workflow,
Task task,
ExecModule module,
Map<String, Object> input
) {
try {
UUID userId = UUID.fromString((String) input.get("userId"));
String onboardingStep = (String) input.get("onboardingStep");
Instant timestamp = Instant.now();
// Get user
User user = userRepo.findById(userId)
.orElseThrow(() -> new NotFoundException("User not found"));
// Update onboarding status
user.setOnboardingStep(onboardingStep);
user.setOnboardingUpdatedAt(timestamp);
// If all steps complete, mark as onboarded
if ("completed".equals(onboardingStep)) {
user.setOnboardingCompletedAt(timestamp);
}
User updated = userRepo.save(user);
return Map.of(
"status", "success",
"userId", updated.getId(),
"onboardingStep", updated.getOnboardingStep()
);
} catch (Exception e) {
logger.error("Update user failed", e);
return Map.of("status", "error", "error", e.getMessage());
}
}
}
Onboarding Checklist Workflow
Implementation Example
workflows:
- id: onboarding-checklist
name: Onboarding Checklist
description: Track user onboarding completion
tasks:
- id: get-checklist
moduleId: getChecklistModule
inputMapping:
userId: workflow.state.userId
outputMapping:
items: workflow.state.checklistItems
completed: workflow.state.completedCount
total: workflow.state.totalCount
- id: check-completion
moduleId: conditionalModule
inputMapping:
completed: workflow.state.completedCount
total: workflow.state.totalCount
outputMapping:
isComplete: workflow.state.onboardingComplete
- id: send-reminder
moduleId: emailModule
conditional:
# Only send if not complete
path: workflow.state.onboardingComplete
value: false
inputMapping:
to: workflow.state.userEmail
subject: "Complete Your Onboarding"
Checklist Items
public enum OnboardingChecklistItem {
COMPLETE_PROFILE("Complete Profile", "Add photo and bio", 1),
CONNECT_CALENDAR("Connect Calendar", "Integrate with Outlook/Google", 2),
INVITE_TEAM("Invite Team", "Add team members", 3),
SETUP_INTEGRATIONS("Setup Integrations", "Connect CRM or tools", 4),
CREATE_FIRST_WORKFLOW("Create First Workflow", "Build automation", 5),
COMPLETE_TRAINING("Complete Training", "Watch onboarding videos", 6);
public final String title;
public final String description;
public final int order;
OnboardingChecklistItem(String title, String description, int order) {
this.title = title;
this.description = description;
this.order = order;
}
}
Analytics: Track Onboarding Metrics
Tracking Implementation
@Service
public class OnboardingAnalyticsService {
@Autowired
private OnboardingEventRepository eventRepo;
/**
* Track step progression
*/
public void recordStepCompleted(UUID userId, String stepId) {
OnboardingEvent event = new OnboardingEvent();
event.setUserId(userId);
event.setEventType(OnboardingEventType.STEP_COMPLETED);
event.setStepId(stepId);
event.setTimestamp(Instant.now());
eventRepo.save(event);
}
/**
* Track step abandonment
*/
public void recordStepAbandoned(UUID userId, String stepId) {
OnboardingEvent event = new OnboardingEvent();
event.setUserId(userId);
event.setEventType(OnboardingEventType.STEP_ABANDONED);
event.setStepId(stepId);
event.setTimestamp(Instant.now());
eventRepo.save(event);
}
/**
* Track onboarding completion
*/
public void recordOnboardingCompleted(UUID userId) {
OnboardingEvent event = new OnboardingEvent();
event.setUserId(userId);
event.setEventType(OnboardingEventType.COMPLETED);
event.setTimestamp(Instant.now());
eventRepo.save(event);
}
/**
* Get completion rate
*/
@Query("""
SELECT COUNT(DISTINCT oe.userId) as completedUsers,
COUNT(DISTINCT u.id) as totalUsers,
(COUNT(DISTINCT oe.userId) * 100.0 / COUNT(DISTINCT u.id)) as completionRate
FROM OnboardingEvent oe
RIGHT JOIN User u ON oe.userId = u.id
WHERE oe.eventType = 'COMPLETED'
AND u.createdAt >= :startDate
""")
OnboardingMetrics getCompletionRate(@Param("startDate") Instant startDate);
}
Metrics Endpoint
@RestController
@RequestMapping("/v1/analytics/onboarding")
@PreAuthorize("hasRole('ADMIN')")
public class OnboardingAnalyticsController {
@Autowired
private OnboardingAnalyticsService analyticsService;
@GetMapping("/metrics")
public ResponseEntity<OnboardingMetrics> getMetrics(
@RequestParam(required = false) String period // day, week, month
) {
Instant startDate = getStartDate(period);
OnboardingMetrics metrics = analyticsService.getCompletionRate(startDate);
return ResponseEntity.ok(metrics);
}
@GetMapping("/completion-by-step")
public ResponseEntity<List<StepMetrics>> getStepMetrics() {
List<StepMetrics> metrics = analyticsService.getMetricsByStep();
return ResponseEntity.ok(metrics);
}
@GetMapping("/funnel")
public ResponseEntity<OnboardingFunnel> getFunnel() {
OnboardingFunnel funnel = analyticsService.buildFunnel();
return ResponseEntity.ok(funnel);
}
}
Funnel Visualization
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
export const OnboardingFunnel = ({ funnel }: { funnel: OnboardingFunnel }) => {
const data = [
{ step: 'Signup', users: funnel.started },
{ step: 'Account', users: funnel.accountCompleted },
{ step: 'Profile', users: funnel.profileCompleted },
{ step: 'Company', users: funnel.companyCompleted },
{ step: 'Preferences', users: funnel.preferencesCompleted },
{ step: 'Completed', users: funnel.completed },
];
return (
<div className="funnel-chart">
<h2>Onboarding Funnel</h2>
<AreaChart width={600} height={400} data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="step" />
<YAxis />
<Tooltip />
<Area
type="monotone"
dataKey="users"
stroke="#8884d8"
fill="#8884d8"
animationDuration={1000}
/>
</AreaChart>
</div>
);
};
Personalization: Conditional Steps
Dynamic Step Logic
// Example: Show company details only for business accounts
const getStepsForUser = (accountType: string): WizardStep[] => {
const baseSteps = [accountStep, profileStep];
if (accountType === "business") {
baseSteps.push(companyStep, integrationStep);
}
return baseSteps;
};
// Example: Update wizard steps based on previous choices
const handleAccountTypeChange = (type: string) => {
const updatedSteps = getStepsForUser(type);
setWizardSteps(updatedSteps);
};
Conditional Workflows
workflows:
- id: post-signup
name: Post-Signup Workflow
description: Route based on user type
tasks:
# Branch: Personal account vs Business
- id: route-by-type
moduleId: branchingModule
inputMapping:
accountType: workflow.state.accountType
outputMapping:
# Output determines which branch executes next
branch: workflow.state.routedBranch
# Personal account path
- id: personal-welcome
moduleId: emailModule
conditional:
path: workflow.state.accountType
value: personal
inputMapping:
to: workflow.state.userEmail
template: "welcome-personal"
# Business account path
- id: business-setup
moduleId: companySetupModule
conditional:
path: workflow.state.accountType
value: business
inputMapping:
companyId: workflow.state.companyId
adminUserId: workflow.state.userId
Error Handling & Recovery
Session Persistence
// Save form progress to localStorage
const saveProgress = (currentStep: number, formData: any) => {
const progress = {
step: currentStep,
data: formData,
savedAt: new Date().toISOString(),
};
localStorage.setItem("onboarding_progress", JSON.stringify(progress));
};
// Recover from saved progress
const getProgress = () => {
const saved = localStorage.getItem("onboarding_progress");
return saved ? JSON.parse(saved) : null;
};
// Resume onboarding from where user left off
const { data: currentProgress } = useEffect(() => {
const saved = getProgress();
if (saved) {
setCurrentStep(saved.step);
setFormData(saved.data);
}
}, []);
Timeout & Recovery
@Service
public class OnboardingTimeoutService {
@Scheduled(fixedRate = 3600000) // Check hourly
public void cleanupExpiredSessions() {
Instant oneHourAgo = Instant.now().minus(1, ChronoUnit.HOURS);
List<OnboardingSession> expired = sessionRepo
.findByUpdatedAtBefore(oneHourAgo);
expired.forEach(session -> {
logger.info("Expiring session: {}", session.getId());
sessionRepo.delete(session);
});
}
@Transactional
public void recordSessionTimeout(UUID userId) {
OnboardingEvent event = new OnboardingEvent();
event.setUserId(userId);
event.setEventType(OnboardingEventType.SESSION_TIMEOUT);
event.setTimestamp(Instant.now());
eventRepo.save(event);
// Send reminder email
sendTimeoutReminder(userId);
}
}
Deployment Checklist
- Onboarding wizard component tested with all step types
- Form validation working for all field types
- Backend endpoints validated and secured
- Post-signup workflows created and tested
- ExecModules for onboarding tasks working
- Analytics tracking enabled
- Email templates for onboarding created
- Error pages for failed signup displayed
- Session recovery implemented
- Mobile-responsive wizard UI verified
- A/B testing framework set up
- Performance optimized (< 3s page load)
✅ ONBOARDING INTEGRATION COMPLETE