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.