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');| 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
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).