Skip to content

Multi-Journey Prerequisites

Journeys can depend on other journeys via requires. The engine checks prerequisites before starting each journey and skips journeys whose dependencies have not been completed. Combined with bounce and suppression, this creates realistic funnel drop-off behavior.

Basic Prerequisites

Set requires to an array of journey IDs that must complete before this journey starts.

ts
import { defineJourney, defineAdventure, defineAction, fake } from '@synode/core';

const signupJourney = defineJourney({
  id: 'signup',
  name: 'Signup Flow',
  adventures: [
    defineAdventure({
      id: 'registration',
      name: 'Registration',
      timeSpan: { min: 2000, max: 5000 },
      actions: [
        defineAction({
          id: 'signup-form',
          name: 'sign_up',
          fields: {
            method: 'email',
            source: fake((f) => f.helpers.arrayElement(['organic', 'paid', 'referral'])),
          },
        }),
      ],
    }),
  ],
});

const browseJourney = defineJourney({
  id: 'browse',
  name: 'Browse Products',
  requires: ['signup'], // must complete signup first
  adventures: [
    /* ... */
  ],
});

const purchaseJourney = defineJourney({
  id: 'purchase',
  name: 'Purchase Flow',
  requires: ['signup', 'browse'], // must complete both
  adventures: [
    /* ... */
  ],
});

How Prerequisites Work

  1. Journeys are passed to generate as an array and executed in order for each user
  2. Before starting a journey, the engine checks ctx.hasCompletedJourney(id) for each required ID
  3. If any prerequisite is not met, the journey is silently skipped
  4. When a journey completes, ctx.markJourneyComplete(id) is called automatically

The order you pass journeys to generate matters -- place prerequisites earlier in the array.

ts
import { generate } from '@synode/core';

// Order matters: signup -> browse -> purchase
await generate([signupJourney, browseJourney, purchaseJourney], {
  users: 1000,
  lanes: 4,
});

Shared Context Across Journeys

Context persists across all journeys for a single user. Data set in one journey is available in later journeys.

ts
// In signup journey: store data for downstream journeys
const signupAction = defineAction({
  id: 'signup-complete',
  name: 'sign_up_complete',
  handler: (ctx) => {
    ctx.set('accountType', 'premium');
    return [
      {
        id: ctx.generateId('event'),
        userId: ctx.userId,
        sessionId: ctx.sessionId,
        name: 'sign_up_complete',
        timestamp: ctx.now(),
        payload: { accountType: 'premium' },
      },
    ];
  },
});

// In purchase journey: read data set by signup journey
const purchaseAction = defineAction({
  id: 'checkout',
  name: 'checkout',
  handler: (ctx) => {
    const accountType = ctx.get<string>('accountType');
    const discount = accountType === 'premium' ? 0.1 : 0;
    return [
      {
        id: ctx.generateId('event'),
        userId: ctx.userId,
        sessionId: ctx.sessionId,
        name: 'checkout',
        timestamp: ctx.now(),
        payload: { discount, accountType },
      },
    ];
  },
});

Context Scoping

Fields can be scoped to automatically clean up when a scope ends. Global fields (no scope) persist across all journeys.

ts
// Persists across all journeys (default)
ctx.set('accountId', 'acc-123');

// Cleared after the current journey completes
ctx.set('cartItems', [], { scope: 'journey' });

// Cleared after the current adventure completes
ctx.set('searchQuery', 'shoes', { scope: 'adventure' });

// Cleared after the current action completes
ctx.set('tempCalc', 42, { scope: 'action' });

Bounce Behavior

Bounce creates realistic funnel drop-off. Users who bounce on a journey never complete it, so downstream journeys with requires are also skipped.

Bounce applies at three levels:

LevelbounceChanceEffect
JourneyJourney.bounceChanceJourney never starts, no events generated
AdventureAdventure.bounceChanceonBounce: 'stop' ends journey, 'skip' continues to next adventure
ActionAction.bounceChanceStops the current adventure

onBounce options: 'stop' (default) ends the entire journey (NOT marked complete), 'skip' skips to the next adventure.

Suppression Periods

After a journey completes or bounces, a suppression period advances the user's clock. This creates realistic gaps between journeys.

ts
const signupJourney = defineJourney({
  id: 'signup',
  name: 'Signup',
  suppressionPeriod: { min: 3600000, max: 86400000 }, // 1 hour to 1 day
  adventures: [
    /* ... */
  ],
});

Suppression is applied in both cases:

  • Journey completes normally: user waits before starting the next journey
  • Journey bounces at the journey level: user waits before the next journey is attempted