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 asthisRow) that represents the current row element inside each column'scellTemplate.columns(list of columns): The ordered list ofcolumndefinitions. Eachcolumnmust include a uniquecolumnIdcontaining at least one alphabetic character (for example,"col1", never"1"alone) and acellTemplatecontaining the widget tag (text,number,date,checkBox,instanceList, or afieldSetcontainer).
| PMD Widget | Primary List Binding | Item Variable | Key Constraints |
|---|---|---|---|
PMD Widgetgrid (static) | Primary List Bindingrows | Item VariablerowVariableName | Key Constraintscolumns and dynamicColumns are mutually exclusive; columnId must include at least one letter |
PMD Widgetgrid (dynamic) | Primary List Bindingrows | Item VariablerowVariableName | Key ConstraintsRequires both dynamicColumns (with columnVariableName, columnId, label, templateId, attributeName) and cellTemplates[] |
PMD WidgetpanelList | Primary List Bindingpanels | Item VariablepanelName | Key ConstraintsUses children[] instead of columns[]; supports inlineButton (button or dropDownButton) |
PMD WidgetinstanceList / dropdown | Primary List Bindingvalues, instanceList, or instanceListLoopTag | Item 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 & filtering | ConfigurationSet "sortableAndFilterable": true and a non-empty "label" on the column | Documented RestrictionsSupported only on column cellTemplates using checkBox, date, instanceList, number, text, or time widgets |
| Column / Grid FeatureFrozen columns | ConfigurationSet "frozen": true on the column | Documented RestrictionsDesktop browsers can freeze multiple left-hand columns on view and edit pages; mobile apps freeze only the first column |
| Column / Grid FeatureColumn subtotals | ConfigurationSet "subtotal": true on a numeric column, or call myGrid.getSubtotal('columnId') in script | Documented RestrictionsThe subtotal column attribute works only on numeric columns of an editable grid |
| Column / Grid FeatureExport to Excel | ConfigurationSet "excelExportEnabled": true on the grid; customize or exclude columns via the column excel map | Documented 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).
- Set
"isArrayOutBinding": "<% true %>"on thegrid. - In each column's
cellTemplate, include[]in thevalueOutBindingpath:
{
"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:
- Create a column with a read-only
cellTemplate(for example,"id": "entryId") bound to<% rowdata.id %>and"valueOutBinding": "putEntry.id". - On the
POSToutbound endpoint, set"exclude": "<% !empty entryId.value %>"so it runs only for newly added rows whereentryId.valueis empty. - On the
PUToutbound endpoint, set"url": "<% '/entries/' + entryId.value %>"and"exclude": "<% empty entryId.value %>"so it runs only for existing rows. - On all editable data columns, use
valuesOutto bind the cell value (<% self.value %>) to bothpostEntry.<field>andputEntry.<field>. - For row deletions, define a
DELETEoutbound 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:
- Set
"selectionEnabled": true,"maxRowSelection": 10(must be greater than1),"currentRowVariable": "currentRow", and"isArrayOutBinding": falseon thegrid. Note that enablingselectionEnabledhides the+(add) and-(remove) row buttons regardless ofdoNotAddanddoNotRemove. - 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
rowsorvalues: Workday REST, WQL, and CBO endpoints wrap results in adata[]array (<% myEndpoint.data %>). Graph API endpoints nest the list under<% myGraphEndpoint.data.<queryField>.data %>. Bindingrowsdirectly to<% myEndpoint %>passes a JSON map instead of a list. - Missing
idanddescriptoroninstanceList: Thevaluesattribute of aninstanceListordropdownrequires root-levelidanddescriptorproperties on every item. If your endpoint returns different field names (for example,workdayID.idandfullName, or nested objects), setidKeyanddisplayKeyor transform the array in PMD scripting with.map(item => {'id': ..., 'descriptor': ...}). - Hidden widget omitting outbound data: When
"visible": falseis set on a grid or widget on an edit page, Presentation Components exclude itsvalueOutBindingandvaluesOutfrom the outbound payload. Use ahiddentag inside a columnfieldSetif 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
limitandoffsetpagination, filtering by the parent ID. isCollection: truetimeouts and payload caps: Setting"isCollection": trueon aGETinbound endpoint forces Presentation Components to loop over the endpoint in batches of 100 rows untiltotalis 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), enablingisCollectionon a large dataset causesRestResponseTooLargeException(HTTP 500) or gateway timeouts. ReplaceisCollection: truewith a paginatedgridusingpagingInfo, or add search parameters to yourinstanceListendpoint.
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
- Workday Extend Developer Guide: Concept, grid
- Workday Extend Developer Guide: grid Tag Reference
- Workday Extend Developer Guide: column Tag Reference
- Workday Extend Developer Guide: endPoints Tag Reference
- Workday Extend Developer Guide: Concept, instanceList
- Workday Extend Developer Guide: Concept, PMD Best Practices
- Workday Extend Developer Guide: Reference, Limits on Presentation Components