Skip to content

Validation Functions

Validation Functions allow us to write conditional logic about whether a property is “valid” based on the current state of an entity or its related entities.

Verify every slug against the live Type before saving a validation

A validation that reads obj.<slug> returns undefined when the slug is misspelled or does not exist on the Type — there is no error. The branch that depends on that slug silently never fires, and the validation effectively becomes a no-op for the missing field. Before deploying, confirm every slug the function references on the actual Type definition.

Customer orgs frequently prefix slugs (wcc..., vrd..., ptc...) and may carry near-duplicates with subtly different meanings (e.g. vrdWholesaleLaunchDate vs wccWholesaleLaunchDate). Match the spec to the actual slug on the Type, not to the friendly label in the requirement. Field names that read as one word in a spec ("launchdate") are typically two separate slugs in the Type.

Writing Validation Functions

Validation Functions are currently ONLY being evaluated on the Plan Placeholder, Item, and Project Item entities.

Assortment Item is not supported

Validation functions cannot be attached to properties on the AssortmentItem entity. If you need to enforce a rule that should fire in a Plan context, attach it to plan-placeholder instead.

Available Inputs

Plan Placeholder

Input Variables Available Context Notes
obj
{
  ...[plan-placeholder properties]

  itemFamily: {
    ...[item properties]

    projectItem: {
     ...[project-item properties]
    }
  },
  itemOption: {
    ...[item properties]

    projectItem: {
     ...[project-item properties]
    }
  },
  itemType: {
   id: string,
   typePath: string,
   label: string,
   slug: string,
   parentId: string,
   typeRootId: string
  },
}
context
{
  assortment: {
   ...[assortment properties]
  }
}
assortment is the Target Assortment of the Plan.

Item

Input Variables Available Context Notes
obj
{
  ...[item properties]

  roles: Array<'family' | 'option' | ...>
}
roles is a system property used to determine if an item is “option” or a “family”. Use this to role-gate a validation — only run a check on options, or only on families. See Role-gated validation in Common Patterns.
context N/A context is not accessible here like it is for Formulas.

Family vs. Option property behavior

A validation attached to a family-level property runs on both Family items and Option items (Options inherit family-level property data). A validation attached to an option-level property runs on Option items only.

Project Item

Input Variables Available Context Notes
obj
{
  ...[project-item properties]

  itemId: string,
  roles: Array<'family' | 'option' | ...>,
}
obj.item is not accessible on the Project Item entity like it is for Formulas. Only obj.itemId (a string ID) is available — you cannot read Item properties from inside a Project Item validation. If you need to check Item properties together with Project Item properties, attach the validation to plan-placeholder instead.
context N/A context is not accessible here like it is for Formulas.

Object Reference Hydration

Object Reference properties defined on a Type are expanded and visible to the validation function 1-level deep.

obj.objRefProperty1.name

obj.objRefProperty1.objRefProperty2.name

Output

The expected output of a validation function is an array of objects with the following structure:

Array<{ message: string, type: 'ERROR' | 'WARNING' }>

Warning

A type of 'WARNING' will display an alert message in the Plan.

Error

A type of 'ERROR' will prevent the update from being saved.

Common Patterns

Single warning

const validations = [];

if (!obj.category) {
  validations.push({ message: 'Category is required.', type: 'WARNING' });
}

return validations;

Mixed WARNING and ERROR

A single validation function can return any combination of warnings and errors.

const validations = [];

if (!obj.msrp) {
  validations.push({ message: 'MSRP is required before submission.', type: 'WARNING' });
}

if (obj.msrp < 0) {
  validations.push({ message: 'MSRP cannot be negative.', type: 'ERROR' });
}

return validations;

Object reference property check

Object reference properties are hydrated 1-level deep, so you can read scalar properties off the referenced entity directly.

const validations = [];

if (obj.colorway?.status === 'DISCONTINUED') {
  validations.push({ message: 'Selected colorway is discontinued.', type: 'WARNING' });
}

return validations;

Role-gated validation (Item)

Use obj.roles on the Item entity to apply different checks to Family items vs Option items.

const validations = [];

// Only require UPC on options — families don't carry a UPC
if (obj.roles?.includes('option')) {
  if (!obj.upc) {
    validations.push({ message: 'UPC is required on options.', type: 'ERROR' });
  }
}

return validations;

Plan Placeholder — reading from Item or Project Item

A Plan Placeholder validation can read the hydrated itemOption / itemFamily and their nested projectItem.

const validations = [];

const item = obj.itemOption || obj.itemFamily;
const projectItem = obj.itemOption?.projectItem || obj.itemFamily?.projectItem;

if (!item?.category) {
  validations.push({ message: 'Category is missing on the item.', type: 'WARNING' });
}

if (projectItem?.status === 'DROPPED') {
  validations.push({ message: 'This item has been dropped.', type: 'ERROR' });
}

return validations;

Plan Placeholder — using assortment context

context.assortment on a Plan Placeholder validation gives you access to the target assortment's properties.

const validations = [];
const item = obj.itemOption || obj.itemFamily;

if (context.assortment?.season && !item?.season) {
  validations.push({ message: 'Item season does not match assortment.', type: 'WARNING' });
}

return validations;

Cross-Entity Visibility

A validation function only sees one entity's obj — there is no automatic cross-link from Item to Project Item or vice versa.

