Where grid validation lives

A Workday Extend grid has two built-in controls: required on the widgets inside its cells, and limits on removing rows (minOccurs, doNotRemove, doNotRemoveRow). Everything else is PMD Scripting. Check a single cell in that cell's onChange, and check the whole grid in the page's onSubmit. Both report problems with setError on a widget. Workday notes that the basic widget methods, including setError, clearError and setRequired, also work on widgets inside a grid column's cellTemplate.

When onSubmit calls setError or setWarning on any widget, the page shows the message and calls none of its outbound endpoints. That is what makes it the right place for the last check before rows are posted.

Required cells

Set required on the widget in the cellTemplate, just as on a standalone widget:

{
  "type": "column",
  "columnId": "itemColumn",
  "id": "itemColumn",
  "label": "Item",
  "cellTemplate": {
    "type": "instanceList",
    "id": "itemCell",
    "label": "Item",
    "required": true,
    "values": "<% items.data %>",
    "valueOutBinding": "postLines.data[].item.id"
  }
}

Give the cell widget a label too. Workday's best practices say any required field on an edit page needs one, for usability and accessibility.

To make a cell required only in some rows, call setRequired from the onChange of the cell it depends on. For example, make a Justification cell required when Quantity is over 5:

"onChange": "<% justificationCell.setRequired(quantityCell.value > 5); %>"

An onChange handler inside a grid refers to the other cells of the same row by their cellTemplate id. That's why Workday recommends giving every cell template an id.

Set defaults for new rows, for example "value": "<% rowdata.quantity ?? 1 %>". PMD Scripting returns an error when an operation meets a null value, and a new row's cells can be null until the user types.

A minimum number of rows

Two built-in settings stop users from deleting rows below a floor:

  • minOccurs sets the minimum number of rows on an edit grid. Once the count drops to the minimum, no more rows can be deleted.
  • doNotRemove takes a binding, so you can compute the rule. Workday's example is "doNotRemove": "<% size(locationGrid.rows) <= 2 %>".

These control deletion. They don't make a grid with too few rows fail on submit, for example a grid that loaded with no rows and was never filled in. Add that check to onSubmit. Because the grid itself has no setError method, show the message on a widget next to it. A number widget that displays the line count works well:

{
  "type": "number",
  "id": "lineCount",
  "label": "Number of lines",
  "enabled": false,
  "value": "<% lineGrid.rows.size() %>"
}

Keep it current with the grid's onRowAdd and onRowRemove events. Workday's grid documentation uses the same pattern for a subtotal:

"onRowAdd": "<% lineCount.value = lineGrid.rows.size(); %>",
"onRowRemove": "<% lineCount.value = lineGrid.rows.size(); %>"

Duplicate rows

Check for duplicates in two places. Do it in onChange so the user sees the problem on the row they just changed, and in onSubmit because a row can become a duplicate through another row's edit.

In the cell's onChange, count the rows that hold the same value. For an instanceList cell, the value is a list of IDs, so compare the first one:

"onChange": "<%
  var picked = self.value[0];
  var matches = lineGrid.rows.filter(row => row.itemColumn.value[0] == picked);
  if (matches.size() > 1) {
    self.setError('This item is already on another line.');
  } else {
    self.clearError();
  }
%>"

If the cell's options depend on another cell in the row, see cascading instance lists.

row.itemColumn reaches the cell through the grid row's get(childId) method. When a column's cellTemplate holds a single tag, you address it by the column's ID. If the cell is wrapped in a container such as a fieldSet, for example to carry a hidden ID, the path continues through the container's id to the child, such as row.itemColumn.itemFieldSet.itemCell.

In onSubmit, compare the number of values with the number of distinct values:

"onSubmit": "<%
  var ids = lineGrid.rows.map(row => row.itemColumn.value[0]);
  if (lineGrid.rows.size() < 1) {
    lineCount.setError('Add at least one line.');
  } else if (ids.size() != ids.distinct().size()) {
    lineCount.setError('Two or more lines have the same item. Remove the duplicates.');
  }
%>"

When a duplicate means two columns together, such as the same item and Cost Center, build a combined key in the map, for example row.itemColumn.value[0] + '|' + row.costCenterColumn.value[0].

Keep the handlers cheap. Workday limits a script execution to 5 seconds of CPU time and recommends find over filter when you need only the first match. For large grids, Workday suggests turning the list into a map keyed by the value you search on.

Clear errors, or the page stays stuck

Once onSubmit has set an error, the user can't resubmit, and onSubmit won't run again until the error is cleared. Users don't edit lineCount, so they have no way to clear an error on it. Clear it yourself in the handlers that fix the cause. Call lineCount.clearError() in onRowAdd, in onRowRemove, and in the cell onChange once the duplicate is gone. Otherwise the user fixes the grid and still can't press OK.

For rules the user may override, use setWarning instead of setError. In onSubmit, event.ignoreWarnings() lets the submission continue despite warnings. Errors can't be ignored.

Page checks don't protect the data

Every check above runs in the page. A record posted another way, through the business object's REST API, a bulk load or an orchestration, never sees them. If a duplicate would break something downstream, repeat the rule where the data is processed. For example, have the orchestration that uses the lines refuse duplicate keys, or check them before an approval step. This is a design recommendation, not a Workday control.

Test it as a user would

On the deployed page, try each failure on purpose. Add two lines with the same item. Submit with no lines. Leave a required cell empty. Raise a quantity past the conditional threshold. Each should block OK with its message, and after you correct it, OK should submit. Then read the saved lines back from the business object to confirm that what passed the checks is what was stored. For how the rows are posted, see posting every row of an Extend grid.

Sources checked