rilaykit
Workflow

Navigation

Navigate between workflow steps with validation, conditions, and skip logic.

Workflow navigation in Rilaykit is fully managed through the useWorkflowContext hook. It handles forward/backward movement, step skipping, validation gating, conditional visibility, and cross-step data manipulation.

All navigation methods are available from useWorkflowContext(). They return a Promise<boolean> indicating whether the navigation succeeded.

import { useWorkflowContext } from '@rilaykit/workflow';

function MyNavigation() {
  const { goNext, goPrevious, goToStep, skipStep } = useWorkflowContext();

  return (
    <div className="flex gap-2">
      <button onClick={() => goPrevious()}>Back</button>
      <button onClick={() => skipStep()}>Skip</button>
      <button onClick={() => goNext()}>Next</button>
      <button onClick={() => goToStep(0)}>Go to first step</button>
    </div>
  );
}

Method Reference

MethodSignatureDescription
goNext()() => Promise<boolean>Validates the current step, runs onAfterValidation if defined, marks the step as passed, then advances to the next visible step.
goPrevious()() => Promise<boolean>Navigates to the previous visible step. Does not trigger validation.
goToStep(index)(stepIndex: number) => Promise<boolean>Jumps to a specific step by its original index (not the visible index). Fails if the step is hidden or out of bounds.
skipStep()() => Promise<boolean>Skips the current step without validation. Only works if allowSkip: true or the step's skippable condition evaluates to true. Fires onStepSkip analytics.

goNext() is the only navigation method that triggers form validation and onAfterValidation. All other methods move directly without validating.


Guards let you check whether a navigation action is possible before attempting it. Use them to conditionally render or disable buttons.

const {
  canGoNext,
  canGoPrevious,
  canGoToStep,
  canSkipCurrentStep,
} = useWorkflowContext();
GuardSignatureReturns true when
canGoNext()() => booleanThere is a visible step after the current one.
canGoPrevious()() => booleanThere is a visible step before the current one.
canGoToStep(index)(stepIndex: number) => booleanThe target step is within bounds and currently visible.
canSkipCurrentStep()() => booleanThe step has allowSkip: true and the skippable condition evaluates to true.
function NavigationButtons() {
  const { goNext, goPrevious, canGoNext, canGoPrevious } = useWorkflowContext();

  return (
    <div className="flex justify-between">
      <button
        onClick={() => goPrevious()}
        disabled={!canGoPrevious()}
      >
        Previous
      </button>
      <button
        onClick={() => goNext()}
        disabled={!canGoNext()}
      >
        Next
      </button>
    </div>
  );
}

Automatic Step Skipping

Invisible steps are automatically skipped during navigation. When you call goNext() and the next step in sequence is hidden by a condition, the workflow finds the next visible step and jumps to it. The same applies for goPrevious().

Steps:  [1: visible] [2: hidden] [3: hidden] [4: visible]

goNext() from step 1  -->  lands on step 4
goPrevious() from step 4  -->  lands on step 1

If the current step becomes invisible (e.g., a condition changes while viewing it), the workflow automatically relocates to the nearest visible step -- first looking forward, then backward.


Step Conditions

Step conditions control visibility and skippability dynamically based on workflow data. They use the when() condition builder from @rilaykit/core.

StepConditionalBehavior

interface StepConditionalBehavior {
  visible?: ConditionConfig;   // When false, the step is hidden and skipped
  skippable?: ConditionConfig; // When true, the step can be skipped
}

Defining Conditions

Use the when() builder to create conditions based on data from any step in the workflow.

import { when } from '@rilaykit/core';

const workflow = rilay
  .flow('onboarding', 'Onboarding')
  .addStep({
    id: 'personal-info',
    title: 'Personal Info',
    formConfig: personalInfoForm,
  })
  .addStep({
    id: 'company-info',
    title: 'Company Info',
    formConfig: companyInfoForm,
    conditions: {
      // Only show this step when accountType is "business"
      visible: when('accountType').equals('business').build(),
    },
  })
  .addStep({
    id: 'preferences',
    title: 'Preferences',
    formConfig: preferencesForm,
    allowSkip: true,
    conditions: {
      // Dynamically make skippable when user has existing preferences
      skippable: when('hasExistingPreferences').equals(true).build(),
    },
  });
