SfSkills Domains Docs GitHub

Home / flow / flow-screen-input-validation-patterns

flow-screen-input-validation-patterns

Design input validation inside Screen Flow screens using component-level <validationRule> (formula + errorMessage), isRequired, cross-field rules on the second field, reactive components, and screen-level Decision fallbacks for cross-screen checks. Trigger keywords: screen flow validation rule, EndDate after StartDate validation in flow screen, block Next button until input valid. NOT for record-level Validation Rules — use admin/validation-rules. NOT for choosing screen components or overall screen UX — use flow/screen-flows.

Open the skill folder on GitHub


Flow Screen Input Validation Patterns

Activate this skill any time a Screen Flow captures user input that must satisfy a format, range, regex, or cross-field constraint before the user clicks Next. The skill is the contract between agents/flow-builder/AGENT.md (which decomposes screens and assigns "validation per screen") and agents/flow-analyzer/AGENT.md (which audits screen flows that allow junk data into the system because they have no inline validation).


Before Starting

Gather this context before designing or auditing any screen-flow input:

The most common wrong assumption: practitioners think a Decision element placed after the input screen is "validation". It is not — the user has already clicked Next. Real validation blocks Next inline.


Core Concepts

Concept 1 — Component-level <validationRule>

Every Screen Input Component (Text, Number, Date, Date/Time, Currency, Picklist, Checkbox, Long Text Area, etc.) accepts a single <validationRule> element with two children:

The component's own value is referenced inside the formula via {!ScreenComponentApiName}. The formula re-evaluates when the user clicks Next; it does not re-evaluate on every keystroke unless the component is reactive.

<fields>
    <name>Email</name>
    <dataType>String</dataType>
    <fieldType>InputField</fieldType>
    <isRequired>true</isRequired>
    <validationRule>
        <formulaExpression>AND(
            LEN({!Email}) &gt; 5,
            CONTAINS({!Email}, "@"),
            CONTAINS({!Email}, ".")
        )</formulaExpression>
        <errorMessage>Enter a valid email address (must contain @ and a domain).</errorMessage>
    </validationRule>
</fields>

The two failure modes that bite in production:

Concept 2 — isRequired vs <validationRule> (and why both are needed)

isRequired = true blocks Next when the input is empty. It does not validate format. An empty Email field with isRequired = true → blocked. The string "asdf" in the same field → allowed (because non-empty), unless a <validationRule> also enforces format.

NeedisRequired<validationRule>
Block empty valuesYesOptional (formula could check NOT(ISBLANK(...)))
Block malformed valuesNoYes
Block out-of-range valuesNoYes
Cross-field checkNoYes (on the second field)

Use isRequired for required-ness and <validationRule> for format. They are complementary, not interchangeable.

Concept 3 — Re-evaluation timing: Next-click vs reactive

Default behaviour: validation rules evaluate only when the user clicks Next. The user can type a malformed value, tab away, and not see an error until they hit Next. This is fine for short forms. For long forms it's bad UX — the user fixes one error, clicks Next, sees the next error, clicks Next, sees the third.

