Skip to content

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.

ts
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.

ts
weighted({ mobile: 60, desktop: 30, tablet: 10 }); // same as 0.6, 0.3, 0.1

oneOf<T>(options: T[]) ​

Returns a uniformly random element from the array.

ts
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.

ts
fake((faker) => faker.commerce.productName());
fake((faker) => faker.location.city());

chance(probability: number) ​

Returns true with the given probability (0-1).

ts
chance(0.3); // 30% chance of true

Locale 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.

ts
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.

LocaleLoadingStartup
en, dePre-loaded (static import)Instant
All othersLazy-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).

ts
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.

ts
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.

ts
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.