rilaykit
Workflow

Advanced Workflows

Dynamic step management, serialization, introspection, and cross-step data manipulation.

This guide covers advanced features of the flow builder for building dynamic, data-driven workflows.

Dynamic Step Management

You can modify steps after the builder is initialized. All methods are chainable.

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

const workflow = rilay.flow('onboarding', 'Onboarding')
  .addStep({ id: 'step-1', title: 'Step 1', formConfig: step1Form });

// Add steps conditionally
if (user.isAdmin) {
  workflow.addStep({ id: 'admin', title: 'Admin Setup', formConfig: adminForm });
}

// Update a step's configuration
workflow.updateStep('step-1', { title: 'Welcome' });

// Remove a step
workflow.removeStep('admin');

Step Management API

MethodDescription
.updateStep(stepId, updates)Updates an existing step. Throws if not found.
.removeStep(stepId)Removes a step by ID.
.getStep(stepId)Returns the step config or undefined.
.getSteps()Returns a copy of all step configurations.
.clearSteps()Removes all steps.

The onAfterValidation Callback

The most powerful feature for cross-step data management. This callback runs after a step's form validation passes but before navigation to the next step.

const workflow = rilay.flow('business-onboarding', 'Business Onboarding')
  .addStep({
    id: 'registration',
    title: 'Business Registration',
    formConfig: registrationForm,
    onAfterValidation: async (stepData, helper, context) => {
      // stepData contains the validated form data from this step
      const { country, registrationNumber } = stepData;

      // Call an external API
      const businessData = await validateBusiness(registrationNumber, country);

      // Pre-fill the next step's fields
      helper.setStepFields('company-details', {
        companyName: businessData.name,
        legalForm: businessData.legalForm,
        address: businessData.address,
      });
    },
  })
  .addStep({
    id: 'company-details',
    title: 'Company Details',
    formConfig: companyDetailsForm,
  });

Throwing an error inside onAfterValidation prevents navigation to the next step. Use this for server-side validation or API checks that must pass before proceeding.

StepDataHelper

The helper parameter provides methods to manipulate data across steps:

interface StepDataHelper {
  // Target a specific step
  setStepData(stepId: string, data: Record<string, any>): void;
  setStepFields(stepId: string, fields: Record<string, any>): void; // merges with existing
  getStepData(stepId: string): Record<string, any>;

  // Target the next step
  setNextStepField(fieldId: string, value: any): void;
  setNextStepFields(fields: Record<string, any>): void; // merges with existing

  // Global access
  getAllData(): Record<string, any>;
  getSteps(): StepConfig[];
}

Key difference: setStepData replaces the entire step's data, while setStepFields merges with existing data.

Cloning

Create variations of a workflow with .clone():

const baseWorkflow = rilay.flow('base', 'Base Onboarding')
  .addStep({ id: 'welcome', title: 'Welcome', formConfig: welcomeForm })
  .addStep({ id: 'profile', title: 'Profile', formConfig: profileForm });

// Create a variant with an extra step
const enterpriseWorkflow = baseWorkflow
  .clone('enterprise', 'Enterprise Onboarding')
  .addStep({ id: 'billing', title: 'Billing', formConfig: billingForm });

Serialization

Export and import workflow definitions as JSON for storage or visual editors.

// Export
const workflow = rilay.flow('survey', 'Survey')
  .addStep({ id: 'q1', title: 'Question 1', formConfig: q1Form });

const json = workflow.toJSON();
// Store in database, send to API, etc.

// Import
const restored = rilay.flow('survey-restored', 'Survey')
  .fromJSON(json);

const config = restored.build();

Validation

The .validate() method checks the builder configuration for common errors before building:

const errors = workflow.validate();
// Returns string[] — empty if valid
// Checks: at least 1 step, unique step IDs, plugin dependencies

.build() calls .validate() internally and throws if errors are found.

Introspection

Get a statistical overview of your workflow:

const stats = workflow.getStats();
// {
//   totalSteps: 3,
//   totalFields: 12,
//   averageFieldsPerStep: 4,
//   maxFieldsInStep: 6,
//   minFieldsInStep: 2,
//   hasAnalytics: true,
// }

Complete Example

import { rilay, required, email, when } from '@rilaykit/core';
import { Workflow, WorkflowBody, WorkflowStepper, WorkflowNextButton, WorkflowPreviousButton } from '@rilaykit/workflow';

// Build forms
const personalForm = rilay.form('personal')
  .add(
    { id: 'firstName', type: 'text', props: { label: 'First Name' }, validation: { validate: [required()] } },
    { id: 'lastName', type: 'text', props: { label: 'Last Name' }, validation: { validate: [required()] } }
  )
  .add({ id: 'email', type: 'text', props: { label: 'Email' }, validation: { validate: [required(), email()] } });

const preferencesForm = rilay.form('preferences')
  .add({ id: 'plan', type: 'select', props: { label: 'Plan', options: [{ value: 'free', label: 'Free' }, { value: 'pro', label: 'Pro' }] } });

// Build workflow
const onboarding = rilay.flow('onboarding', 'User Onboarding')
  .addStep({
    id: 'personal',
    title: 'Personal Info',
    formConfig: personalForm,
    onAfterValidation: async (data, helper) => {
      // Check if email already exists
      const exists = await checkEmailExists(data.email);
      if (exists) throw new Error('Email already registered');
    },
  })
  .addStep({
    id: 'preferences',
    title: 'Preferences',
    formConfig: preferencesForm,
    allowSkip: true,
  })
  .configure({
    analytics: {
      onWorkflowComplete: (id, totalTime) => {
        trackEvent('onboarding_complete', { duration: totalTime });
      },
    },
  });

// Render
function OnboardingPage() {
  return (
    <Workflow workflowConfig={onboarding} onWorkflowComplete={handleComplete}>
      <WorkflowStepper />
      <WorkflowBody />
      <div className="flex justify-between mt-6">
        <WorkflowPreviousButton />
        <WorkflowNextButton />
      </div>
    </Workflow>
  );
}

On this page