How fileUploader actually sends a file
A fileUploader on an edit page doesn't hand you file content to encode. When the user clicks OK, the page posts the selected files to the outbound endpoint named in the widget's valueOutBinding, as parts of a multipart request where the endpoint needs one. There is no Base64 step on the page, and Workday documents no PMD function for producing one.
The format of valueOutBinding tells the page what kind of request to build:
| Target endpoint | valueOutBinding on the uploader |
|---|---|
| Target endpointWorkday REST attachment endpoint that takes files only | valueOutBinding on the uploaderoutboundEndpointName |
| Target endpointWorkday multipart REST endpoint (files plus JSON) | valueOutBinding on the uploaderoutboundEndpointName:filePartName |
| Target endpointYour app's attachment object collection | valueOutBinding on the uploaderattachmentEndpointName:defaultCollectionName |
Target endpointGraph API mutation with a File input field | valueOutBinding on the uploaderoutboundEndpointName:filePartName |
| Target endpointThird-party multipart endpoint | valueOutBinding on the uploaderoutboundEndpointName:filePartName |
The uploader only submits to POST endpoints, so a PUT or PATCH endpoint in valueOutBinding won't take the files.
Where Base64 does come in
Base64 appears in three places in Extend, and none of them is a page encoding a file.
An attachment object stores its file this way. Workday's Concept: Attachment Objects page states that the generated file field holds the attached file as a Base64-encoded string. You still write to it with a multipart request: the JSON part refers to the file with "file": "cid:filedata" and the file goes in its own part. Workday also notes that the order of the parts matters: sending the JSON data part after the file part returns a 500 internal server error.
Images in the PMD are the second place, and the rule is to avoid it. The image tag's documentation warns against putting a Base64 image string in imageUri, because the serialized PMD on the server is limited to 100K and the page stops working if it goes over. Point imageUri at a hosted image, or use the tag's endPoint attribute.
A third-party API that wants Base64 inside JSON is the third. Workday documents no way for a page to build that body. Pass the saved attachment's ID to an orchestration instead, and encode there: Orchestrate's data member functions include base64encode and base64decode. The full pattern is in Base64 attachments in Orchestrate.
Accepting more than one file
singular defaults to false, so an uploader accepts several files unless you set "singular": true. Workday's limits are five files per uploader and 10 MB per file (10,485,760 bytes), which makes 50 MB in total. minRows sets the minimum number of files and can't be more than 5. The per-file size can only be lowered with maxFileSize, and only at page load. Extend upload limits and file types covers those rules and the tenant's file-type setting.
What changes with several files is the endpoint. A Workday REST attachment endpoint that takes one file per call needs singular: true. For your own app's files, post them all at once to the attachment object's collection endpoint: append Collection to the defaultCollection name in the endpoint url, and bind the uploader as endpoint:collection.
{
"type": "fileUploader",
"id": "receiptUploader",
"label": "Receipts",
"singular": false,
"fileTypes": "pdf,png,jpg,jpeg",
"valueOutBinding": "createReceipts:receipts",
"metadata": "<% { 'uploadedBy': { 'id': worker.id } } %>"
}
{
"name": "createReceipts",
"httpMethod": "POST",
"authType": "sso",
"baseUrlType": "app",
"url": "receiptsCollection",
"exclude": "<% empty(receiptUploader.children) %>"
}
The page sends one form-data part per file (receipts0, receipts1, …) and a JSON array named after the collection, with a "file": "cid:receipts0" reference and your metadata in each entry. Three documented rules apply here:
- The
metadataattribute is required for an attachment object collection, even if the object has no extra fields. Use"<% { : } %>"in that case. metadatais evaluated at page load, so it can't see values the user types afterwards. Set those in the endpoint'sonMultiPartSendhandler, which can editself.data.metadata.receiptsbefore the request goes out.- Never replace or drop the
cid:file references inonMultiPartSend. Without them the endpoint doesn't save the files. The handler's last line must beself.data.
The exclude line keeps the endpoint from firing when no file was added.
Why you can't put an uploader in each grid row
Workday's grid and column pages show cellTemplate examples with text, number, currency, instance lists and container tags, but never a fileUploader. The uploader's valueOutBinding also names an endpoint, not a field on a row. Treat an uploader per grid row as unsupported. Workday's docs are silent on it, so the pattern below is a recommended design rather than a documented feature.
Use one uploader beneath the grid and let each file say which line it belongs to. fileUploaderRowTemplate adds widgets to every row the user uploads, and those widgets are readable per file through the row's contentArea:
{
"type": "fileUploader",
"id": "lineFiles",
"label": "Attach files to lines",
"valueOutBinding": "createLineFiles:lineAttachments",
"metadata": "<% { : } %>",
"fileUploaderRowTemplate": [
{
"type": "instanceList",
"id": "fileLine",
"label": "Expense line",
"required": true,
"values": "<% pageVariables.lineChoices %>"
}
]
}
Then set the link field on each file entry in onMultiPartSend. This pairs the uploader's rows with the metadata entries by position, which Workday's attachment-object pages don't spell out, so confirm it with the read-back at the end of this guide before you rely on it:
"onMultiPartSend": "<%
var rows = lineFiles.rows;
var entries = self.data.metadata.lineAttachments;
for (let i = 0; i < entries.size(); i++) {
entries[i].expenseLine = { 'id': rows[i].contentArea[0].value[0] };
}
self.data;
%>"
If you submit through a Graph API mutation instead, Workday documents the same positional pairing directly: myFileUploader.rows.map((row, index) => { … 'attachmentContent': 'file' + index … }) builds one input entry per uploaded row with that row's part name, and the same closure can read row.contentArea[0] for the line.
The lines must exist before their files can point at them. Either save the grid first and upload on a following page that knows the new IDs, or link files to a parent record and let a stable key on each line, such as a line number, identify it.
Reacting to files as they're added
The uploader fires onFileAdd and onFileRemove. Both give you event.row, the row added or removed. Use them to check names or counts and set an error that blocks submission, and clear it when the problem file goes:
"onFileAdd": "<%
if (lineFiles.rows.size() > pageVariables.lineChoices.size()) {
lineFiles.setError('Attach no more than one file per line.');
}
%>",
"onFileRemove": "<%
if (lineFiles.rows.size() <= pageVariables.lineChoices.size()) {
lineFiles.clearError();
}
%>"
getRows() returns every row the user can see; getChildren() returns only rows whose file content is cached on the server, including rows loaded with fileSource. Count with rows when you validate what the user has attached.
Reading the saved attachments back
The page closing without an error only shows that the request was accepted. Read the attachments back. Call the attachment object's GET endpoint with Accept: application/json to get the field data without the file bytes. Each saved file should come back with its fileName, contentType and fileLength, which Workday derives from the upload, and with the link field you set. One entry per uploaded file, each pointing at the right line, is the proof.
If an entry is missing, look in Analytics > Logs on the Workday Developer Site for the page submission. A cid: reference overwritten in onMultiPartSend and a metadata map that was left out are the two documented ways files get dropped. To show the saved files back to the user, see displaying attachments and PDFs on an Extend page.