The short answer: Loop over an XPath iterator and fold the rows into CSV
Orchestrate has no single "XML to CSV" function. You loop over the repeating element with xml.iterator("<xpath>"), read each column's value with an XPath accessor, and add a CSV Fold aggregation to the loop so every iteration contributes one row to a single CSV document.
The pattern has four parts: get the XML as an XML value, define the CSV layout once as a CSV format, loop and fold, then store or send the file. Workday's App Catalog sample Location Changes Outbound does exactly this with the output of Core Connector: Locations, so its source is a working reference for every expression below.
Get the Core Connector output into the orchestration as XML
When a Core Connector hands its output to an Orchestrate integration system (in the Location Changes Outbound sample, through an integration step on the connector's business process), the file arrives as an event document. The sample reads it with the documents global functions, filtering on the label the connector puts on its output:
documents.labelsMatchAny(List.stringList("Deliverable")).get()
get() returns a Data value. Call asXML() on it before applying any XPath; the XPath Support page is explicit that a string or document that is XML-compliant but not stored as an XML object must be cast first.
The same applies to XML from other sources. A SOAP response from Send Workday API Request, a RaaS response requested as XML or a file you fetched over HTTP all become XML values you can iterate. If the document might be empty, test before looping, as the sample does:
data.InputFile.CCW.asXML().exists("/locc:Locations/locc:Location[1]")
Branch on that and write an integration message when it is false, so an empty extract produces a clear "no input" message instead of an empty CSV that looks like success.
Define the columns once as a CSV format
Open the gear icon above the canvas, choose Settings, then CSV Formats. A format has a Name, a Header Row check box, a Separator Character (default ,), an Escape Character (default \), a **Quote Character** (default ") and a list of Columns, each with a name and a required flag.
Set the separator to | or a tab here if the receiving system wants a pipe- or tab-delimited file; nothing else in the flow changes. The format is the contract with the downstream system, so name the columns exactly as the file specification does.
Loop and fold: one XML element in, one CSV row out
Add a Loop step. For its data set, iterate the element that repeats once per output row:
data.InputFile.CCW.asXML().iterator("/locc:Locations/locc:Location")
Inside the loop the current element is data.csvloop.item (the loop's reference name, then item). It is an XML fragment rooted at that element, so XPaths start from the element itself, not from the document root. The Location Changes sample passes the item to a suborchestration that maps the fields, and reads each one like this:
data.start.CCW_Record.stringAtXPathWithDefault("/locc:Location/locc:Location_Name", emptyString())
Directly in the loop, the same read is data.csvloop.item.stringAtXPathWithDefault(...).
Use the same prefixes the document itself uses. The sample writes locc: because that is how the Core Connector: Locations output is prefixed; a Get_Workers SOAP response is read with wd: paths such as /env:Envelope/env:Body/wd:Get_Workers_Response/....
Then open the loop's three-dot menu, choose Add Aggregation, add an output and pick the CSV Fold strategy. You have two ways to fill it:
- With a CSV format. Select your format from the Format list and give an expression for each column. This is what the Employee Demographic Outbound sample does, with one expression per named column.
- With a String List. Supply one
StringListper iteration, in column order. The Join Loop orchestration in Workday's Orchestrate for Integrations Sampler builds a row like this:
List.stringList(data.JoinLoop.xml.stringAtXPath("/*/EmpID"), data.JoinLoop.csv.columnValueAtIndex(1), data.JoinLoop.csv.columnValueAtIndex(2))
Prefer the format route for anything a partner will receive. The column names, header row and quote and escape characters then live in one place in Settings instead of being implied by the order of a list.
Every aggregation also takes a Condition. The Location Changes sample sets it to the loop group's isSuccess, so a location that failed mapping is reported through the loop's error handler and left out of the file instead of producing a half-filled row.
Why a value missing from the XML stops the run
The XPath Support page states that if a [dataType]AtXPath function finds no matching node, the orchestration fails. So if an element is absent for one record, for example a worker with no work email, reading it with stringAtXPath ends the whole run at that record.
Use stringAtXPathWithDefault(xpath, emptyString()) or stringAtXPathOrEmptyString(xpath) for every optional column. Keep the strict stringAtXPath only for values the file cannot be valid without, such as the employee ID, where failing loudly is the correct behaviour.
Two other XPath limits come from the same page. Most accessors return only leaf values (text or an attribute), not fragments; iterator, fragmentAtXPath and countItemsAtXPath are the exceptions. And predicates cannot be nested more than one level, so a filter like "the phone whose device type ID is Landline" works, but a predicate inside a predicate inside a predicate does not.
Repeating child elements: pick one or flatten into extra rows
A worker can have several addresses or emails. CSV has one value per cell, so decide per column:
- Take one occurrence with an index or a predicate, for example
locc:Address_Line_Data[1]orwd:ID[@wd:type='Employee_ID']. - Produce one row per child by iterating the child element instead of the parent, and read the parent's fields with absolute paths from the original document if you need them on every row.
When the XML must be reshaped heavily before it fits a row, do the reshaping first, outside the loop; replacing XSLT with Orchestrate steps covers the components for that.
When a text template is the better tool
A Create Text Template step with content type text/csv can also write CSV with {{#each ... }}. A template is plain text with no CSV format attached, so a comma or quote inside a name is yours to escape. Use a template when the file is not really CSV (a header record, detail records and a trailer with a count), and the CSV Fold with a format when it is. The delimited and fixed-width files guide covers the mixed-record case, and the Location Changes sample shows the fixed-width variant using rightPad(n, chars.space()).substring(0, n) per field.
Store the file and prove it is right
Pass the fold output (in the sample, data.csvloop.CSVFile) to a Store Document step with a document title such as locations.csv. In an Integration System orchestration the stored document is not attached to the integration event unless you set Attach document to integration event to true; set it, so the file appears on the event in the tenant. Storage Expires In takes an ISO 8601 duration such as P7D.
Then check three things on a real run:
- The integration event has the CSV attached and your success message.
- The file's row count matches the number of repeating elements in the source.
countItemsAtXPathon the input and a Count aggregation on the loop give you both numbers to write into the success message. - Open the file and look at a record with an empty optional field and one with a comma in a name. Those two rows are where conversions break.
For the reverse direction, reading a CSV that arrives in Orchestrate, see processing a CSV file in Orchestrate.
Sources checked
- Add Aggregations
- Create CSV Formats
- Reference: Xml Member Functions
- Concept: XPath Support
- Reference: Loop
- Reference: Store Document
- Reference: Documents Global Functions
- Concept: Text Templates
- App Catalog sample: Location Changes Outbound
- App Catalog sample: Employee Demographic Outbound
- App Catalog sample: Orchestrate for Integrations Sampler