Skip to content

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.

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

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

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

typescript
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 hydrated

Dry-run testing

Validate configuration and generate a single user to verify the journey produces expected events.

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

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