rilaykit
Workflow

Plugins

Extend workflow behavior with the plugin system.

The Rilaykit workflow plugin system lets you encapsulate reusable behavior and inject it into any workflow. Plugins receive the flow builder instance during installation, giving them full access to add steps, configure analytics, modify existing steps, and more.

WorkflowPlugin Interface

A plugin is a plain object that implements the WorkflowPlugin interface:

interface WorkflowPlugin {
  /** Unique name identifying the plugin */
  name: string;

  /** Semantic version (for dependency resolution and debugging) */
  version?: string;

  /** Names of plugins that must be installed before this one */
  dependencies?: string[];

  /** Called with the flow builder instance when the plugin is installed */
  install(builder: flow): void;
}

Using Plugins

Install a plugin by calling .use() on the flow builder. Plugins are installed immediately and have access to the builder at the point of installation.

import { rilay } from '@/lib/rilay';

const workflow = rilay
  .flow('onboarding', 'Onboarding')
  .use(myPlugin)
  .addStep({
    id: 'personal-info',
    title: 'Personal Info',
    formConfig: personalInfoForm,
  })
  .addStep({
    id: 'preferences',
    title: 'Preferences',
    formConfig: preferencesForm,
  })
  .build();

You can chain multiple .use() calls:

const workflow = rilay
  .flow('checkout', 'Checkout')
  .use(loggingPlugin)
  .use(validationPlugin)
  .use(analyticsPlugin)
  .addStep(...)
  .build();

Plugins are called in the order they are installed. If plugin B depends on modifications made by plugin A, install A first.


Creating a Plugin

A plugin's install method receives the flow builder instance. You can call any builder method inside it: addStep, configure, updateStep, addStepConditions, etc.

Example: Logging Plugin

This plugin adds an onAfterValidation callback to every existing step that logs the step data:

import type { WorkflowPlugin } from '@rilaykit/core';

const loggingPlugin: WorkflowPlugin = {
  name: 'step-logger',
  version: '1.0.0',

  install(builder) {
    // Get all steps that have been added so far
    const steps = builder.getSteps();

    for (const step of steps) {
      const existingCallback = step.onAfterValidation;

      builder.updateStep(step.id, {
        onAfterValidation: async (stepData, helper, context) => {
          console.log(`[step-logger] Step "${step.id}" validated:`, stepData);

          // Call the original callback if it existed
          if (existingCallback) {
            await existingCallback(stepData, helper, context);
          }
        },
      });
    }
  },
};

Usage:

const workflow = rilay
  .flow('onboarding', 'Onboarding')
  .addStep({
    id: 'personal-info',
    title: 'Personal Info',
    formConfig: personalInfoForm,
  })
  .addStep({
    id: 'preferences',
    title: 'Preferences',
    formConfig: preferencesForm,
  })
  .use(loggingPlugin) // Install AFTER adding steps so getSteps() returns them
  .build();

Since install is called immediately, the plugin only sees steps that have been added before .use() is called. If you need the plugin to affect all steps, install it after all .addStep() calls.

Example: Analytics Integration Plugin

A plugin that adds analytics tracking to the workflow:

import type { WorkflowPlugin } from '@rilaykit/core';

function createAnalyticsPlugin(trackFn: (event: string, data: any) => void): WorkflowPlugin {
  return {
    name: 'analytics-tracker',
    version: '1.0.0',

    install(builder) {
      builder.configure({
        analytics: {
          onWorkflowStart: (id) => {
            trackFn('workflow_started', { workflowId: id });
          },
          onStepComplete: (stepId, duration, stepData) => {
            trackFn('step_completed', { stepId, duration });
          },
          onWorkflowComplete: (id, totalTime) => {
            trackFn('workflow_completed', {
              workflowId: id,
              totalTimeSeconds: Math.round(totalTime / 1000),
            });
          },
          onError: (error) => {
            trackFn('workflow_error', { message: error.message });
          },
        },
      });
    },
  };
}

// Usage
const workflow = rilay
  .flow('checkout', 'Checkout')
  .addStep(...)
  .use(createAnalyticsPlugin(posthog.capture))
  .build();

Example: Conditional Step Plugin

