Skip to content

Validation rule expressions

Validation rules are object-level checks that run on entity instance create, update, and import. A rule that applies to Record merge decides instead which records may be merged; see Record merge rules. Each enabled rule holds one boolean expression: it must return TRUE for the record to save. A FALSE or unknown result blocks the save and returns the rule’s failure message. Rules run in ascending sort order, before any C# business logic, and the first failing message is returned.

  • Use a field’s RequiredWhen condition when the only requirement is that one field has a value.
  • Use a validation rule for everyday consistency checks that describe the record as a whole — required-if rules, ranges, date ordering, text quality, first-level reference checks, regex checks — or when the check compares multiple values, checks a reference, or needs a tailored failure message.
  • Use a C# validation function when the logic needs orchestration or goes beyond the expression profile below.

A validation expression can use:

  • scalar fields of the record’s primary table;
  • first-level reference paths such as Customer.DisplayName;
  • the variables @currentUserId, @today, @today+7d, and @today-7d;
  • the shared operators — comparisons, AND/OR/NOT, null/empty checks, text operators, IN, NOT IN, BETWEEN;
  • the validation-only function MATCHES(value, "regex") (the supported .NET-compatible subset, case-sensitive by default, leading (?i) for ignore-case);
  • Classifier categories through the portable CATEGORY(...) literal.

Not supported: EXISTS, ANY, ALL and COUNT collection forms, aggregate functions, raw SQL, inverse-reference traversal, and deep reference chains.

MATCHES(Code, '^[A-Z]{3}-[0-9]{3}$')
DueDate IS NULL OR DueDate >= StartDate
DueDate IS NULL OR DueDate >= @today
Amount <= Customer.CreditLimit
Capability = CATEGORY('product-area', 'operations', 'platform')

Classifier fields support =, !=, IN, NOT IN, IS NULL, and IS NOT NULL. Exact and set comparisons do not match null. Field-to-field equality or inequality requires the same Classifier Catalog. Classifier fields are supported on the record and through a first-level Reference path. In the rule editor, place the cursor in the expression and choose Insert category to insert a portable CATEGORY(...) value; category names and breadcrumbs are for display only and are not used for matching. The logical root of a Classifier Catalog is not a category value, while stored non-leaf categories are valid values. See Classifier category values.

Saved validation rules execute in PostgreSQL. MATCHES accepts only patterns whose behavior agrees with the .NET culture-invariant regex semantics: characters, dot, character classes (including subtraction), character classes such as \d, \w, \s and Unicode categories, anchors, capturing and non-capturing groups, alternation, and admitted greedy/lazy quantifiers. A leading (?i) applies ignore-case to the whole pattern.

Unsupported constructs return moltaro.dsl.regex.unsupported: lookarounds, backreferences, balancing groups, word boundaries, other inline options, non-BMP pattern characters, and unsafe repetition shapes. Patterns are bounded to 1,024 characters, repetition counts of 255, 512 expanded atoms, 64 distinct character sets and eight internal anchors. Counted repetitions of groups that can match empty text are rejected. A malformed .NET pattern returns moltaro.dsl.regex.invalid. Always validate the exact pattern on the server.

The DSL string parser consumes backslashes before the regex parser sees them:

MATCHES(Code, '^\\d{3}$')
MATCHES(Email, '(?i)@example[.]com$')
NOT (MATCHES(Code, '^temporary'))

In JSON, the first expression is {"Expression":"MATCHES(Code, '^\\\\d{3}$')"}. A single DSL backslash before d or . does not preserve a regex escape. Parenthesize MATCHES when negating it. A null input yields unknown; use Code IS NULL OR MATCHES(...) when missing text should pass.

Message is the required fallback failure-message template, with optional per-locale translations (en, uk, de, pl, es). Templates use the same { … } placeholder style as presentation templates and can render:

  • record fields: {Name}, first-level paths: {Customer.DisplayName};
  • system values: {Number}, {DisplayName}, {Rule.Name}, {Rule.DisplayName}, {CurrentUserId}, {Today}.

A field referenced only in a failure message still counts as a schema dependency: it blocks field deletion and incompatible type changes the same way an expression reference does.

Validate the expression before saving with the validate-expression operation — the response reports validity plus structured errors (message, token, field path, and stable code). Rules are managed through the validation-rule endpoints of the Configuration API; reordering takes the full active rule set with row versions.

Creating or changing an expression that uses CATEGORY(...) requires Catalog Reader or management access to the referenced Classifier Catalog. A saved rule continues to validate records if that access is later removed, and its name, message, enabled state, and other metadata can still be updated while the expression remains unchanged.

Disabled rules stay stored but are not evaluated.

A validation rule with Applies to: Record merge (Scope: 1 in the API; the default 0 is every save) is not checked on save. It is checked when two records of the type are merged, and every enabled merge rule must return TRUE. Without enabled merge rules, merging works as before. Existing rules keep applying to saves.

The expression reads three versions of the record type’s own fields:

  • Source: the record that is archived;
  • Survivor: the record that is kept, as it was before the merge;
  • Result: the survivor with the values the merge writes.
Source.Beneficiary = Survivor.Beneficiary AND Source.Direction = Survivor.Direction
Result.Status IS NULL OR Result.Status <> 'Closed'
Source.Region IS NULL AND Survivor.Region IS NULL OR Source.Region = Survivor.Region

References compare by record identity, not by name, and cannot be followed: Source.Customer.Name is not valid. Functions, variables, MATCHES, Classifier fields and collection forms are not available. An empty value follows the usual rules: Source.Region = Survivor.Region is not TRUE when both are empty, so write the third form above when two empty values should be accepted. Choosing the other record as the survivor swaps Source and Survivor.

The failure message is shown to the person merging. It can use only {Rule.Name} and {Rule.DisplayName}, never record values. If a rule reads a field that the person cannot read, the merge is refused with a general message and the rule is not evaluated, so the answer reveals nothing about the hidden values.

Merge rules apply in the merge wizard, to the merge API and to C# merges. Result rules are checked for the values selected in the wizard, before any save function runs, and again for the saved record after the application’s save functions, so a function cannot bypass them. A rule must therefore hold for the selected values too; it cannot rely on a value that only a save function sets. Configure rules in the Constructor on the record type’s Validation rules tab: choose Applies to: Record merge, write and check the expression, and save. Through the API, send Scope: 1 when you create a rule and to the validate-expression operation. See Record merge API for the refusal codes.