Personas
Personas define weighted attribute distributions for synthetic users. When generation runs, each user gets a concrete set of attributes sampled from the persona's distributions. Attributes are stored in the user's context and persist across all journeys.
Defining a Persona
Use definePersona with an attributes map. Each attribute can be a static value, a field generator (weighted, oneOf, fake, chance), or a function receiving the context.
import { definePersona, weighted, oneOf, fake, chance } from '@synode/core';
const ecommerceUser = definePersona({
id: 'ecommerce-user',
name: 'E-Commerce User',
attributes: {
locale: weighted({ en: 0.5, de: 0.25, fr: 0.15, ja: 0.1 }),
deviceType: weighted({ mobile: 0.6, desktop: 0.3, tablet: 0.1 }),
customerTier: weighted({ free: 0.7, premium: 0.2, enterprise: 0.1 }),
isReturning: chance(0.4),
preferredCategory: oneOf(['electronics', 'clothing', 'home', 'sports']),
displayName: fake((faker) => faker.person.fullName()),
email: fake((faker) => faker.internet.email()),
},
});Field Generators
weighted<T>(options: Record<T, number>)
Returns a value sampled by weight. Weights are normalized automatically -- they do not need to sum to 1.
weighted({ mobile: 60, desktop: 30, tablet: 10 }); // same as 0.6, 0.3, 0.1oneOf<T>(options: T[])
Returns a uniformly random element from the array.
oneOf(['chrome', 'firefox', 'safari', 'edge']);fake<T>(generator: (faker: Faker) => T)
Runs a function against the Faker.js instance. The faker locale is set from the persona's locale attribute.
fake((faker) => faker.commerce.productName());
fake((faker) => faker.location.city());chance(probability: number)
Returns true with the given probability (0-1).
chance(0.3); // 30% chance of trueLocale Support
Set the locale attribute to control the Faker.js locale for the user. When a persona includes locale, the context's faker instance uses that locale for all subsequent fake() calls and ctx.faker access.
const germanUser = definePersona({
id: 'de-user',
name: 'German User',
attributes: {
locale: 'de', // static: all users get German locale
city: fake((faker) => faker.location.city()), // generates German city names
name: fake((faker) => faker.person.fullName()), // generates German names
},
});
const multiLocale = definePersona({
id: 'multi-locale',
name: 'Multi-Locale User',
attributes: {
locale: weighted({ en: 0.5, de: 0.3, ja: 0.2 }), // varies per user
name: fake((faker) => faker.person.fullName()), // locale-appropriate name
},
});Locale Loading Performance
English (en) and German (de) are pre-loaded and available instantly. All other locales are lazy-loaded on first use via a dynamic import, which adds a few milliseconds of initialization time per user context.
| Locale | Loading | Startup |
|---|---|---|
en, de | Pre-loaded (static import) | Instant |
| All others | Lazy-loaded (dynamic import) | ~5-10ms on first use |
This keeps the default bundle small (~200KB for en+de) while supporting all 75 Faker.js locales on demand. The lazy-load happens once per unique locale during generation — after the first user with that locale is created, subsequent users reuse the cached module.
Accessing Persona Attributes in Journeys
Persona attributes are stored in the user's context. Access them with ctx.get<T>(key).
import { defineAction } from '@synode/core';
const browseAction = defineAction({
id: 'browse',
name: 'page_view',
handler: (ctx) => {
const tier = ctx.get<string>('customerTier');
const device = ctx.get<string>('deviceType');
return [
{
id: ctx.generateId('event'),
userId: ctx.userId,
sessionId: ctx.sessionId,
name: 'page_view',
timestamp: ctx.now(),
payload: {
url: tier === 'enterprise' ? '/dashboard' : '/products',
deviceType: device,
locale: ctx.locale,
},
},
];
},
});Using Personas with generate
Pass the persona definition to generate via the persona option.
import { generate, defineJourney } from '@synode/core';
await generate(browseJourney, {
users: 1000,
persona: ecommerceUser,
lanes: 4,
});Custom Generator Functions
For complex attribute logic, use a raw function. It receives the context and the partially-built attributes object.
const advancedUser = definePersona({
id: 'advanced',
name: 'Advanced User',
attributes: {
region: oneOf(['us-east', 'us-west', 'eu-west', 'ap-south']),
isPremium: chance(0.25),
maxSessions: (ctx, attrs) => {
return attrs.isPremium
? ctx.faker.number.int({ min: 5, max: 20 })
: ctx.faker.number.int({ min: 1, max: 3 });
},
},
});Attributes are resolved in order, so later attributes can reference earlier ones through the second argument.
