Cookbook
Practical recipes for common Synode use cases. Each recipe is self-contained and can be copied directly into your project.
E-commerce purchase funnel
A multi-journey flow: signup, browse products, add to cart, purchase. The purchase journey requires browse to have completed first.
import {
defineJourney,
defineAdventure,
defineAction,
defineDataset,
definePersona,
generate,
InMemoryAdapter,
weighted,
fake,
oneOf,
InferDatasetRow,
} from '@synode/core';
const persona = definePersona({
id: 'shoppers',
name: 'Shoppers',
attributes: {
locale: weighted({ en: 0.6, de: 0.2, fr: 0.2 }),
device: oneOf(['mobile', 'desktop', 'tablet']),
},
});
const productsDef = defineDataset({
id: 'products',
name: 'Products',
count: 100,
fields: {
id: (_ctx, row) => `prod-${row.index}`,
name: (ctx) => ctx.faker.commerce.productName(),
price: (ctx) => ctx.faker.number.float({ min: 10, max: 200 }),
},
});
type Product = InferDatasetRow<typeof productsDef>;
const browse = defineJourney({
id: 'browse',
name: 'Browse Products',
adventures: [
defineAdventure({
id: 'view-products',
name: 'View Products',
timeSpan: { min: 1000, max: 5000 },
actions: [
defineAction({
id: 'view-product',
name: 'product_viewed',
handler: (ctx) => {
const product = ctx.typedDataset<Product>('products').randomRow();
ctx.set('lastProduct', product, { scope: 'journey' });
return [
{
id: ctx.generateId('event'),
userId: ctx.userId,
sessionId: ctx.sessionId,
name: 'product_viewed',
timestamp: ctx.now(),
payload: { productId: product.id, price: product.price },
},
];
},
}),
],
}),
],
});
const purchase = defineJourney({
id: 'purchase',
name: 'Purchase',
requires: ['browse'],
bounceChance: 0.3,
adventures: [
defineAdventure({
id: 'checkout',
name: 'Checkout',
actions: [
defineAction({
id: 'purchase',
name: 'purchase_completed',
fields: { total: (ctx) => ctx.faker.number.float({ min: 20, max: 500 }) },
}),
],
}),
],
});
const adapter = new InMemoryAdapter();
await generate([browse, purchase], {
users: 1000,
lanes: 4,
persona,
datasets: [productsDef],
adapter,
});
console.log(`Generated ${adapter.events.length} events`);Historical data generation
Generate 3 months of backdated events by specifying startDate and endDate. User start times are randomized within the range.
import { generate } from '@synode/core';
import { FileAdapter } from '@synode/adapter-file';
await generate(journey, {
users: 5000,
startDate: new Date('2026-01-01'),
endDate: new Date('2026-03-31'),
adapter: new FileAdapter({ path: './out/events.jsonl', format: 'jsonl' }),
});Multi-adapter output
Write events to a local file and forward them to an HTTP webhook simultaneously using CompositeAdapter.
import { generate } from '@synode/core';
import { FileAdapter } from '@synode/adapter-file';
import { HttpAdapter } from '@synode/adapter-http';
import { CompositeAdapter } from '@synode/adapter-composite';
const adapter = new CompositeAdapter([
new FileAdapter({ path: './out/events.jsonl', format: 'jsonl' }),
new HttpAdapter({
url: 'https://webhook.example.com/ingest',
batchSize: 50,
headers: { Authorization: 'Bearer my-token' },
}),
]);
await generate(journey, { users: 500, adapter });
await adapter.close();Dataset-only generation
Generate datasets without running any journeys. Useful for seeding product catalogs, location tables, or other reference data.
import {
generate,
InMemoryAdapter,
defineJourney,
defineAdventure,
defineAction,
} from '@synode/core';
// Minimal no-op journey (required by generate)
const noop = defineJourney({
id: 'noop',
name: 'No-op',
adventures: [
defineAdventure({
id: 'noop',
name: 'No-op',
actions: [defineAction({ id: 'noop', name: 'noop', fields: {} })],
}),
],
bounceChance: 1, // always bounces, no events generated
});
const adapter = new InMemoryAdapter();
await generate(noop, { users: 1, datasets: [productsDef, locationsDef], adapter });
// adapter.events is empty, but datasets are hydratedDry-run testing
Validate configuration and generate a single user to verify the journey produces expected events.
import { validateConfig, dryRun } from '@synode/core';
// Step 1: validate structure
validateConfig(journey);
// Step 2: generate 1 user and inspect
const events = await dryRun(journey, 1);
console.log(`Events: ${events.map((e) => e.name).join(', ')}`);Schema validation in CI
Run generation in strict mode so the pipeline fails on any schema violation.
import { z } from 'zod';
import { generate, InMemoryAdapter, defineEventSchema } from '@synode/core';
const pageViewSchema = defineEventSchema({
url: z.string().url(),
referrer: z.string().optional(),
});
const addToCartSchema = defineEventSchema({
productId: z.string(),
quantity: z.number().int().positive(),
price: z.number().positive(),
});
try {
await generate(journey, {
users: 100,
adapter: new InMemoryAdapter(),
eventSchema: {
schema: {
page_view: pageViewSchema,
add_to_cart: addToCartSchema,
},
mode: 'strict', // throws SynodeValidationError on first failure
},
});
console.log('All events passed validation');
} catch (err) {
console.error('Schema validation failed:', err);
process.exit(1);
}Use mode: 'warn' to log failures without halting, or mode: 'skip' to silently drop invalid events.
