Advanced Workflows
Learn about advanced workflow features including dynamic steps, hooks, and analytics
The flow
builder, like the formBuilder
, is more than a simple configuration tool. It offers advanced features for creating highly dynamic and powerful user flows.
Dynamic Step Management
You can modify a workflow's steps after the builder has been initialized.
.updateStep(stepId, updates)
: Modifies an existing step's configuration..removeStep(stepId)
: Removes a step from the workflow..getStep(stepId)
: Retrieves the configuration for a specific step..getSteps()
: Returns an array of all step configurations..clearSteps()
: Removes all steps.
import { rilay } from '@/lib/rilay';
const myFlow = rilay.flow('my-flow', 'My Dynamic Flow')
.addStep({ id: 'step1', ... });
// Later, based on some condition...
if (user.isAdmin) {
myFlow.addStep({ id: 'admin-step', ... });
}
// Or update a step
myFlow.updateStep('step1', {
title: 'Step 1 (Updated)'
});
const workflowConfig = myFlow.build();
Cloning
The .clone(newWorkflowId?, newWorkflowName?)
method creates a deep copy of the builder instance, allowing you to create variations of a workflow.
const baseFlow = rilay.flow('base', 'Base Flow').addStep({ id: 'a', ... });
const variantFlow = baseFlow.clone('variant', 'Variant Flow').addStep({ id: 'b', ... });
Serialization (JSON Import/Export)
Workflow configurations can be serialized to and from JSON. This is useful for storing flow definitions in a database or building visual workflow editors.
.toJSON()
: Exports the current workflow structure as a JSON-serializable object..fromJSON(json)
: Populates a builder instance from a JSON object.
// 1. Define and serialize
const originalFlow = rilay.flow('flow1', 'Flow 1').addStep(...);
const jsonDefinition = originalFlow.toJSON();
// 2. Later, rehydrate from the definition
const newFlow = rilay.flow('flow2', 'Flow 2').fromJSON(jsonDefinition);
const workflowConfig = newFlow.build();
Plugins
Rilaykit's workflow builder supports a plugin architecture to extend its functionality. A plugin is an object with a name, version, and an install
method that gets called with the builder instance.
This is an advanced feature for creating reusable pieces of workflow logic.
// Example of a simple logging plugin
const loggingPlugin = {
name: 'simple-logger',
version: '1.0.0',
install(builder) {
const existingAnalytics = builder.build().analytics || {};
builder.setAnalytics({
...existingAnalytics,
onStepStart: (stepId) => {
console.log(`PLUGIN: Step started: ${stepId}`);
existingAnalytics.onStepStart?.(stepId);
}
});
}
};
// Use it
const myFlow = rilay.flow('my-flow', 'My Flow')
.use(loggingPlugin)
.addStep(...)
.build();
The builder also supports .removePlugin(pluginName)
and validates plugin dependencies.
Introspection
The .getStats()
method provides a quick overview of the workflow's structure.
const stats = myFlowBuilder.getStats();
console.log(stats);
/*
{
totalSteps: 3,
dynamicSteps: 0,
pluginsInstalled: 1,
estimatedFields: 10,
hasPersistence: false,
hasAnalytics: true,
allowBackNavigation: true
}
*/
This is useful for debugging or analyzing the complexity of your workflows.
Advanced Workflows
This guide covers advanced workflow features that help you build complex, dynamic workflows with sophisticated validation and data management.
Best Practices
// Step 1: Business Registration Form
const businessRegistrationForm = form.create(config, 'business-registration')
.add({
type: 'select',
props: {
label: 'Country',
placeholder: 'Select your country',
options: [
{ value: 'US', label: 'United States' },
{ value: 'FR', label: 'France' },
{ value: 'DE', label: 'Germany' },
{ value: 'UK', label: 'United Kingdom' },
{ value: 'CA', label: 'Canada' },
],
},
})
.add({
type: 'text',
props: {
label: 'Business Registration Number',
placeholder: 'Enter your business registration number',
helperText: 'EIN (US), SIREN (FR), HRB (DE), Companies House (UK), etc.',
},
})
.build();
// Step 2: Company Details Form (will be pre-filled)
const companyDetailsForm = form.create(config, 'company-details')
.add({
type: 'text',
props: {
label: 'Company Name',
disabled: true, // Will be filled from API
},
})
.add({
type: 'text',
props: {
label: 'Legal Form',
disabled: true,
},
})
.add({
type: 'textarea',
props: {
label: 'Registered Address',
disabled: true,
},
})
.add({
type: 'text',
props: {
label: 'Contact Email',
placeholder: 'contact@company.com',
},
})
.build();
// Create workflow with step transition validation
const businessOnboardingWorkflow = flow.create(
config,
'business-onboarding',
'Business Onboarding'
)
.addStep({
id: 'registration',
title: 'Business Registration',
formConfig: businessRegistrationForm,
onAfterValidation: async (stepData, helper, context) => {
const { country, registrationNumber } = stepData;
// Validate required fields
if (!country || !registrationNumber) {
// You can throw an error to halt transition
throw new Error('Country and registration number are required');
}
try {
// Call API to validate and fetch business data
const businessData = await validateBusinessRegistration(
registrationNumber,
country
);
// Use the helper to modify data for subsequent steps
helper.setStepFields('company-details', {
companyName: businessData.name,
legalForm: businessData.legalForm,
registeredAddress: businessData.address,
});
// You can also modify the data for the entire workflow
helper.setNextStepFields({
taxId: businessData.taxId,
});
} catch (error) {
// Throwing an error will prevent navigation and can be caught to display a message
if (error.status === 404) {
throw new Error('Business registration number not found');
}
throw new Error('Unable to validate registration number. Please try again.');
}
},
})
.addStep({
id: 'company-details',
title: 'Company Details',
formConfig: companyDetailsForm,
})
.addStep({
id: 'verification',
title: 'Verification',
formConfig: verificationForm,
})
.build();
Using the Step Error Management Functions
The workflow context provides functions to manage step-level errors:
import { useWorkflowContext } from '@rilaykit/workflow';
function BusinessRegistrationStep() {
const {
getCurrentStepErrors,
isCurrentStepValidating,
goNext,
} = useWorkflowContext();
const stepErrors = getCurrentStepErrors();
const isValidating = isCurrentStepValidating();
return (
<div>
<WorkflowBody />
{/* Display step-level errors */}
{stepErrors.length > 0 && (
<div className="error-panel">
<h4>Validation Errors:</h4>
<ul>
{stepErrors.map((error, index) => (
<li key={index} className="error-message">
{error.message}
</li>
))}
</ul>
</div>
)}
{/* Navigation buttons */}
<div className="navigation">
<WorkflowPreviousButton />
<WorkflowNextButton disabled={isValidating}>
{isValidating ? 'Validating...' : 'Continue'}
</WorkflowNextButton>
</div>
</div>
);
}
Tax ID Validation Example
Here's another example for tax ID validation that works across multiple countries:
const taxIdValidationStep = {
id: 'tax-validation',
title: 'Tax Information',
formConfig: taxIdForm,
onAfterValidation: async (stepData, helper, context) => {
const { country, taxId } = stepData;
try {
// Different validation patterns by country
const validationRules = {
US: /^\\d{2}-\\d{7}$/, // EIN format
FR: /^\\d{11}$/, // SIRET format
DE: /^\\d{11}$/, // Steuernummer format
UK: /^\\d{10}$/, // UTR format
CA: /^\\d{9}$/, // BN format
};
const pattern = validationRules[country];
if (!pattern || !pattern.test(taxId)) {
throw new Error(`Invalid tax ID format for ${country}`);
}
// Validate with tax authority API
const response = await fetch(`/api/tax/validate/${country}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ taxId }),
});
if (!response.ok) {
throw new Error('Tax ID could not be verified with authorities');
}
const taxData = await response.json();
helper.setNextStepFields({
taxStatus: taxData.status,
registeredName: taxData.registeredName,
});
} catch (error) {
throw new Error('Unable to validate tax ID. Please try again later.');
}
},
};
Best Practices
- Error Handling: Always provide meaningful error messages in the user's language
- Loading States: Use
isCurrentStepValidating()
to show loading indicators - Fallback Validation: Have client-side validation as a fallback
- API Timeouts: Implement proper timeout handling for API calls
- Retry Logic: Consider implementing retry mechanisms for transient failures
// Example with retry logic
const retryApiCall = async (fn: () => Promise<any>, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
};
// Usage in onAfterValidation
const stepConfig = {
// ... other step properties
onAfterValidation: async (stepData, helper, context) => {
try {
const result = await retryApiCall(() =>
validateBusinessRegistration(stepData.registrationNumber, stepData.country)
);
helper.setNextStepFields({ businessInfo: result });
} catch (error) {
throw new Error('Validation failed after retries');
}
},
};
This approach provides a robust, international-friendly way to handle step transitions with API validation that can adapt to different countries and regulatory requirements.