Skip to content
Prose v0.3.2

User Onboarding

This example shows a user onboarding flow that validates email, creates an account, optionally sends an SMS, and publishes domain events.

import { createFlow, ValidationError } from '@celom/prose';
interface OnboardInput {
email: string;
name: string;
phone?: string;
}
const onboardUser = createFlow<OnboardInput>('onboard-user')
// 1. Validate email format
.validate('checkEmail', (ctx) => {
if (!ctx.input.email.includes('@'))
throw ValidationError.single('email', 'Invalid email address');
})
// 2. Check if user already exists — break early if so
.step('checkExisting', async (ctx) => {
const existing = await db.findByEmail(ctx.input.email);
return { existing };
})
.breakIf(
(ctx) => ctx.state.existing != null,
(ctx) => ({ user: ctx.state.existing, created: false })
)
// 3. Create the account (with retry for transient DB errors)
.step('createAccount', async (ctx) => {
const user = await db.createUser({
email: ctx.input.email,
name: ctx.input.name,
});
return { user };
})
.withRetry({ maxAttempts: 3, delayMs: 200, backoffMultiplier: 2 })
// 4. Send welcome email
.step('sendWelcome', async (ctx) => {
await mailer.send(ctx.state.user.email, {
template: 'welcome',
name: ctx.state.user.name,
});
})
// 5. Optionally send SMS if phone number provided
.stepIf(
'sendSms',
(ctx) => ctx.input.phone != null,
async (ctx) => {
await sms.send(ctx.input.phone!, `Welcome ${ctx.state.user.name}!`);
return { smsSent: true };
}
)
// 6. Publish domain event
.event('users', (ctx) => ({
eventType: 'user.onboarded',
userId: ctx.state.user.id,
email: ctx.input.email,
}))
// 7. Shape output
.map((input, state) => ({ user: state.user, created: true }))
.build();
const result = await onboardUser.execute(
{ email: 'alice@example.com', name: 'Alice', phone: '+1234567890' },
{ db, eventPublisher },
{
observer: {
onStepSkipped: (name) => console.log(`Skipped: ${name}`),
onFlowBreak: (name, step) => console.log(`Early exit at: ${step}`),
},
}
);
// result: { user: User; created: true } | { user: User; created: false }
  • Validation — email check runs first and is never retried
  • Early exitbreakIf returns the existing user without running creation steps
  • Conditional steps — SMS is only sent when a phone number is provided
  • Retries — account creation retries transient database errors
  • Events — domain event enriched with correlationId
  • Type-safe output — return type is a union reflecting both paths (new user vs. existing)