What Create Text Template does and how later steps read it

Create Text Template turns a message with {{ }} expressions into a document of the content type you pick, and every later step reads that document as data.<stepName>.message. Inside the braces you read JSON with stringAtJsonPath, read XML with stringAtXPath, repeat a block with {{#each … }}, and escape anything that would break the output format.

The step has three properties: a Reference Name, a Content Type and the Message. The content types on offer are application/json, application/octet-stream, application/x-www-form-urlencoded, application/xml, text/csv, text/html, text/plain and text/xml. Pick the type the receiving step or system expects, and check the output parses before you rely on it (the last section shows how).

Workday describes the template runtime as fully streaming, so embedding several large upstream documents in one template does not cost efficiency.

Read a JSON value with JSONPath

Call the JSON accessor on the upstream output. With a Send Workday API Request step named getRequest whose response is JSON:

{
  "requestId": "{{data.getRequest.response.stringAtJsonPath("$.id")}}",
  "status": "Submitted"
}

data.getRequest.response is shorthand for data("getRequest", "response"); both forms work. Choose the accessor by what should happen when the path is missing:

  • stringAtJsonPath throws if there is no value or the value is not a string, which fails the step.
  • stringAtJsonPathWithDefault(path, default) returns your default when the value is absent.
  • stringAtJsonPathOrEmptyString(path) returns an empty string instead of throwing.

The same pattern exists for numbers and booleans (numberAtJsonPath, booleanAtJsonPathWithDefault and so on). Use the throwing form for fields the receiver cannot do without, so a bad input stops at the template instead of producing a half-empty request.

The JSONPath inside the expression is itself a double-quoted string, so write string literals in filters with single quotes. Workday's Employee Relations Incident Management sample counts rows of a WQL response inside its SOAP template this way:

{{data.callProtectedCaseConnectionsWQL.response.countItemsAtJsonPath("$.data[?(@.connectionType!='Linked Case - Workday Help')]")}}

Read an XML value with XPath

XML accessors follow the same naming: stringAtXPath, stringAtXPathWithDefault, stringAtXPathOrEmptyString, numberAtXPath, isValueAtXPath. Orchestrate supports a subset of XPath: absolute paths, index predicates, attribute and descendant-value predicates, the * wildcard and //, with predicates nested at most one level and no nested index predicates. Namespace prefixes appear in the path as they do in the document, for example /env:Envelope/env:Body/wd:Get_Workers_Response/... on a SOAP response.

Two XPath rules cause most failed template steps. Most XPath accessors return leaf values only: a path that selects a fragment returns no data unless you use iterator, fragmentAtXPath or countItemsAtXPath. And a plain [type]AtXPath call fails the orchestration when no node matches, so use the WithDefault variant or test with isValueAtXPath first.

A string that holds XML must be cast before you can apply an XPath to it: data.someStep.payload.asXML().stringAtXPath("/Equipment_Request/ID").

Loop with #each

{{#each expression}}…{{/each}} repeats its body once per item. The expression must yield an iterator, which you get from iterator(path) on JSON or XML. Inside the block, this is the current item:

{{#each data.getRequests.response.iterator("$.data[*]")}}
Request: {{this.stringAtJsonPath("$.descriptor")}}
{{/each}}

You can name the item instead of using this; the documented syntax is {{#each expression as name}}, and the item is then item.name. Naming matters in nested loops, where this always means the nearest each item or with value:

{{#each data.payroll.response.iterator("$.PayGroup[*]") as payGroup}}
{{#each item.payGroup.iterator("$.Employee") as employee}}
{{item.employee.stringAtJsonPath("$.Employee_ID")}}
{{/each}}
{{/each}}

Commas between JSON items. A JSON array built in a loop needs a comma before every item except the first. Workday's own samples do it with isFirst:

{
  "data": [
  {{#each data.getEntries.response.iterator("$.data[*]")}}
    {{#if this.isFirst.not()}},{{/if}}{"id": "{{this.stringAtJsonPath("$.id")}}"}
  {{/each}}
  ]
}

XML items are elements. An XML iterator returns elements, not strings, and XPaths inside the loop start at that element. In Workday's Supplier Invoices Inbound sample the loop runs over /invoice-header/invoice-lines/invoice-line and reads each line with this.stringAtXPathWithDefault("/invoice-line/po-number", emptyString): the path begins at the item's own root element, not at the document root.

If the loop body needs a value from outside the item, reach it through data. as usual. For a long expression used several times, define it once with {{#alias name}}…{{/alias}} and refer to it as alias.name; Workday evaluates the aliased expression only once.

Escape braces, backslashes and quotes

Three escapes belong to the template syntax itself:

  • A literal {{ in the output is written \{{.
  • A backslash at the end of a line is a line continuation: Orchestrate drops it and the line break. A double backslash also drops the next line's leading whitespace, which lets you lay out a JSON template readably and still emit it on one line. When you need a real backslash as the last character of a line, write {{"\\"}}.
  • Comments are {{! … }} or {{!-- … --}} and produce no output.

Quotes need two different answers. Quotes in the template text are ordinary characters: a JSON key in quotes is just text. Quotes inside a value are the problem. If a worker comment contains He said "urgent" and you place it between quotes yourself, the JSON is no longer valid. Let Orchestrate add the quotes and escape the content instead:

"comment": {{this.stringAtJsonPath("$.comment").toJsonStringValue}},

toJsonStringValue returns the text escaped for JSON and surrounded with double quotes; Workday's AP e-invoice sample uses it this way on string values. escapeToJsonString escapes without adding the quotes, for when you write them yourself.

For an XML template the equivalent is escapeXml() on each free-text value:

<wd:Title>{{this.stringAtJsonPathOrEmptyString("$.title").escapeXml()}}</wd:Title>

When the value comes out of another XML document, xmlString(xpath) and xmlStringWithDefault return it already escaped for XML, so do not add escapeXml() on top. The full set of ampersand, angle-bracket and non-ASCII cases for SOAP bodies is in fixing special characters in SOAP calls from Orchestrate.

Reuse logic with functions and named templates

A user-defined function, {{#function isOverLimit(amount: Number)}}{{amount > 5000}}{{/function}}, returns a value and writes nothing; call it as function.isOverLimit(...), and declare it above the expressions that use it. A named template, {{#template line(id: String)}}…{{/template}}, writes output and is called as template.line("A-1"); inside it, its parameters are read as data.line.id. A named template cannot be called from a user-defined function or used as an alias body. Both are worth it when the same row layout repeats, which is common when rebuilding an XSLT transformation in Orchestrate.

Prove the template produces what the receiver expects

Open the template step in Orchestration Builder, choose Debug Step, connect to the tenant where the app is deployed and run Debug Current Step. If the upstream steps have not run in the debugger, supply their outputs under Required Inputs. Read the message in Debug Outputs and check three things: the document parses (for XML, isValid() on the cast message returns true), the loop produced the item count the source has, and a test value containing a quote, an ampersand and an accented letter arrives intact.

Then run the step that consumes the message and read the receiving system's result back. A template can be syntactically valid and still put the right value in the wrong field, which only the target record shows.

Sources checked