Most Workday Extend grid and collection widget errors in Page Metadata (PMD) come from five root causes: binding rows to the endpoint object instead of its .data array, mixing mutually exclusive attributes (columns vs. dynamicColumns, or pagingInfo vs. autoPaging), omitting null-coalescing defaults (??) on editable cells, sending row-by-row outbound calls when the target API expects an array (isArrayOutBinding), or hitting the 500-instance limit on related business objects and the 24-second endpoint timeout with isCollection.

Below is a reference to how PMD grid and collection widgets bind, paginate, sort, and submit data, followed by concrete fixes for each validation and runtime failure.

Core PMD grid and collection architecture

Every static grid in a PMD requires three top-level bindings to render rows:

  • rows (dynamicBinding-List): The list of items to render—typically <% myEndpoint.data %> for a REST or WQL endpoint, or <% myGraphEndpoint.data.worker.data %> for a Graph API endpoint.
  • rowVariableName (string): An alphanumeric/underscore identifier (such as thisRow) that represents the current row element inside each column's cellTemplate.
  • columns (list of columns): The ordered list of column definitions. Each column must include a unique columnId containing at least one alphabetic character (for example, "col1", never "1" alone) and a cellTemplate containing the widget tag (text, number, date, checkBox, instanceList, or a fieldSet container).
PMD Widget Primary List Binding Item Variable Key Constraints
PMD Widgetgrid (static)Primary List BindingrowsItem VariablerowVariableNameKey Constraintscolumns and dynamicColumns are mutually exclusive; columnId must include at least one letter
PMD Widgetgrid (dynamic)Primary List BindingrowsItem VariablerowVariableNameKey ConstraintsRequires both dynamicColumns (with columnVariableName, columnId, label, templateId, attributeName) and cellTemplates[]
PMD WidgetpanelListPrimary List BindingpanelsItem VariablepanelNameKey ConstraintsUses children[] instead of columns[]; supports inlineButton (button or dropDownButton)
PMD WidgetinstanceList / dropdownPrimary List Bindingvalues, instanceList, or instanceListLoopTagItem VariableN/A (or as in instanceLoop)Key Constraintsvalues expects root-level id and descriptor fields unless overridden with idKey and displayKey or reshaped via .map()

Configuring grid pagination (pagingInfo vs. autoPaging)

By default, a PMD grid renders all rows returned by the initial endpoint call without pagination. On view pages (pagination options do not apply to edit grids), you can paginate large datasets using either pagingInfo or autoPaging. The two attributes are mutually exclusive.

1. Server-side pagination with pagingInfo

Use pagingInfo when your inbound REST, WQL, or Graph API endpoint supports limit and offset parameters. Workday recommends setting the initial limit to at least 50 (and up to the 100-row API maximum) for paginated grids.

In the inbound endpoint, read the predefined string variables pagingVariables.limit and pagingVariables.offset. Each grid on a page maintains its own isolated pagingVariables state and updates offset automatically as the user scrolls down:

{
  "endPoints": [
    {
      "name": "workersPaged",
      "baseUrlType": "workday-staffing",
      "authType": "sso",
      "url": "<% var limit = pagingVariables.limit ?: 50; var offset = pagingVariables.offset ?: 0; '/workers?limit=' + limit + '&offset=' + offset; %>"
    }
  ]
}

When binding pagingVariables.limit and pagingVariables.offset into a Graph API variables block where the schema expects BigInt, remember that pagingVariables are strings—convert them with .toInt():

"limit": "<% empty pagingVariables.limit ? 50 : pagingVariables.limit.toInt() %>",
"offset": "<% empty pagingVariables.offset ? 0 : pagingVariables.offset.toInt() %>"

Then wire all three required properties inside pagingInfo on the grid:

{
  "type": "grid",
  "id": "workersGrid",
  "label": "Workers",
  "rows": "<% workersPaged.data %>",
  "rowVariableName": "worker",
  "pagingInfo": {
    "endPoint": "<% endpoints.workersPaged %>",
    "rowCount": "<% workersPaged.total %>",
    "data": "<% workersPaged.data %>"
  },
  "columns": [
    {
      "type": "column",
      "columnId": "workerNameCol",
      "label": "Worker",
      "sortableAndFilterable": true,
      "cellTemplate": {
        "type": "text",
        "value": "<% worker.descriptor %>"
      }
    }
  ]
}

2. Client-side scroll paging with autoPaging

Set "autoPaging": true only on view pages backed by fast endpoints that do not support limit and offset parameters and return a large dataset in a single response. When the result exceeds 50 rows, the grid renders the first 50 rows and re-invokes the endpoint as the user scrolls. Never enable autoPaging on slow endpoints, because scrolling triggers repeated full calls.