Reactive Screen Components (Winter '24+) re-evaluate validation on each component change. Adopt them when:

Reactive validation is opt-in per component; standard input components support it natively, custom LWCs must implement reactivity via FlowAttributeChangeEvent and the @api validate() hook.

Concept 4 — Component-level vs screen-level vs record-level

Three tiers, each with a clear scope:

TierLives inScopeTriggers
Component-level<validationRule> on the inputSingle input (or cross-field on this screen)Click Next, or live if reactive
Screen-levelDecision element after the screenCross-screen logic (uses prior-screen variables)When Decision is reached
Record-levelValidation Rule on the objectAlways, regardless of UIDML (Update Records / Create Records)

The order of preference is component → screen → record. Component-level catches errors closest to the user, screen-level handles dependencies the input doesn't know about, record-level is the last-resort safety net.


Common Patterns

Pattern 1 — Per-Input Inline Validation

When to use: Standard input field with a single-field constraint (regex, range, length).

How it works:

  1. Set isRequired = true if the field is mandatory.
  2. Add <validationRule> with a formula returning BOOLEAN.
  3. Reference the component value as {!ComponentApiName}.
  4. Write a user-facing errorMessage (no jargon, no formula syntax).
<validationRule>
    <formulaExpression>AND(
        ISNUMBER({!Age}),
        VALUE({!Age}) &gt;= 18,
        VALUE({!Age}) &lt;= 120
    )</formulaExpression>
    <errorMessage>Age must be a number between 18 and 120.</errorMessage>
</validationRule>

Why not the alternative: Putting this in a Decision after the screen forces the user to click Next, see a screen-level error message (or worse, a fault path), then click Back. Component-level keeps the error attached to the field that caused it.

Pattern 2 — Cross-Field on the Second Field

When to use: Two fields on the same screen with a relational constraint (EndDate > StartDate, ConfirmEmail = Email, Quantity * UnitPrice <= AccountCreditLimit).

How it works:

Put the validation rule on the second field (the one whose validity depends on the other). When the user clicks Next, both fields are populated, and the rule sees both values.

<!-- StartDate field: no cross-field rule needed -->
<fields>
    <name>StartDate</name>
    <dataType>Date</dataType>
    <isRequired>true</isRequired>
</fields>

<!-- EndDate field: rule references both -->
<fields>
    <name>EndDate</name>
    <dataType>Date</dataType>
    <isRequired>true</isRequired>
    <validationRule>
        <formulaExpression>{!EndDate} &gt; {!StartDate}</formulaExpression>
        <errorMessage>End Date must be after Start Date.</errorMessage>
    </validationRule>
</fields>

Why not the alternative: Putting the rule on StartDate doesn't work — when the user is filling in StartDate, EndDate may still be empty, so the rule errors or short-circuits. The rule belongs on the dependent field.

Pattern 3 — Reactive Validation for Multi-Step Forms

When to use: Long forms (5+ fields), or forms where the user benefits from live feedback (password strength, IBAN check, dependent picklists).

How it works:

  1. Confirm Reactive Screens is enabled (default on Winter '24+ orgs).
  2. Use standard reactive input components (Text, Number, Date, etc.).
  3. The <validationRule> formula is unchanged — Flow re-evaluates it on each change.
  4. For a custom LWC, implement @api validate() returning {isValid: boolean, errorMessage: string} — this is the contract the Flow runtime calls.
// custom LWC screen component
import { LightningElement, api } from 'lwc';

export default class IbanInput extends LightningElement {
    @api value;

    @api
    validate() {
        if (!this.value || !this._isValidIban(this.value)) {
            return { isValid: false, errorMessage: 'Enter a valid IBAN.' };
        }
        return { isValid: true };
    }

    _isValidIban(v) { /* check digits */ return true; }
}

Why not the alternative: Without reactivity, the user types the entire form, clicks Next, then plays whack-a-mole with one error per click.

Pattern 4 — Screen-Level Decision for Cross-Screen Validation

When to use: Validation that needs data from a previous screen (e.g. "the Account Tier picked on Screen 1 limits the Quantity on Screen 2").

How it works:

Put a Decision element after the second screen with the cross-screen check. If invalid, route back to the second screen via a Loop or via re-rendering the screen with an error variable.

[Screen 1: pick Account Tier] → [Screen 2: enter Quantity]
        → [Decision: Quantity within Tier limit?]
              ├── Yes → continue
              └── No  → [Screen 2 again, prepopulated, with error variable shown via display text]

This is the only legitimate use of Decision-as-validation. Component-level can't do it because the component on Screen 2 doesn't have direct access to the Screen 1 variable in its formula context (it does, but the Decision is clearer and easier to maintain for this case).


Decision Guidance

Validation shapeRecommended approachReason
Single-field format (email, regex, range)Component-level <validationRule>Closest to the user, blocks Next inline
Field is requiredisRequired = true (combined with <validationRule> for format)isRequired handles emptiness; format is separate
Cross-field on same screen (EndDate > StartDate)<validationRule> on the second (dependent) fieldBoth values are populated when Next is clicked
Cross-screen (Quantity limited by Tier picked earlier)Decision element after the dependent screen, route back on failureCross-screen variable scope is clearer in a Decision
Live feedback while typingReactive Screen Components (Winter '24+) + standard <validationRule>Avoids click-Next-fix-click-Next cycle
Custom UI component (e.g. IBAN, postcode lookup)Custom LWC implementing @api validate()Standard <validationRule> doesn't apply to custom LWCs
Defence-in-depth against API-level insertsObject-level Validation Rule (in addition to component-level)The flow's screen validation isn't run during Apex / API DML

  1. Inventory inputs per screen. For each Screen element, list every input component, its type, and whether it's isRequired. Save as a table.
  2. Define the validation contract per input. For each input, write the boolean condition that means "valid" and the user-facing error message. If the condition references another field, note whether that field is on the same screen (cross-field) or earlier (cross-screen).
  3. Translate each contract to <validationRule> XML or LWC @api validate(). Use templates/flow/RecordTriggered_Skeleton.md style as a reference for XML shape; the formula language is the same as Validation Rules.
  4. Place cross-field rules on the second (dependent) field. Verify that the formula references both fields with {!FirstField} and {!SecondField}.
  5. Place cross-screen checks in a Decision after the dependent screen, with a route-back path. Do not try to cram them into a per-input rule.
  6. Confirm reactivity if relevant. If the org is on Winter '24+ and the form is long or interdependent, ensure standard reactive components are used; for custom LWCs, confirm they emit FlowAttributeChangeEvent and implement @api validate().
  7. Add defence-in-depth and verify accessibility. Add object-level Validation Rules for any field the flow writes to (so Apex/API inserters can't bypass the flow). Confirm standard inputs associate errors via aria-describedby automatically and custom LWCs do the same (see flow-screen-flow-accessibility).

Review Checklist

Run through these before marking screen-flow validation work complete:


Salesforce-Specific Gotchas

  1. The formula must return BOOLEAN, not Text or Number. A formula returning the error message as text — IF(LEN({!Email}) > 5, "", "Email too short") — does not validate; it always returns a string, which is truthy, and Next is allowed regardless. Use LEN({!Email}) > 5 directly.
  2. isRequired does not validate format. A field marked isRequired = true allows the string " " (a single space) — non-empty, so the requirement is satisfied. Add a <validationRule> like LEN(TRIM({!Field})) > 0 if you need true non-blank enforcement.
  3. Decision-after-screen is bad UX, not a validation pattern. When the Decision fires, the user has already clicked Next — the only way back is a manual Back click or a programmatic re-render. Component-level rules block Next inline before the user moves on.
  4. Validation re-evaluates on Next, not in real-time, unless the component is reactive. Pre–Winter '24 components and non-reactive flows hold all errors until the click. If the user expects live feedback, you need reactive components.
  5. Custom LWC screen components don't inherit <validationRule>. Authors copy-paste a <validationRule> element under a custom component's <extensionName> block — it is silently ignored. Custom LWCs must implement @api validate() returning {isValid: boolean, errorMessage: string}; the Flow runtime calls this on Next.
  6. Cross-field rule on the wrong field short-circuits. Putting EndDate > StartDate on the StartDate field means when the user enters StartDate first (with EndDate still null), the formula compares against null and the result is unpredictable. Always put the rule on the dependent (later) field.
  7. Object-level Validation Rules still fire when the flow does Update Records. A flow's screen validation doesn't replace the object's Validation Rule — both run. If they contradict, the user passes the flow validation and then the object Validation Rule throws, surfacing as a flow runtime error. Keep the two formulas consistent.
  8. Picklist validation must compare against the API name, not the label. {!Status} = "Closed Won" works only if "Closed Won" is the API name. Translated labels in the user's locale won't match. Use the Decision builder's picklist-aware compare or hardcode the API name.

Output Artifacts

ArtifactDescription
Validated input componentsPer-input <validationRule> blocks with boolean formula + user-facing error message, plus isRequired where applicable.
Cross-field rules on the second fieldValidation formulas on the dependent field referencing both fields, fired on Next.
Screen-level Decision fallbackDecision element after the dependent screen for cross-screen checks, with a route-back-to-screen path on failure.
Custom LWC @api validate() contractsFor custom screen components, the JS hook returning {isValid, errorMessage} plus FlowAttributeChangeEvent for reactivity.
Defence-in-depth Validation RulesObject-level Validation Rules covering the same fields, in case an Apex / API inserter bypasses the flow.

Official Sources Used

Package files