A plugin that adds visibility conditions to specific steps:

import type { WorkflowPlugin } from '@rilaykit/core';
import { when } from '@rilaykit/core';

const premiumStepsPlugin: WorkflowPlugin = {
  name: 'premium-steps',
  version: '1.0.0',

  install(builder) {
    // Make the "advanced-settings" step visible only for premium users
    try {
      builder.addStepConditions('advanced-settings', {
        visible: when('userPlan').equals('premium').build(),
      });
    } catch {
      // Step may not exist in all workflows using this plugin
      console.warn('[premium-steps] Step "advanced-settings" not found, skipping.');
    }
  },
};

Plugin Dependencies

The dependencies array declares which plugins must be installed before the current one. The builder validates this at installation time and throws if any dependency is missing.

const enhancedLoggingPlugin: WorkflowPlugin = {
  name: 'enhanced-logger',
  version: '1.0.0',
  dependencies: ['step-logger'], // Requires the base logging plugin

  install(builder) {
    // This plugin extends the base logger with more detail
    const steps = builder.getSteps();

    for (const step of steps) {
      const existingCallback = step.onAfterValidation;

      builder.updateStep(step.id, {
        onAfterValidation: async (stepData, helper, context) => {
          console.log(`[enhanced-logger] Step "${step.id}" context:`, {
            currentIndex: context.currentStepIndex,
            totalSteps: context.totalSteps,
            visitedSteps: Array.from(context.visitedSteps),
          });

          if (existingCallback) {
            await existingCallback(stepData, helper, context);
          }
        },
      });
    }
  },
};

Dependency Resolution

Install dependencies first

Plugins are checked against the list of already-installed plugins. If a dependency is missing, an error is thrown immediately.

Install the dependent plugin

Once all dependencies are satisfied, the plugin's install method is called.

// This works -- step-logger is installed before enhanced-logger
rilay.flow('test', 'Test')
  .use(loggingPlugin)          // name: 'step-logger'
  .use(enhancedLoggingPlugin)  // depends on 'step-logger'
  .build();

// This throws -- enhanced-logger's dependency is not satisfied
rilay.flow('test', 'Test')
  .use(enhancedLoggingPlugin)  // Error: Plugin "enhanced-logger" requires missing dependencies: step-logger
  .build();

The error message includes the names of all missing dependencies so you know exactly what to install first.


Removing Plugins

Use .removePlugin(pluginName) to remove a plugin from the builder. Note that this only removes the plugin from the internal registry -- any modifications the plugin made during install (added steps, configured analytics, etc.) are not rolled back.

const workflow = rilay
  .flow('onboarding', 'Onboarding')
  .use(loggingPlugin)
  .addStep(...)
  .removePlugin('step-logger')
  .build();

This is primarily useful when cloning a workflow and wanting to swap plugins:

const baseWorkflow = rilay
  .flow('base', 'Base Workflow')
  .use(productionAnalytics)
  .addStep(...);

const testWorkflow = baseWorkflow
  .clone('test', 'Test Workflow')
  .removePlugin('production-analytics')
  .use(testAnalytics);

Validation

The .validate() method on the builder checks plugin dependencies as part of its validation pass. If a plugin lists a dependency that is no longer installed (e.g., after removePlugin), validation reports it.

const errors = workflow.validate();
// ["Plugin "enhanced-logger" requires missing dependencies: step-logger"]

This is also checked automatically when calling .build(), which throws if validation fails.


Best Practices

  1. Name plugins clearly -- Use descriptive names like analytics-posthog or conditional-premium-steps so dependency errors are easy to understand.

  2. Install after addStep when accessing steps -- If your plugin calls getSteps() or updateStep(), install it after all steps have been added.

  3. Preserve existing callbacks -- When wrapping onAfterValidation, always call the original callback to avoid breaking other plugins or user-defined logic.

  4. Handle missing steps gracefully -- Not every workflow will have the same steps. Use try/catch around updateStep and addStepConditions calls that target specific step IDs.

  5. Use factory functions -- When plugins need configuration, wrap them in a factory function that returns a WorkflowPlugin object. This makes them reusable across workflows with different settings.

On this page