rilaykit
Workflow

Navigation

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

Workflow navigation lives on the useFlow() hook: forward/backward movement, skipping, validation gating, conditional visibility, and cross-step data.

All methods come from useFlow() and return Promise<boolean> (did the navigation succeed).

import { useFlow } from '@rilaykit/workflow/react';

function MyNavigation() {
  const { goNext, goPrevious, goToStep, skipStep, canGoNext, canGoPrevious } = useFlow();

  return (
    <div className="flex gap-2">
      <button onClick={() => goPrevious()} disabled={!canGoPrevious()}>Back</button>
      <button onClick={() => skipStep()}>Skip</button>
      <button onClick={() => goNext()} disabled={!canGoNext()}>Next</button>
      <button onClick={() => goToStep(0)}>First step</button>
    </div>
  );
}
MethodSignatureDescription
goNext()() => Promise<boolean>Runs onAfterValidation, marks the step passed, then advances to the next visible step.
goPrevious()() => Promise<boolean>Goes to the previous visible step. No validation.
goToStep(index)(stepIndex: number) => Promise<boolean>Jumps to a step by its original index. Fails if hidden or out of bounds.
skipStep()() => Promise<boolean>Skips without validation when the step is skippable. Fires onStepSkip. Skipping the last visible step completes the workflow.

Field validation belongs to the step's form: <FlowNext> submits the form (validating per its mode/reValidateMode) and only calls goNext() on success. Calling goNext() directly does not run field validation.


Guards check whether an action is possible — use them to disable buttons.

GuardReturns true when
canGoNext()A visible step exists after the current one.
canGoPrevious()A visible step exists before the current one.
canGoToStep(index)The target step is in bounds and visible.
canSkipCurrentStep()allowSkip resolves to true or the skippable condition is true.

Automatic Step Skipping

Hidden steps are skipped transparently: goNext() and goPrevious() land on the nearest visible step in their direction.

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 itself becomes hidden (a condition changed while viewing it), the workflow relocates to the nearest visible step — forward first, then backward.


Step Conditions

Conditions control visibility and skippability from workflow data, using the when() builder from @rilaykit/core.

interface StepConditionalBehavior {
  visible?: ConditionConfig;   // false = hidden and skipped over
  skippable?: ConditionConfig; // true = step can be skipped
}
import { when } from '@rilaykit/core';

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

You can also attach conditions after the fact with .addStepConditions(stepId, conditions).

Conditions evaluate against a flattened view of all workflow data: fields from any step are addressable by field ID when evaluating conditions on another step.

Skipping rules

A step is skippable when allowSkip resolves to true or its skippable condition does. allowSkip is a boolean or a predicate over the collected data:

allowSkip: ({ allData }) => allData.plan === 'free'

Skipped steps are absent from the completion payload and listed in meta.skippedSteps. Navigating back onto a skipped step lets you skip it again — or complete it, which removes it from the skipped set. <FlowSkip> renders only when the current step is skippable.


The onAfterValidation Callback

Runs after the step's form passes validation, before the transition. Use it for API calls on the validated data, pre-filling later steps, or external checks.

.step({
  id: 'registration',
  title: 'Business Registration',
  formConfig: registrationForm,
  onAfterValidation: async (stepData, helper, context) => {
    const companyInfo = await fetchCompanyBySiren(stepData.siren);

    // Pre-fill the next visible 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, navigation is cancelled and the user stays on the current step (the error is also reported to analytics/monitoring).

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

StepDataHelper

Read and write data across steps from within onAfterValidation. The setNextStep* methods target the next visible step — a conditionally hidden step in between never swallows the prefill.

interface StepDataHelper {
  setStepData(stepId: string, data: Record<string, any>): void;   // replace a step's data
  setStepFields(stepId: string, fields: Record<string, any>): void; // merge fields into a step
  getStepData(stepId: string): Record<string, any>;
  setNextStepField(fieldId: string, value: any): void;
  setNextStepFields(fields: Record<string, any>): void;
  getAllData(): Record<string, any>;
  getSteps(): StepConfig[];
}

When the user clicks Next:

  1. Form validation — the step's form validates; errors block navigation.
  2. onAfterValidation — called with the validated data and helper; a throw cancels navigation.
  3. Step marked passed — the step ID joins the passedSteps set.
  4. Find next visible step — hidden steps are skipped over.
  5. TransitiononStepChange fires, the index updates, the new step is marked visited. If no visible step remains, the workflow completes instead.

On this page