What PMD scripting is
PMD scripting is the expression language inside a Workday Extend page (a PMD file). You write it between <% and %>, and Workday runs it on the server when the page loads, when someone changes a field, and when they submit. It looks a little like JavaScript, but Workday is explicit that it isn't JavaScript and that JavaScript can't be used in Extend apps.
If you write calculated fields or condition rules, you already have the right instinct. A PMD script is mostly an expression that produces a value, and by default the last line it evaluates is its result, much as a calculated field's final function is its output. The difference is that a script can also change other fields on the page.
Where scripts go
There are four places, and knowing which one you're in explains most surprises.
Inside an attribute. The value of almost any widget attribute can be a script. This shows a worker list from an endpoint called
workersEndpoint:{ "type": "instanceList", "id": "workerList", "label": "Worker", "values": "<% workersEndpoint.data %>" }The
scriptblock. Ascriptattribute at the top of the page holds reusable functions, declared withvar. Every widget on that page can call them.Event handlers. Attributes such as
onChange,onLoad,onSubmitandonSendhold scripts that run when that event happens.Script modules. A
.scriptfile in the app'spresentation/scriptsfolder holds functions several pages share. A page loads it with"include": ["date.script"]. The file must end by returning a map of what it exports, it can't use the page's predefined variables directly, and it's limited to 100 KB.
Reading what's on the page
Three kinds of names cover nearly everything:
- An endpoint's name gives you its response. If the inbound endpoint is named
request, thenrequest.datais a list it returned, andrequest.statusis a field on a single record. - A widget's
idgives you that widget.quantity.valueis what the user typed. For lists, the value is a list of IDs, socostCenter.value[0]is the first selection. selfinside an event handler means the widget that fired the event.
Formatting dates, numbers and currency has its own functions, covered in the PMD formatting guide. There are also predefined variables, such as queryParams for the name=value pairs after the ? in the page's URL, flowVariables for values carried from an earlier page, and userTimeZone for the signed-in user's time zone.
Two operators save a lot of trouble with missing data. ?? uses a fallback when a value is null: request.notes ?? ''. ?. reads a property only if the thing before it exists, returning null instead of failing. And empty(x) is true for null, an empty string, an empty list or an empty map.
The five patterns that cover most pages
Show a value. Put the expression where the value goes. Text joins with +:
"value": "<% request.itemName + ' for ' + request.requester.descriptor %>"
Calculate a value. Arithmetic works as you'd expect. Workday's example computes a fee from a function and a carried-over rate:
"value": "<% getRegistrationFee() + (12 * flowVariables.monthlyRateFlowValue) %>"
React to a change. An onChange handler can set other widgets. This hides or shows a justification box when the amount crosses a limit:
"onChange": "<% justification.setVisible(self.value > 1000) %>"
Widget methods such as setVisible, setRequired, setEnabled, setValue and setValues are listed on each widget's reference page. Hiding has a side effect: Workday documents that a list widget with visible false leaves its bound value out of the outbound request unless you add it in onSend. Workday also says a widget whose visibility changes should sit inside a fieldSet.
Validate input. Call setError with a message, and clearError when the input is fine; the onChange and onSubmit validation guide goes further. Workday's validation example compares two fields:
"onChange": "<%
if (empty dataFieldCopy.value || dataField.value == dataFieldCopy.value) {
dataFieldCopy.clearError();
} else {
dataFieldCopy.setError('Doesn\\'t Match Data Field.');
}
%>"
Shape a list. filter, map and find take a small function, written x => { ... }. This keeps only workers with an email address and shows the email:
"values": "<% workers.data
.filter(worker => { !empty worker.person.email })
.map(worker => { { 'id': worker.person.id, 'descriptor': worker.person.email } }) %>"
The doubled braces in map are deliberate: the outer pair is the function body, the inner pair builds the record it returns.
When each event runs
The timing matters because a script can only use what exists at that moment.
onLoadruns as the page loads, before widgets exist. Use it to prepare endpoint data, not to touch widgets.onChange, and the other widget events such asonSearchandonRowAdd, run while the user works and can read and set any widget.onSubmitruns when the user clicks OK. It can read widgets, but values it sets won't appear on screen because the page is already submitting.onSendruns for an outbound endpoint and can rewrite the request body, which is how you send data that doesn't map one widget to one field.
Outbound endpoints are sent in the order they appear in the page, so place the one that creates a record above the one that uses its ID. On view pages, event handlers can't reference widgets at all; interactivity belongs on edit pages.
The mistakes that catch new authors
and,orandnotdon't work. PMD scripting reserves them but uses&&,||and!.- Reserved words in field names. A response field called
for, common in business process events, can't be read asevent.for.id. Useevent['for'].id. The same bracket form handles names with a hyphen, which would otherwise be read as subtraction. - A log line at the end returns nothing.
console.info(...)returns no value, so if it's the last line, the script's result is empty. Put the value on the final line. - A function changing an outside counter. Inside
x => { ... }you can't reassign a variable declared outside it. Add to an outside list instead, or use aforloop, which isn't a closure. - Using a function before it's declared. Inside one script, declare a function above any function that calls it.
selectedValuesset from an event. It works only at page load. In a handler, usesetValue([id]).
Prefer const for values that shouldn't change, let for ones that do inside a block, and var otherwise; that's Workday's own guidance and it prevents accidental overwrites.
How to see what a script is doing
Log as you go with console.info, console.warn, console.error or console.debug, then open Analytics > Logs on the Workday Developer Site and filter on wd_category is console. To see failures, filter on wd_category = pageError.script; in a Development tenant the page error itself also shows the script error's details. If a user reports a page error, the common request ID in their message (VPS|...) filters the logs to that one request through wd_common_request_id. Keep personal data out of log messages.
Scripts also have a resource budget: the scriptUsage log category records each run's CPU and memory, which is where to look when a heavy loop starts failing. The script constraint guide covers what to do when a page hits that limit.
The quickest way to learn the rest is Workday's PMD Scripting sample app in the App Catalog and the examples page in the Developer Guide: change one line, preview, read the log.