Orchestrate has no Excel reader, but it can open the file

Orchestrate has no step or expression that parses an .xlsx workbook into rows the way asCSV() parses a CSV file. What it does have are zip functions on the Data type, and an .xlsx file is a zip archive of XML parts (the Office Open XML format). Open the archive, take the worksheet XML out of it and read it with the same XPath functions you use for any XML payload.

Workday's own Orchestrate Sampler app in the App Catalog shows the format from the other direction: its CreateExcel suborchestration builds an .xlsx from scratch by writing xl/workbook.xml, the worksheet parts and [Content_Types].xml as XML, then packing them with a Zip Archive aggregation. Reading is the same structure in reverse.

Before building this, ask whether the sender can deliver CSV. A CSV file reads in one call (asCSV(), or asCSV() with a CSV format from your app's Settings) and avoids everything below. Processing a CSV file in Orchestrate covers that route.

Which parts of the workbook you need

These are the parts of the Office Open XML spreadsheet format that matter for reading values. The paths are inside the zip:

Part What it holds
Partxl/workbook.xmlWhat it holdsThe tab names and their order, each with a relationship ID
Partxl/_rels/workbook.xml.relsWhat it holdsWhich worksheet file each relationship ID points to
Partxl/worksheets/sheet1.xml, sheet2.xml, ...What it holdsThe cells, row by row, under sheetData
Partxl/sharedStrings.xmlWhat it holdsEvery distinct text value in the workbook, in a numbered list

The file name sheet1.xml does not have to be the first tab the user sees. If your integration depends on a tab name such as "Equipment", look the name up in workbook.xml and follow its relationship ID through workbook.xml.rels to the worksheet file. For a template your team controls with one tab, sheet1.xml is usually enough, but check it once with listFilesInZipArchive().

Step 1: get the file and open the archive

How the file arrives depends on the orchestration. In an Integration System orchestration that receives documents on its event, the documents accessor finds it by name or by label. Workday's sample apps read an event document with documents.name(...).get(), which returns a Data value.

In a Create Values step:

Workbook      documents.name("Equipment Request.xlsx").get()
Parts         data.Values.Workbook.listFilesInZipArchive()
HasSheet      data.Values.Workbook.isFileInZipArchive("xl/worksheets/sheet1.xml")
SheetXml      data.Values.Workbook.fetchFileFromZipArchive("xl/worksheets/sheet1.xml").asXML().excludeAllNamespaces()
StringsXml    data.Values.Workbook.fetchFileFromZipArchive("xl/sharedStrings.xml").asXML().excludeAllNamespaces()

fetchFileFromZipArchive returns the part as Data. asXML() treats it as XML. excludeAllNamespaces() strips the SpreadsheetML namespace so the XPath below can use plain element names. Without it, every element sits in the SpreadsheetML default namespace and unprefixed paths match nothing.

Guard the run with a Continue on Conditions step whose assertion is data.Values.HasSheet; when it is false, the orchestration stops and logs the message. The Orchestrate Sampler's unzip example does the same with listFilesInZipArchive() and stops with a message that lists the files it found, which is the message you want when someone uploads the wrong file.

A workbook without any text cells may have no sharedStrings.xml. Test for it with isFileInZipArchive("xl/sharedStrings.xml") before you fetch it.

Step 2: loop over the rows

Each row is a row element under sheetData, and each cell is a c element inside it:

<row r="2">
  <c r="A2" t="s"><v>14</v></c>
  <c r="B2"><v>3</v></c>
  <c r="C2" t="s"><v>15</v></c>
  <c r="E2"><v>45962</v></c>
</row>

Point a Loop at the rows, filtering out the header row:

Data Set: data.Values.SheetXml.iterator("/worksheet/sheetData/row")
Filter:   data.Rows.item.exists("/row[@r = '1']").not()

The r attribute is the row number on the sheet, and on a cell it is the cell reference. Use it rather than position, because Excel does not write empty cells. In the example, column D is missing from row 2. Reading "the fourth c element" would give you column E's value.

To read one column, select the cell by its reference. Build the reference from the column letter and the row's own number, using the string-interpolation form Workday's sample apps use:

RowNum    data.Rows.item.stringAtXPathOrEmptyString("/row/@r")
CellPath  s"${"/row/c[@r = 'C"}${data.RowValues.RowNum}${"']"}"
Type      data.Rows.item.stringAtXPathOrEmptyString(s"${data.RowValues.CellPath}${"/@t"}")
Raw       data.Rows.item.stringAtXPathOrEmptyString(s"${data.RowValues.CellPath}${"/v"}")

stringAtXPathOrEmptyString reads text nodes and attributes and returns an empty string when the cell or attribute is absent, which is the normal case for blank cells, instead of failing the step.

Step 3: turn cell values into real values

What v holds depends on the cell's t attribute:

t Meaning of the cell How to read it
tsMeaning of the cellShared stringHow to read itv is a zero-based index into sharedStrings.xml
tabsent or nMeaning of the cellNumberHow to read itv is the number
tinlineStrMeaning of the cellText stored in the cellHow to read itThe text is in is/t, not v
tbMeaning of the cellBooleanHow to read itv is 1 or 0
tstrMeaning of the cellText result of a formulaHow to read itv is the text

Shared strings are the common case: most text cells hold only an index. XPath positions start at 1 and the index starts at 0, so add one before you look it up:

StrPath   s"${"/sst/si["}${math.plus(1, data.RowValues.Raw.toNumber()).format("0")}${"]//t"}"
Text      data.Values.StringsXml.stringsAtXPath(data.RowValues.StrPath).mkString("")

The //t and mkString("") handle rich text, where Excel splits one cell's text into several runs, each with its own t element. Wrap the lookup in a Branch on Conditions on Type, so numbers are not sent through it.

Dates are numbers. Excel stores a date as a serial day count, and a cell formatted as a date only differs from any other number in its style. In the default 1900 date system the count is days after 30 December 1899, so 45962 is 1 November 2025:

date.parse("1899-12-30").plusDays(data.RowValues.Raw.toNumber())

A time of day is the fractional part of the same number. Formula cells carry their last calculated result in v, next to the formula in f. Orchestrate reads that cached value, and nothing recalculates it, so a workbook saved by a tool that does not calculate formulas can arrive with stale or missing results.

Size and shape limits that apply

An .xlsx is compressed, so the XML you fetch is larger than the file. Orchestrate allows 200 MB of concurrent data in memory and keeps larger values in file-backed structures on disk, up to 2 GB concurrent, according to the runtime limits page. A worksheet part read with asXML() and held across a long loop is the value to watch. Do the shared-strings lookup inside the loop, aggregate only the fields you need (a JSON Fold or CSV Fold), and let the full worksheet XML go out of scope.

Merged cells keep their value in the top-left cell only, and the others are absent. Hidden rows and filtered-out rows are still in sheetData. Neither is visible from the values alone, which is one more reason to agree a fixed template with the sender.

When a connector or EIB is the better route

If the workbook lives in Microsoft 365, Workday's connector catalogue lists Microsoft Excel actions such as Get Spreadsheet and Get Table Rows, and the file storage connectors include OneDrive's Get Table and SharePoint's Get Excel Table, which read a table from a workbook stored there. Connectors are available only to Extend AI customers and only in orchestrations built on the Custom Agent Action template, so they are not an option for an Integration System orchestration.

If the goal is only to load the spreadsheet's rows into Workday, an inbound EIB is usually the simpler tool. Workday's integration setup considerations list inbound EIB as simpler to use than the other integration technologies, best for one-time bulk loads, and able to generate a spreadsheet for data entry by people without Workday access. Turning a messy spreadsheet into an EIB load and fixing EIB load errors cover that route.

How to prove the read is right

Store what you parsed before you load anything. Aggregate the rows into a CSV Fold or JSON Fold, write it with Store Document attached to the integration event, and open it next to the original workbook. Check four rows: the first data row, the last, a row with a blank cell in the middle, and a row with a date. If those four match, the cell references, shared-string offsets and date conversion are right, and a Batch Loop can send the rows to Workday in groups.

Sources checked