Date, LocalDateTime and ZonedDateTime: pick the type first

In Orchestrate you parse a date string with date.parse, datetime.parse or zoneddatetime.parse and a pattern, write it out with format(pattern) or toXMLDate(), and pass a zone ID to now(). Which of those you call depends on the type, and Orchestrate has a Date (a calendar day, no time, no zone), a LocalDateTime (a day and a time, no zone), a ZonedDateTime (a day, a time and a zone, so a real instant) and a LocalTime (a time of day). Hire dates, effective dates and pay period end dates are Date values and should stay that way from the moment you read them to the moment you write them.

Each type has a global qualifier with now and parse: date, datetime, zoneddatetime and localtime. Each has member functions to format, compare and do arithmetic.

Getting "now": always pass a zone ID

Every now() has two forms, with and without a zone ID:

date.now("America/New_York")
datetime.now("UTC")
zoneddatetime.now("Europe/London")

The references say the no-argument form uses "the default time zone" without saying which zone that is. Pass the zone explicitly whenever "today" matters, for example when an orchestration decides which records are effective as of today, names a file by date, or compares against an end date. Run near midnight, the two forms can disagree about which day it is. Most of Workday's sample apps call now() without a zone, but where the zone matters they pass it: the AP E-Invoice sample uses datetime.now("UTC"), and the Employee Relations Incident Management sample uses zoneddatetime.now("America/Los_Angeles").

Parsing dates from strings, XML and JSON

date.parse(text) takes the text alone, and the reference does not list the formats it accepts. Use it only for ISO dates such as 2026-11-04, and give the pattern for anything else:

date.parse(data.raw.invoiceDate, "yyyyMMdd")
datetime.parse(data.raw.stamp, "yyyy-MM-dd HH:mm:ss")
zoneddatetime.parse(data.raw.stamp, "yyyy-MM-dd'T'HH:mm:ssXXX")

The first line is the AP E-Invoice sample's own pattern for invoice dates that arrive as 20261104.

XML has typed accessors. dateAtXPath returns a Date and throws if the value is not a valid ISO local date; dateTimeAtXPath returns a LocalDateTime; zonedDateTimeAtXPath accepts an ISO offset date-time or an ISO instant. Each has a WithDefault form, and isDateAtXPath, isDateTimeAtXPath and isZonedDateTimeAtXPath let you test before reading. Use those for a SOAP response's wd:Hire_Date instead of reading a string and parsing it.

JSON has no date accessors: the Json member functions read strings, numbers, booleans, objects and arrays. Read the string and parse it, as Workday's Prism and Extend design-patterns sample does when it reformats a transaction date:

date.parse(data.Loop.item.stringAtJsonPath("$.transactionDate")).format("MM/dd/yyyy")

Formatting: format(pattern), toXMLDate() and toString()

format(pattern) exists on Date, LocalDateTime and ZonedDateTime. The Date reference gives ISO_LOCAL_DATE and yyyy-MM-dd as example arguments, so both a named format and a letter pattern work. ISO_LOCAL_DATE is the name of a standard Java formatter and the LocalDateTime reference writes its patterns with Java's uuuu year letter, so the safe assumption is Java's pattern letters:

  • HH is the 24-hour clock and hh the 12-hour clock. A timestamp written with hh:mm and no a marker cannot tell 02:00 from 14:00.
  • MM is the month and mm the minute.
  • yyyy is the calendar year. Avoid YYYY, which in that convention is the week-based year and differs from the calendar year in the last days of December.
  • Text in a pattern goes in single quotes, as in 'T'.

For values going back into Workday, prefer the ISO helpers over hand-written patterns. toXMLDate() on a Date returns yyyy-MM-dd. toXMLDateTime() returns the ISO local date-time for a LocalDateTime and the ISO offset date-time for a ZonedDateTime. toString() on a LocalDateTime uses the shortest ISO form that keeps the full value, so seconds disappear when they are zero; that is fine for logs and risky for a fixed-format file.

Converting between time zones

atZone(zoneId) on a LocalDateTime attaches a zone and gives you a ZonedDateTime: use it when you know which zone a zoneless timestamp was recorded in. toDate() and toLocalTime() drop the time or the date. ZonedDateTime also has an atZone(zoneId), which the reference describes only as combining the date-time with the zone. Before relying on it to convert an instant to another zone's wall-clock time, test it with a known value: log zoneddatetime.now("UTC").toString() and the same value after .atZone("America/New_York"), and check whether the hour moved or only the offset changed.

Use region IDs such as America/Chicago rather than fixed offsets like -06:00 whenever daylight saving matters. A fixed offset is correct for half the year.

What Workday's APIs send and expect

The zone handling on the Workday side differs between SOAP and REST, and an orchestration often uses both.

SOAP (Workday Web Services). Workday returns datetimes in ISO 8601 in Pacific Time, as UTC-07:00 or UTC-08:00 depending on the season. From v11 onward, inbound datetimes may carry any offset and are converted to Pacific Time; inbound date values are taken as Pacific Time regardless of offset. Workday's page notes that with the Workday 32 Time Zones feature and a non-ISU user, the user's local time zone applies instead.

REST. Response dateTime fields are in UTC with no offset, for example 2023-07-25T23:43:15.000Z. Request dateTime fields take any offset and must include minutes and seconds. Endpoints that need a zone use a dateTimeZone object with both timeZone (a standard name such as US/Eastern) and dateTime. If the timeZone is invalid, Workday defaults to GMT, and if the offset in dateTime contradicts the timeZone, the request fails. 24:00 is not accepted for midnight on either form; Workday's page says to use 23:59 (or 23:59:59).

The practical consequence: a datetime read from a SOAP response and a datetime read from a REST response for the same event will print differently. Convert both to ZonedDateTime (with zonedDateTimeAtXPath, or zoneddatetime.parse on the REST string with a pattern such as yyyy-MM-dd'T'HH:mm:ss.SSSXXX) before comparing them, and compare with isBefore, isAfter or isEqual, never by string.

Month-end arithmetic: plusMonths returns the last valid day

plusMonths, minusMonths, plusYears and minusYears on a Date return the last valid day of the month when the target day does not exist. January 31 plus one month is the last day of February; February 29 plus one year is February 28. If a schedule must stay on "the last day of the month", compute it from the month, not by adding months to a previous last day, which drifts after February.

diffDays, diffMonths and diffYears return the difference as a number. Keep date-only calculations on Date so a daylight-saving change cannot shift a count by an hour into a different day.

Dates in PDFs, files and messages

Trigger PDF Generation has its own Locale, Language and Timezone fields (examples en_US, de_DE, US/Eastern) that control how dates appear in the rendered document; see generating a PDF in Orchestrate. For CSV and fixed-width files, format each date explicitly in the mapping step with the partner's pattern rather than relying on toString().

Prove it with values that break naive code

Write the values you produce into an Add Integration Message or a log entry (see logging in Orchestrate) and run the orchestration against records chosen to expose mistakes:

  1. A worker whose effective date is the 31st of a month, to test month arithmetic.
  2. A timestamp between 00:00 and 01:00 UTC, which falls on the previous day in the Americas.
  3. A timestamp in the week a daylight-saving change happens in the target zone.
  4. A date written back to Workday, read back with the same API, and compared with what you sent.

If the fourth check returns a different day from the one you sent, look first for a Date that passed through a datetime or a zone on the way.

Sources checked