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
| Method | Description |
|---|---|
.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>
);
}