Validation attached to Can read Cannot read
item Any property on the item via obj.<slug>; object references 1-level deep Project Item state — there is no cross-link
project-item Any property on the project item via obj.<slug>; obj.itemId (string ID only) Item properties — obj.item is not hydrated here
plan-placeholder Plan Placeholder properties + obj.itemOption.<itemSlug> + obj.itemOption.projectItem.<projectItemSlug> (and the same for itemFamily); context.assortment

Practical consequence. If a rule depends on properties from more than one entity (e.g. a trigger field on the Project Item and required attributes on the Item), neither single-entity validation can see both sides. Attach the rule to plan-placeholder — it denormalizes both — and accept that the rule will only enforce in Plan contexts. If Item Modal / Boards Property Editor enforcement is also required, the missing field must be denormalized onto the trigger entity at the data-model level.

Plan Placeholder Mirror Pattern

item and project-item validations do not run on Plan cell edits and do not block Plan Publish. Only plan-placeholder validations do.

When a rule must be enforced inside a Plan, mirror the validation onto the matching plan-placeholder slug. The Plan Placeholder type typically has the same slugs denormalized, so the JavaScript body is usually identical — only the attach point changes.

Surface Item validation runs? Project Item validation runs? Plan Placeholder validation runs?
Item Modal ✅ blocks save
Boards Property Editor ✅ blocks save ✅ blocks save
Plan cell edit ✅ (ERROR blocks; WARNING shows)
Plan Publish ✅ (both WARNING and ERROR block)

Patterns for State-Dependent Required Fields

A specification like "fields X, Y, Z are required when status = Released" can mean two different things. Confirm with the spec owner before implementing, because the attach point and JS shape are completely different.

Pattern Spec phrasing Fires when Attach to Error surfaces under
Per-property (continuous) "While in Released state, X must be filled." Field-level required-ness, persistent. Save attempt while the trigger is in the target state, on any of the listed fields. Each required field's slug. The actual missing field (good UX).
State-transition gate (discrete) "You can't move to Released unless X, Y, Z are filled." Block on the transition itself. Save attempt that sets the trigger to its target value. The trigger property's slug. The trigger field (acceptable: user is editing it anyway).

Per-property pattern

Generate one validation function per required field, each attached to that field's slug. Each function checks the trigger condition and its own field's emptiness.

// Attached to: age
const v = [];

if (obj.lifecycleStage === 'development' && obj.productType === 'inline') {
  if (!obj.age) {
    v.push({ message: 'Age is required at Ready For PLM stage.', type: 'ERROR' });
  }
}

return v;

State-transition gate pattern

Attach one validation function to the trigger property. The function reads the trigger's incoming value, branches on any productType / dimension filters, collects every empty required field, and emits a single ERROR listing them all.

// Attached to: lifecycleStage
const v = [];
const target = obj.lifecycleStage;
const pt = obj.productType;

if (target === 'development' && pt === 'inline') {
  const missing = [];
  if (!obj.age) missing.push('Age');
  if (!obj.brand) missing.push('Brand');
  if (!obj.sbu) missing.push('SBU');

  if (missing.length) {
    v.push({
      message: `Cannot set Ready for PLM to 'Development' for productType=inline. Missing: ${missing.join(', ')}.`,
      type: 'ERROR'
    });
  }
}

return v;

When the same trigger has different requirements per dimension (productType, brand, etc.), branch inside the validation by reading those fields off obj — don't generate multiple validations on the same slug.

When Validations Are Processed

In Plan

Plan Placeholder validation functions run in the Plan:

  • On load of a Plan.
    • Plan Placeholder validations are run on all rows.
  • On cell edit.
    • Plan Placeholder validations run for the modified row(s).

In Boards Property Editor

Item and Project Item validation functions run in the Property Editor in Boards:

  • On Property Editor open.
  • On property update in Property Editor.

In Item Modal

Item validation functions run in the Item Modal:

  • On Item Modal open.
    • Item validations run for every Item Property on the entity.
  • On Item update.
    • Item validations run for every Item Property on the entity.

On Publish (API)

On publish, all Plan Placeholder validation functions run for every row of the Plan. If any validation warnings or errors exist, the Publish fails.

Neither Item nor Project Item validation functions run on Publish.

Existing Gaps in Validation Functions

Validation Functions Are Not Run On Create or Update Operations Server Side

An entity, such as an Item, can be edited outside of one of the apps (e.g. with an API request), which results in an ERROR validation state. This state should not be saved, but because we do not run validations server-side, it is saved.

In Plan, Item or Project Item Validations Are Not Run On Cell Edit

Outside of the Item modal, the plan depends entirely on the Plan Placeholder Validation Functions for evaluating warnings and errors.

If Item and Plan Placeholder Validations have different outputs, an update operation could be blocked in one location, but not the other

Example - Plan Cell Edit vs Item Modal
  • Plan Cell Edit.
    • Plan Placeholder Validation Function returns [] (no validation alerts).
    • Update is allowed and no alerts are displayed.
  • Plan/Boards/Showcase Item Modal.
    • Item Validation Function returns ERROR validation alert.
    • Update is blocked.

In Boards and Item Modals (all apps), edits are blocked which results in ANY validation (WARNING or ERROR), in Plan cell editing, only ERROR validations are blocked from being saved.

Editing Items and Project Items in Boards is a relatively newly introduced functionality. It's not yet defined how alerts will be displayed in Boards, currently any edit results in a validation ERROR or WARNING in Boards is blocked from being saved.