Sorting, filtering, freezing, and Excel export constraints

Column / Grid Feature Configuration Documented Restrictions
Column / Grid FeatureColumn sorting & filteringConfigurationSet "sortableAndFilterable": true and a non-empty "label" on the columnDocumented RestrictionsSupported only on column cellTemplates using checkBox, date, instanceList, number, text, or time widgets
Column / Grid FeatureFrozen columnsConfigurationSet "frozen": true on the columnDocumented RestrictionsDesktop browsers can freeze multiple left-hand columns on view and edit pages; mobile apps freeze only the first column
Column / Grid FeatureColumn subtotalsConfigurationSet "subtotal": true on a numeric column, or call myGrid.getSubtotal('columnId') in scriptDocumented RestrictionsThe subtotal column attribute works only on numeric columns of an editable grid
Column / Grid FeatureExport to ExcelConfigurationSet "excelExportEnabled": true on the grid; customize or exclude columns via the column excel mapDocumented RestrictionsView pages only; not supported on edit grids, dynamicColumns, mobile devices, or grids exceeding the 60-second export limit (Workday recommends RaaS above ~4,000 rows)

If an Excel-exported column uses a container widget (fieldSet or section) or an image, Workday cannot infer the scalar export value automatically—you must define "excel": { "value": "<% ... %>" } on that column.

Submitting editable grids to outbound endpoints

When a page includes an outboundData block, it becomes an edit page and any grid on it becomes editable by default (doNotAdd: false, doNotRemove: false). How you bind cells to outbound endpoints depends on whether the target API accepts a single row per request or a batch array.

Pattern 1: Single request with an array payload (isArrayOutBinding: true)

Workday's PMD best practices recommend submitting grid rows as a single array whenever the target endpoint supports it (such as a Custom Business Object ?bulk=true endpoint, a parent object with child lines like requisitionLines[], or an orchestration input).

  1. Set "isArrayOutBinding": "<% true %>" on the grid.
  2. In each column's cellTemplate, include [] in the valueOutBinding path:
{
  "type": "grid",
  "id": "linesGrid",
  "label": "Requisition Lines",
  "rows": "<% existingLines.data %>",
  "rowVariableName": "line",
  "isArrayOutBinding": "<% true %>",
  "columns": [
    {
      "type": "column",
      "columnId": "qtyCol",
      "label": "Quantity",
      "cellTemplate": {
        "type": "number",
        "id": "qtyCell",
        "value": "<% line.quantity ?? 1 %>",
        "valueOutBinding": "submitRequisition.requisitionLines[].quantity"
      }
    }
  ]
}

Pattern 2: Separate POST (new rows) and PUT/PATCH (existing rows)

When your API requires POST /entries for newly added rows and PUT /entries/{id} for modified rows, leave isArrayOutBinding as false (the default) and use the row ID cell to conditionally exclude each outbound endpoint:

  1. Create a column with a read-only cellTemplate (for example, "id": "entryId") bound to <% rowdata.id %> and "valueOutBinding": "putEntry.id".
  2. On the POST outbound endpoint, set "exclude": "<% !empty entryId.value %>" so it runs only for newly added rows where entryId.value is empty.
  3. On the PUT outbound endpoint, set "url": "<% '/entries/' + entryId.value %>" and "exclude": "<% empty entryId.value %>" so it runs only for existing rows.
  4. On all editable data columns, use valuesOut to bind the cell value (<% self.value %>) to both postEntry.<field> and putEntry.<field>.
  5. For row deletions, define a DELETE outbound endpoint using <% '/entries/' + entryId.value %> and reference its name in the grid's "deleteEndPoint" attribute.

Pattern 3: Submitting only selected rows (selectionEnabled: true)

To let users select specific rows on an edit grid:

  1. Set "selectionEnabled": true, "maxRowSelection": 10 (must be greater than 1), "currentRowVariable": "currentRow", and "isArrayOutBinding": false on the grid. Note that enabling selectionEnabled hides the + (add) and - (remove) row buttons regardless of doNotAdd and doNotRemove.
  2. On the outbound endpoint, filter unselected rows in onSend:
"onSend": "<% currentRow.selected ? self.data : null %>"

Never reference currentRowVariable inside a valueOutBinding script—Workday only populates currentRowVariable per row when isArrayOutBinding is false and the endpoint evaluates onSend.

Fixing common PMD grid and collection errors

