XSCREENSAVER / 3D PIPES

[readonly] markdown buffer

Screen Flow Component Validation Without Surprises

Jul 2, 2026 · 6 min read

A custom LWC can look valid while Flow thinks it is invalid. It can also block Next without showing why, display an error too early, or lose an error supplied by Flow.

The validation rule is rarely the difficult part. The problem is misunderstanding the contract between Flow and the component.

That contract has three methods: validate(), setCustomValidity(), and reportValidity().

The useful rule is: calculate, store, then display.

traceview://flow-validation
Service requestScreen 2 of 3

Enter a reference, then choose Next.

  1. 01
    validate()calculate internal validity
    waiting
  2. 02
    setCustomValidity()store Flow's external error
    separate
  3. 03
    reportValidity()display the right message
    not shown
Pressing Next runs the three-part contract: calculate the component's validity, retain any external Flow error separately, then display the message that currently owns the screen.

Calculate internal validity

validate() should calculate the component's own validity and return Flow's expected shape.

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 '';
}

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

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

Do not make validate() paint the UI. Keeping calculation separate makes the rules easy to test and leaves rendering to the reporting step.

Salesforce documents the lifecycle and call conditions, but the ownership split is the part worth remembering.

Store external errors separately

Flow can supply an error the component cannot calculate locally.

externalErrorMessage = '';

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

Do not overwrite internal validation state. Flow may later clear its message with setCustomValidity(''), and that must not clear an invalid local format.

reportValidity() then decides what the user should see:

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

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

This version gives the external error priority. A complex component may render both in a component-level error region. Either way, blocked navigation needs a visible explanation.

Show errors at the right time

A required field is technically invalid on first load. That does not mean the screen should open as a wall of red.

traceview://flow-validation-timing
01screen opens
Reference number
required, but not painted red
02Next is blocked
Reference number
Enter a reference number.
03user corrects it
ABC-123456
validation stays live
The component begins neutral, reports its error after a blocked navigation attempt, then keeps validation live while the user corrects the value.

Track whether the user has interacted or Flow has requested reporting. After a failed navigation attempt, keep validation live while the user corrects the value.

When an output property changes, dispatch FlowAttributeChangeEvent so Flow receives the current value. Validation and state synchronisation are related, but they are not the same contract.

import { FlowAttributeChangeEvent } from 'lightning/flowSupport';

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

Keep the component's rules local. A postcode component should validate postcodes, not decide whether an order is eligible. Process-level rules belong in Flow, Apex, or a composite component that owns all relevant inputs.

Prove the whole contract

Test the public methods rather than only the private regex. Cover internal validity, external errors, clearing an external error while an internal error remains, delayed rendering, attribute-change events, and revisiting the screen with restored values.

A well-behaved component can answer three questions:

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

Those questions map directly to the three methods.

Keep them separate, and Flow navigation stops feeling mysterious.