What an attachment object stores, and which fields you can write
An Extend attachment object is a model component whose instances each hold one file. When you define it, Workday adds four fields you never declare: file, fileName, fileLength and contentType. file holds the content as a base64 string, fileName is writable through POST and PUT, and fileLength and contentType are derived from the upload and can only be read.
Anything else on the object, such as an uploadedBy worker or a link back to the business object the file belongs to, is an ordinary field you declare in the .attachment file:
{
"id": 1,
"name": "RequestFile",
"label": "Request File",
"defaultSecurityDomains": ["ManageEquipmentRequests"],
"defaultCollection": { "name": "requestFiles", "label": "Request Files" },
"fields": [
{ "id": 1, "name": "uploadedBy", "type": "SINGLE_INSTANCE", "target": "WORKER", "label": "Uploaded By" },
{ "id": 2, "name": "uploadedDate", "type": "DATE", "precision": "DAY", "label": "Uploaded Date" },
{ "id": 3, "name": "equipmentRequest", "type": "SINGLE_INSTANCE", "target": "EquipmentRequest", "label": "Equipment Request" }
]
}
The defaultCollection name becomes the endpoint: https://api.workday.com/apps/{appId}/v1/requestFiles. An app can hold 5 attachment objects, and the model-level maximum size of one attachment is 30 MB (Reference: Limits on Model Components). The page widget has its own, lower default; see file upload and attachment limits in Extend.
One file or many per record: which object points at which
Where the reference lives decides the cardinality, and Workday's attachments tutorial (summarised in Example: Attachment Object APIs in Presentation Components) builds both shapes:
- One file per record. The business object has a
SINGLE_INSTANCEfield whosetargetis the attachment object (for examplelogotargetingCharityLogoin the Charitable Donations sample). The record can only point at one file at a time. - Many files per record. The attachment object has a
SINGLE_INSTANCEfield targeting the business object (theequipmentRequestfield above). Any number of files can point at the same record.
For the many-files shape, read a record's files back with the generated query parameter. Extend indexes every SINGLE_INSTANCE field automatically, so GET requestFiles?equipmentRequest={requestId} returns only that record's files without you adding enableIndex (Example: Query Parameters in Business Object API Requests).
The multipart POST: data part first, file part after
As soon as the attachment object has fields of its own, a POST that writes the file and those fields together must use multipart/form-data. The JSON goes in a part named data, and its file value is a cid: reference to the name of the file part:
FILE=quote.pdf
curl -X POST "https://api.workday.com/apps/{appId}/v1/requestFiles" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: multipart/form-data" \
-F 'data={"file":"cid:filedata","uploadedBy":{"id":"Employee_ID=21001"},"uploadedDate":"2026-11-04","equipmentRequest":{"id":"{requestId}"}}' \
-F "filedata=@$FILE"
Workday documents that part order matters here: if the data part comes after the file part, the request fails with a 500 internal server error (Concept: Attachment Objects). When Postman or a script builds the form for you, check the order it actually sends before debugging anything else.
To send several files in one request, post to the collection endpoint, which is the defaultCollection name with Collection appended (requestFilesCollection). The JSON part then holds an array named after the default collection, one entry per file, each with its own cid: reference:
{
"requestFiles": [
{ "file": "cid:requestFiles0", "uploadedBy": { "id": "{workerId}" } },
{ "file": "cid:requestFiles1", "uploadedBy": { "id": "{workerId}" } }
]
}
Reading the file versus reading its fields
A GET on one instance, GET requestFiles/{fileId}, returns the file content by default, not JSON. To read fileName, fileLength, contentType and your own fields, send Accept: application/json; without that header you get the file data and no field data. In a PMD this means two inbound endpoints against the same URL if a page shows both the file and its details, which is what the Charitable Donations sample's updateCharityImage page does with its charityLogo and charityLogoMetadata endpoints:
{
"name": "requestFileDetails",
"baseUrlType": "app",
"url": "<% 'requestFiles/' + equipmentRequest.quote.id %>",
"authType": "sso",
"exclude": "<% empty(equipmentRequest.quote) %>",
"headers": [ { "name": "Accept", "value": "application/json" } ]
}
Uploading from a page with fileUploader
In a PMD, the fileUploader widget builds the multipart request for you. For a model attachment object it needs two things (Concept: fileUploader):
valueOutBindingin the formendpointName:defaultCollectionName, for examplecreateRequestFiles:requestFiles.- An outbound endpoint whose
urlis the Collection endpoint,requestFilesCollection, even whensingularis true. The sample's single-logo uploader does exactly that.
The metadata attribute supplies the field values for each file. It can hold values known at page load, such as the current worker and today's date, but not data that changes after load or comes from other widgets. For values that come from other widgets, or a link to a record created earlier in the same submit, set them in the endpoint's onMultiPartSend event, which runs just before the request goes out and works on self.data:
{
"name": "createRequestFiles",
"baseUrlType": "app",
"url": "requestFilesCollection",
"httpMethod": "POST",
"authType": "sso",
"exclude": "<% empty(quoteUploader.children) %>",
"onMultiPartSend": "<%
for (var file : self.data.metadata.requestFiles) {
file.put('uploadedBy', {'id': worker.id});
file.put('equipmentRequest', {'id': equipmentRequest.id});
}
self.data;
%>"
}
Four rules from Reference: PMD Scripting Events catch most broken uploads:
- Give
metadataat least an empty map,"<% {:} %>", when you set everything inonMultiPartSend. Workday requires the empty map when the attachment object has no metadata, and the sample's comment saysonMultiPartSendneeds metadata to work with. - End the script with
self.data. Aconsolecall as the last line breaks it; put logging before the return. - Return the full
self.data. If it comes back empty or null, only the files are sent and the POST might fail without its data part. Do not replace or remove thefilereferences in theself.data.metadata.{defaultCollectionName}array either: the endpoint then does not save the files. onSenddoes not support multipart endpoints, so fields added there do not reach the attachment request. UseonMultiPartSend.
When metadata is set, do not also put valueOutBinding on other widgets (hidden ones, for example) to build form-data parts for the same attachment endpoint.
Updating an attachment: replace, repoint, delete
The documented write paths are narrow. fileName can be written through POST and PUT, and onMultiPartSend applies to POST and PUT multipart endpoints. The Charitable Donations sample does not edit a stored file in place: to change a charity's logo, updateCharityImage submits three outbound endpoints in order:
- POST the new file to
charityLogosCollection. - PATCH the parent record so its
logofield holds the id returned increateCharityLogo.charityLogos[0].id, excluded if that id is empty. - DELETE the old instance at
charityLogos/{oldId}, excluded when there was no previous logo.
Follow the same order in your app; this is a design judgement, not a documented rule. Create first, so a failed upload leaves the old file in place; repoint second, so the record never points at a deleted file; delete last. Outbound endpoints run in the order listed in outboundEndPoints, and when one fails the updates from earlier ones are not rolled back (Concept: Submitting Data), so if the DELETE fails the record is still correct and the old file is an orphan you can remove later.
For the many-files shape there is nothing to repoint: add new files with the Collection POST and remove unwanted ones one at a time with DELETE requestFiles/{fileId}. DELETE on attachment objects does not support bulk=true.
Securing attachment data
An attachment object is secured by its defaultSecurityDomains, like a business object, and the processing user must be allowed by that domain for any request to return data. If the equipmentRequest field also sets secureByTarget, a query by that parameter returns only files on records the user can see. Domain design is covered in securing Extend apps with security domains.
Deleting the attachment object definition itself is different from deleting instances: once the definition is gone, nobody can open the stored files (Add Attachment Objects).
How to prove the upload worked
Do not stop at the page closing without an error. Check the stored result:
GET requestFiles?equipmentRequest={requestId}withAccept: application/jsonand confirm one instance per uploaded file, with the expectedfileName, a non-zerofileLength, the rightcontentTypeand your own fields filled in.GET requestFiles/{fileId}without the header and confirm the bytes open as the original document.- After a replace, confirm the parent's reference field holds the new id and that the old id now returns nothing.
The File Size report field, secured to the Public Reporting Items domain, lets a custom report or WQL query show the disk space each attachment uses, which is a quick way to spot uploads that stored an empty or truncated file. To show the files to users once they are stored, see displaying attachments and PDFs on an Extend page.