Getting Started
Get from zero to generated events in five minutes.
Install
The fastest way to start is the interactive scaffolder:
npm install -g @synode/cli
synode initThis creates a project with config, dependencies, and an optional example journey. See the CLI guide for details.
Or install the core library directly:
npm install @synode/coreRequires Node.js 18+.
Core Concepts
Synode organizes generation around three pillars:
| Pillar | Purpose | Required? |
|---|---|---|
| Users | Synthetic identities with personas | Yes |
| Datasets | Pre-generated entity tables (products, locations) | No |
| Journeys | Behavioral flows that emit events | No |
Events are produced through a four-level hierarchy: Journey (user goal) -> Adventure (session) -> Action (behavior) -> Event[] (output).
Your First Journey
1. Define an action
The fields shorthand generates a single event with the given payload:
import { defineAction } from '@synode/core';
const pageView = defineAction({
id: 'page-view',
name: 'page_viewed',
fields: {
url: '/home',
title: 'Home Page',
referrer: (ctx) => ctx.faker.internet.url(),
},
});2. Define an adventure
Group actions into a session with timing and optional bounce behavior:
import { defineAdventure } from '@synode/core';
const browsing = defineAdventure({
id: 'browse',
name: 'Browse Website',
actions: [pageView],
timeSpan: { min: 1000, max: 3000 },
bounceChance: 0.2,
});3. Define a journey
Tie adventures into a complete user flow:
import { defineJourney } from '@synode/core';
const visit = defineJourney({
id: 'website-visit',
name: 'Website Visit',
adventures: [browsing],
});4. Generate
Collect events with InMemoryAdapter:
import { generate, InMemoryAdapter } from '@synode/core';
const adapter = new InMemoryAdapter();
await generate(visit, { users: 50, adapter });
console.log(`Generated ${adapter.events.length} events`);Adding a Dataset
Datasets are pre-generated entity tables you can reference inside actions. Define a catalog, then use ctx.dataset() in a handler:
import { defineDataset, defineAction, oneOf } from '@synode/core';
const products = defineDataset({
id: 'products',
name: 'Product Catalog',
count: 50,
fields: {
productId: (_ctx, row) => `prod-${row.index}`,
name: (ctx) => ctx.faker.commerce.productName(),
price: (ctx) => ctx.faker.number.float({ min: 9.99, max: 499.99 }),
category: oneOf(['electronics', 'clothing', 'home', 'sports']),
},
});
const viewProduct = defineAction({
id: 'view-product',
name: 'product_viewed',
handler: (ctx) => {
const product = ctx.dataset('products').randomRow();
return [
{
id: ctx.generateId(),
userId: ctx.userId,
sessionId: ctx.sessionId,
name: 'product_viewed',
timestamp: ctx.now(),
payload: { productId: product.productId, price: product.price },
},
];
},
});
await generate(visit, { users: 50, datasets: [products], adapter });Adding a Persona
Personas assign weighted attributes to each user. The locale attribute configures Faker.js automatically:
import { definePersona, weighted } from '@synode/core';
const shopper = definePersona({
id: 'shopper',
name: 'Online Shopper',
attributes: {
locale: weighted({ en: 50, es: 20, fr: 15, de: 15 }),
deviceType: weighted({ mobile: 60, desktop: 30, tablet: 10 }),
},
});
await generate(visit, { users: 50, persona: shopper, datasets: [products], adapter });Each user's ctx.locale and ctx.faker reflect their assigned locale. Access persona attributes with ctx.get('deviceType').
Next Steps
- Personas -- attribute distributions and locale control
- Datasets -- typed datasets with
InferDatasetRow - Multi-journey -- prerequisites and suppression
- Parallel processing -- lanes and worker threads
- Event validation -- Zod schema enforcement
- Output adapters -- File, HTTP, Stream, and more
- CLI -- config-driven generation from the terminal
- Cookbook -- real-world recipes
