rilaykit
Workflow

Advanced Workflows

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

Advanced features of the flow builder for dynamic, data-driven workflows.

Dynamic Step Management

Steps can be modified after the builder is initialized. All methods are chainable.

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

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

if (user.isAdmin) {
  workflow.step({ id: 'admin', title: 'Admin Setup', formConfig: adminForm });
}

workflow.updateStep('step-1', { title: 'Welcome' });
workflow.removeStep('admin');
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

Runs after a step's form validation passes but before navigation. Use it for server-side checks and to prefill later steps.

const workflow = rilay.flow('business-onboarding', 'Business Onboarding')
  .step({
    id: 'registration',
    title: 'Business Registration',
    formConfig: registrationForm,
    onAfterValidation: async (stepData, helper, context) => {
      const businessData = await validateBusiness(stepData.registrationNumber, stepData.country);

      helper.setStepFields('company-details', {
        companyName: businessData.name,
        legalForm: businessData.legalForm,
      });
    },
  })
  .step({ id: 'company-details', title: 'Company Details', formConfig: companyDetailsForm });

Throwing inside onAfterValidation blocks navigation. The error is also routed to analytics.onError and the monitoring adapter.

StepDataHelper

The helper parameter manipulates data across steps:

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

  // Target the next VISIBLE step (a conditionally hidden step never swallows the prefill)
  setNextStepField(fieldId: string, value: any): void;
  setNextStepFields(fields: Record<string, any>): void; // merges

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

Cloning

.clone(newId?, newName?) creates an independent copy:

const enterpriseWorkflow = baseWorkflow
  .clone('enterprise', 'Enterprise Onboarding')
  .step({ id: 'billing', title: 'Billing', formConfig: billingForm });

Serialization

.toJSON() / .fromJSON(json) export and import workflow definitions for storage or visual editors:

const json = workflow.toJSON();

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

Validation

.validate() returns a string[] of configuration errors (at least 1 step, unique step IDs, plugin dependencies) — empty if valid. .build() calls it internally and throws on errors.

Introspection

const stats = workflow.getStats();
// { totalSteps, totalFields, averageFieldsPerStep,
//   maxFieldsInStep, minFieldsInStep, hasAnalytics }

Complete Example

import { rilay } from '@/lib/rilay';
import { email, required } from '@rilaykit/core';
import { Flow } from '@rilaykit/workflow/react';

const personalForm = rilay.form('personal')
  .add(
    { id: 'firstName', type: 'text', props: { label: 'First Name' }, validation: { validate: [required()] } },
    { 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' }] },
  });

const onboarding = rilay.flow('onboarding', 'User Onboarding')
  .step({
    id: 'personal',
    title: 'Personal Info',
    formConfig: personalForm,
    onAfterValidation: async (data) => {
      if (await checkEmailExists(data.email)) throw new Error('Email already registered');
    },
  })
  .step({ id: 'preferences', title: 'Preferences', formConfig: preferencesForm, allowSkip: true });

function OnboardingPage() {
  return (
    <Flow of={onboarding} onComplete={(data, meta) => save(data, meta.skippedSteps)}>
      <Flow.Progress />
      <Flow.Body />
      <div className="flex justify-between mt-6">
        <Flow.Back />
        <div className="flex gap-2">
          <Flow.Skip />
          <Flow.Next />
        </div>
      </div>
    </Flow>
  );
}

onComplete(data, meta) receives only answered visible steps in data — skipped or never-visible steps are absent. meta is a WorkflowCompletionMeta (visitedSteps, skippedSteps, passedSteps).

On this page