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_INSTANCE field whose target is the attachment object (for example logo targeting CharityLogo in 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_INSTANCE field targeting the business object (the equipmentRequest field 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):

  • valueOutBinding in the form endpointName:defaultCollectionName, for example createRequestFiles:requestFiles.
  • An outbound endpoint whose url is the Collection endpoint, requestFilesCollection, even when singular is 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:

  1. Give metadata at least an empty map, "<% {:} %>", when you set everything in onMultiPartSend. Workday requires the empty map when the attachment object has no metadata, and the sample's comment says onMultiPartSend needs metadata to work with.
  2. End the script with self.data. A console call as the last line breaks it; put logging before the return.
  3. 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 the file references in the self.data.metadata.{defaultCollectionName} array either: the endpoint then does not save the files.
  4. onSend does not support multipart endpoints, so fields added there do not reach the attachment request. Use onMultiPartSend.

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:

  1. POST the new file to charityLogosCollection.
  2. PATCH the parent record so its logo field holds the id returned in createCharityLogo.charityLogos[0].id, excluded if that id is empty.
  3. 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:

  1. GET requestFiles?equipmentRequest={requestId} with Accept: application/json and confirm one instance per uploaded file, with the expected fileName, a non-zero fileLength, the right contentType and your own fields filled in.
  2. GET requestFiles/{fileId} without the header and confirm the bytes open as the original document.
  3. 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.

Sources checked