1. Grid or instanceList renders completely blank despite HTTP 200

  • Wrong root path in rows or values: Workday REST, WQL, and CBO endpoints wrap results in a data[] array (<% myEndpoint.data %>). Graph API endpoints nest the list under <% myGraphEndpoint.data.<queryField>.data %>. Binding rows directly to <% myEndpoint %> passes a JSON map instead of a list.
  • Missing id and descriptor on instanceList: The values attribute of an instanceList or dropdown requires root-level id and descriptor properties on every item. If your endpoint returns different field names (for example, workdayID.id and fullName, or nested objects), set idKey and displayKey or transform the array in PMD scripting with .map(item => {'id': ..., 'descriptor': ...}).
  • Hidden widget omitting outbound data: When "visible": false is set on a grid or widget on an edit page, Presentation Components exclude its valueOutBinding and valuesOut from the outbound payload. Use a hidden tag inside a column fieldSet if you need to persist a non-editable row identifier.

2. Null pointer exceptions in PMD scripts when adding a row

When a user clicks + to add a new row to an editable grid, all rowVariableName properties on the new row are null. Any arithmetic or string concatenation in value or onChange (such as quantityCell.value * amountValue.value) will throw a PMD scripting error if a cell evaluates to null. Always provide a fallback using the null-coalescing operator (??), for example: "value": "<% rowdata.amount ?? 0.00 %>".

3. Undefined variable validation error when referencing a grid

Presentation Components evaluate tags sequentially from top to bottom in the PMD file. If a summary number widget at the top of the page calls <% expenseGrid.getSubtotal('amountCol') %> or <% expenseGrid.rows.size() %> before expenseGrid is declared in the PMD, validation fails with Undefined variable. Either place the dependent widget after the grid in the PMD, or place both inside a parent section or fieldSet with "sorted": true and control their visual placement using "sortOrder": "a" and "sortOrder": "b".

Also remember that during initial widget construction, a widget's value expression cannot call a script function that references that same widget's id.

4. Hitting the 500-row collection limit or 24-second isCollection timeout

Two platform limits frequently break large collections and grids:

  • 500-instance limit on related business objects: Both the Workday Graph API and WQL cap multi-instance related business object fields at 500 instances per parent record. If a parent record has more than 500 child instances, querying the multi-instance relationship throws an error. Fix this by querying the child business object directly as its own endpoint with limit and offset pagination, filtering by the parent ID.
  • isCollection: true timeouts and payload caps: Setting "isCollection": true on a GET inbound endpoint forces Presentation Components to loop over the endpoint in batches of 100 rows until total is reached. Because every PMD endpoint has a strict 24-second timeout (and the entire page load has a 60-second timeout and a 25 MB response payload limit), enabling isCollection on a large dataset causes RestResponseTooLargeException (HTTP 500) or gateway timeouts. Replace isCollection: true with a paginated grid using pagingInfo, or add search parameters to your instanceList endpoint.

How kiweely fits

kiweely validates your PMD widget bindings against the actual JSON response schemas of your tenant's REST, WQL, and Graph API endpoints—catching mismatched rows paths, missing pagingInfo conversions, and valueOutBinding errors before you deploy. In your Workday personal developer account and its tenant, Kiweely Personal is free (Kiweely Personal is free for your Workday personal developer account and its tenant. Any other tenant needs a Personal licence, $99 a month, or a seat in your firm.).

Frequently asked questions

Why does my PMD columnId fail validation when I use "0" or "1"?

Workday Extend requires every columnId on a column tag to contain at least one alphabetic character (such as "col0" or "amountColumn"). Purely numeric columnId strings are rejected or cause childrenMap lookup failures. Meanwhile, widget id attributes must always start with a letter and be unique across the PMD page.

How do I read a cell value from a specific grid row in PMD scripting?

For a grid row instance (for example, var row = myGrid.rows[0] or myGrid.selectedRows[0]), you can access a cell either via the column's columnId in childrenMap (row.childrenMap.myColumnId.value) or directly via the row's get(childId) shorthand using the column's id (row.myColumnId.value). If the column's cellTemplate wraps multiple widgets inside a fieldSet (such as a readOnlyText and a hidden tag), traverse the nested childrenMap: row.childrenMap.myColumnId.childrenMap.myHiddenTagId.value.

Why does indexVariableName return the wrong index after a user deletes a row?

In PMD grids and panelList widgets, indexVariableName is assigned when rows render and does not recalculate dynamically when a user deletes an intermediate row on an edit page. To reliably locate a specific row on an edit page, store the record's unique ID in a hidden tag inside a column fieldSet and look up the row with myGrid.rows.find(row => ...).

Sources checked