Log Entry

Screen Flow Component Validation Without Surprises

Jul 2, 2026 · 10 min read

A custom LWC on a Flow screen can look valid while Flow thinks it is invalid.

It can also show an error too early, lose an external error, or block Next with no visible explanation.

The problem is usually not the validation rule itself.

It is misunderstanding the contract between Flow and the component.

That contract has three public methods:

  • validate()
  • setCustomValidity(externalErrorMessage)
  • reportValidity()

They have different jobs.

Quick snapshot

  • validate() calculates and returns the component's internal validity when Flow attempts navigation.
  • setCustomValidity() stores an external error supplied by Flow.
  • reportValidity() renders the appropriate internal and external errors.
  • Internal errors belong to the component's own input rules.
  • External errors come from outside the component, such as Flow validation or another part of the screen.

Salesforce documents the lifecycle and call conditions in Validation Methods for Screen Components.

My rule is:

Calculate in validate(). Store in setCustomValidity(). Display in reportValidity().

Keep internal validity as a pure calculation

The component should have one function that can determine its current internal error.

get internalErrorMessage() {
  if (!this.value) {
    return 'Enter a reference number.';
  }

  if (!/^[A-Z]{3}-\d{6}$/.test(this.value)) {
    return 'Use the format ABC-123456.';
  }

  return '';
}

Then validate() returns Flow's expected shape:

@api
validate() {
  const errorMessage = this.internalErrorMessage;

  return {
    isValid: errorMessage.length === 0,
    errorMessage
  };
}

I do not make validate() responsible for painting the UI. Flow can call validation as part of navigation decisions, and rendering belongs in the reporting step.

Keeping calculation separate also makes it easy to unit test every validation rule.

Store external errors exactly as external errors

Flow may supply an error that the component could not calculate locally.

externalErrorMessage = '';

@api
setCustomValidity(externalErrorMessage) {
  this.externalErrorMessage = externalErrorMessage ?? '';
}

Do not immediately overwrite the component's internal error state. The two sources have different ownership and can exist at the same time.

This matters when Flow later clears its external message by calling setCustomValidity(''). Clearing the external error must not accidentally clear an internal invalid format.

Render both sources in reportValidity()

@api
reportValidity() {
  const input = this.template.querySelector('lightning-input');
  const message = this.externalErrorMessage || this.internalErrorMessage;

  input.setCustomValidity(message);
  input.reportValidity();
}

This simple version gives external validation priority. A more complex component might render separate messages in a component-level error region.

The important part is that the user sees the reason navigation is blocked.

An invalid return with no rendered message feels like a broken Next button.

Do not show a wall of red on first load

A required field can be technically invalid before the user has touched it.

That does not mean the first screen render should immediately accuse the user of leaving it blank.

Track whether the input has been changed or whether Flow has requested error reporting.

hasInteracted = false;
shouldShowErrors = false;

handleChange(event) {
  this.hasInteracted = true;
  this.value = event.detail.value;

  if (this.shouldShowErrors) {
    this.reportValidity();
  }
}

@api
reportValidity() {
  this.shouldShowErrors = true;
  // Render current errors.
}

This gives a useful interaction:

  • no premature error on load
  • immediate feedback after a failed navigation attempt
  • live correction while the user fixes the value

Tell Flow when output values change

Validation and state synchronisation are related but separate.

When an output property changes, dispatch FlowAttributeChangeEvent from lightning/flowSupport so Flow knows the current value.

import { FlowAttributeChangeEvent } from 'lightning/flowSupport';

handleChange(event) {
  this.value = event.detail.value;
  this.dispatchEvent(
    new FlowAttributeChangeEvent('referenceNumber', this.value)
  );
}

Do not rely on Flow discovering private component state during navigation.

The public property is the Flow contract. The event keeps that contract current.

Validate the component, not the whole process

A postcode component should validate postcode-shaped concerns.

It should not query whether an order is eligible, inspect an unrelated screen component, and decide the entire Flow can continue.

Cross-field or process-level rules may belong in:

  • Flow logic
  • an Apex action
  • a parent composite component that genuinely owns all relevant inputs

Keeping the component's validation local makes it reusable and prevents invisible coupling between screen elements.

Handle revisited screens

Users can move back and forward. Flow can restore output values. External errors can be set and later cleared.

Test these sequences:

  1. first visit with no value
  2. invalid input, then Next
  3. corrected input
  4. external error supplied after local input is valid
  5. external error cleared
  6. navigate back to the screen with a saved value

The component should not preserve a stale error merely because reportValidity() ran on a previous visit.

Derive validity from current inputs and current external error state.

Test the public validation API

These are public methods, so test them as public methods.

Useful Jest cases include:

  • validate() returns invalid with the expected message
  • validate() returns valid for a correct value
  • setCustomValidity() stores an external error
  • reportValidity() displays the external error
  • clearing the external error reveals any remaining internal error
  • changing the input dispatches FlowAttributeChangeEvent
  • errors are not rendered prematurely

Do not test only the private regex helper. The important behaviour is the agreement between the component and Flow.

The finished contract

A well-behaved Flow screen component can answer three questions clearly:

  1. Am I internally valid?
  2. Has Flow given me an external error?
  3. What should the user see right now?

Those questions map neatly to the three validation methods.

Keep them separate, and Flow navigation stops feeling mysterious.