const workflow = rilay
  .flow('onboarding', 'Onboarding')
  .addStep({
    id: 'company-info',
    title: 'Company Info',
    formConfig: companyInfoForm,
  })
  .addStepConditions('company-info', {
    visible: when('accountType').equals('business').build(),
  });

Conditions evaluate against a flattened view of all workflow data across all steps. Field values from step personal-info are available by their field ID when evaluating conditions on step company-info.

How Conditions Affect Navigation

  • visible: false -- The step is excluded from the step list, the stepper, and all navigation. goNext() and goPrevious() skip over it automatically.
  • skippable: true -- Combined with allowSkip: true, the step can be skipped via skipStep() or the <WorkflowSkipButton>.

The onAfterValidation Callback

The onAfterValidation callback is a powerful hook that runs after a step's form passes validation but before the navigation to the next step occurs. It is the right place for:

  • API calls triggered by the validated data
  • Pre-filling subsequent steps with fetched data
  • Custom validation that depends on external services
.addStep({
  id: 'registration',
  title: 'Business Registration',
  formConfig: registrationForm,
  onAfterValidation: async (stepData, helper, context) => {
    // Call an API with the validated data
    const companyInfo = await fetchCompanyBySiren(stepData.siren);

    // Pre-fill the next step
    helper.setNextStepFields({
      companyName: companyInfo.name,
      address: companyInfo.address,
    });

    // Or target a specific step by ID
    helper.setStepFields('company-details', {
      legalForm: companyInfo.legalForm,
    });
  },
})

If onAfterValidation throws an error, navigation is cancelled and the user stays on the current step. Use this to block progression when an API call fails.

Signature

onAfterValidation?: (
  stepData: Record<string, any>,
  helper: StepDataHelper,
  context: WorkflowContext
) => void | Promise<void>;

StepDataHelper

The StepDataHelper interface provides clean methods to read and modify data across steps from within onAfterValidation.

interface StepDataHelper {
  /** Replace all data for a specific step */
  setStepData(stepId: string, data: Record<string, any>): void;

  /** Merge specific fields into a step's existing data */
  setStepFields(stepId: string, fields: Record<string, any>): void;

  /** Read the current data for a step */
  getStepData(stepId: string): Record<string, any>;

  /** Set a single field value on the next step */
  setNextStepField(fieldId: string, value: any): void;

  /** Merge multiple field values into the next step's data */
  setNextStepFields(fields: Record<string, any>): void;

  /** Get all data across all steps */
  getAllData(): Record<string, any>;

  /** Get the full list of step configurations */
  getSteps(): StepConfig[];
}

Usage Patterns

onAfterValidation: async (stepData, helper) => {
  const result = await lookupCompany(stepData.registrationNumber);

  helper.setNextStepFields({
    companyName: result.name,
    address: result.address,
    industry: result.sector,
  });
}
onAfterValidation: async (stepData, helper) => {
  const result = await lookupCompany(stepData.registrationNumber);

  // Pre-fill step 3, even though we are on step 1
  helper.setStepFields('review', {
    summary: `${result.name} - ${result.address}`,
  });
}
onAfterValidation: async (stepData, helper, context) => {
  const personalInfo = helper.getStepData('personal-info');
  const allData = helper.getAllData();

  // Use data from a previous step to enrich the current call
  await enrichProfile({
    email: personalInfo.email,
    preferences: stepData,
  });
}

Here is the complete flow when the user clicks "Next":

Form validation

The current step's form is validated using its field validators. If validation fails, errors are shown and navigation is blocked.

onAfterValidation

If the step defines onAfterValidation, it is called with the validated data and the StepDataHelper. If it throws, navigation is cancelled.

Step marked as passed

The current step ID is added to the passedSteps set, indicating it has been successfully validated.

Find next visible step

The workflow scans forward from the current index and finds the next step whose visible condition evaluates to true (or has no condition). Hidden steps are skipped.

Transition

The onStepChange callback fires, the current step index updates, and the new step is marked as visited.

On this page