A University ran both PeopleSoft Campus Solutions and Workday HCM. The Campus system needs certain employee data (job records, biographical changes, new hires) to be synced from Workday on a regular basis. The team built a reusable integration framework using Workday’s Report as a Service (RAAS) endpoints and PeopleSoft App Engine to automate this data flow.
This case study documents the reusable RAAS Runner framework — a generic App Engine and PeopleCode base class that solved a common integration problem: how to call Workday reports repeatedly, process the XML responses, and keep multiple data flows in sync without writing boilerplate code for each one.
{{LASTRUNDATE}}, {{CURRENTDATE_MINUSDAYS_5}}) solve the “what changed” problem — each run only fetches recent changes, keeping response sizes small.RAAS stands for “Report as a Service.” Workday exposes every saved report — both custom and system — as an HTTP endpoint that returns XML, JSON, CSV, or other formats.
Custom reports:
https://{host}/ccx/service/customreport2/{tenant}/{owner}/{report_name}
System reports:
https://{host}/ccx/service/systemreport2/{tenant}/{report_name}?format=xml
Example: fetching a custom report called “Employee Job Data” from a custom report owner:
GET https://{host}/ccx/service/customreport2/{tenant}/HR_Reports/Employee_Job_Data
I chose the Basic Auth approach for simplicity. The Workday user account used for RAAS should be a service account with only the necessary permissions to run the reports.
Workday RAAS returns XML with a <wd:Report_Data> root element containing <wd:Report_Entry> rows:
<?xml version="1.0" encoding="UTF-8"?>
<wd:Report_Data>
<wd:Report_Entry>
<wd:Employee_ID>12345</wd:Employee_ID>
<wd:First_Name>Jane</wd:First_Name>
<wd:Last_Name>Smith</wd:Last_Name>
<wd:Hire_Date>2023-01-15</wd:Hire_Date>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:Employee_ID>12346</wd:Employee_ID>
<wd:First_Name>John</wd:First_Name>
<wd:Last_Name>Doe</wd:Last_Name>
<wd:Hire_Date>2023-02-01</wd:Hire_Date>
</wd:Report_Entry>
</wd:Report_Data>
RAAS returns all matching rows in a single response. There is no pagination or streaming API. For large datasets, this is mitigated by using date-range parameters in the report query string so that each call only retrieves recent changes.
sequenceDiagram
box "PeopleSoft"
participant Daemon as Daemon App Engine
participant Config as Config Table
participant Processor as Custom Processor
participant PS as PS Tables
end
participant Workday as Workday RAAS
Daemon->>Config: Poll for active reports
Config-->>Daemon: Report config + auth URL
Daemon->>Workday: HTTP GET + Basic Auth
Workday-->>Daemon: XML Response
Daemon->>Processor: Invoke processor class
Processor->>Processor: Parse XML
Processor->>PS: Write/update records
PS-->>Processor: Success
Processor->>Config: Update last run date
The architecture is:
X_WD_RAAS_D) runs continuously on a configurable heartbeat (e.g., every 30 minutes).X_WD_RAAS_CONF) defines which reports to run, their parameters, and which PeopleCode processor to use.The framework is built on two pieces: a configuration table that defines what to run, and a PeopleCode base class that handles the boilerplate of how to run it.
PS_X_WD_RAAS_CONFThis table drives the entire integration. Each row defines one report to fetch and process.
| Field | Purpose |
|---|---|
DATABASE_NAME |
Scopes config to a PS database name (e.g., DEV, TST, PRD). Prevents a test environment from accidentally calling production Workday. Required key. |
WD_RAAS_GUID |
System-generated UUID, primary key. Matches config row to log rows. |
REPORT_NAME |
Workday report name in the format OWNER/REPORT_NAME (no leading slash, no query string). Example: HR_Reports/Employee_Job_Data. |
RUN_TIME_PARAMS |
URL query string parameters as key=value&key=value (no leading ?). Supports dynamic tokens like {{LASTRUNDATE}} (see below). |
ACTIVE_YN |
Boolean. Only active reports are run by the daemon. |
FAILURE_NOTIFY_EMAIL |
Optional. Email address to notify if the report run fails. |
OUTPUT_FORMAT |
XML (recommended), CSV, JSON, or Simple XML. Must match what the Workday report produces. |
OUTPUT_DEST_TYPE |
Either APPCLASS (invoke a PeopleCode processor class) or FILE (write raw response to PS_HOME/datafiles/). |
OUTPUT_CLASS_PATH |
If OUTPUT_DEST_TYPE = APPCLASS, the fully qualified class path (e.g., COMPANY:RAASJOBProcessor). |
OUTPUT_FILE_NAME |
If OUTPUT_DEST_TYPE = FILE, the filename (relative to PS_HOME/datafiles/) to write the raw response to. |
Query string parameters can include placeholders that are substituted at runtime. This is how you achieve incremental polling without having to update the config each time.
All tokens are case-sensitive.
| Token | Substitution |
|---|---|
{{DATE}} |
Current date in YYYY-MM-DD format. |
{{DATETIME}} |
Current date and time in YYYY-MM-DD-HHmmss format. |
{{LASTRUNDATE}} |
Last successful run date for this report in YYYY-MM-DD format. On first run, defaults to 30 days ago. |
{{LASTRUNDATETIME}} |
Last successful run date and time in YYYY-MM-DD-HHmmss format. |
{{CURRENTDATE_MINUSDAYS_n}} |
Current date minus n days. Example: {{CURRENTDATE_MINUSDAYS_5}} = 5 days ago. |
{{CURRENTDATE_PLUSDAYS_n}} |
Current date plus n days. |
{{LASTRUNDATE_MINUSDAYS_n}} |
Last successful run date minus n days. Useful for overlap windows. |
{{LASTRUNDATETIME_MINUSMINUTES_n}} |
Last successful run date/time minus n minutes. For high-frequency reports. |
A report to fetch all job changes since the last run:
DATABASE_NAME: PRD
REPORT_NAME: HR_Reports/Employee_Job_Data
RUN_TIME_PARAMS: Entry_Moment_From={{LASTRUNDATE}}&Entry_Moment_To={{DATE}}
OUTPUT_FORMAT: XML
OUTPUT_CLASS_PATH: COMPANY_WORKDAY:RAASJOBProcessor
ACTIVE_YN: Y
A report to fetch future hires in the next 30 days (daily check):
DATABASE_NAME: PRD
REPORT_NAME: HR_Reports/Future_Hires
RUN_TIME_PARAMS: Hire_Date_From={{DATE}}&Hire_Date_To={{CURRENTDATE_PLUSDAYS_30}}
OUTPUT_FORMAT: XML
OUTPUT_CLASS_PATH: COMPANY_WORKDAY:RAASFutureHireProcessor
ACTIVE_YN: Y
PS_X_WD_INT_CONFOne separate table stores Workday connection details per environment:
| Field | Purpose |
|---|---|
DATABASE_NAME |
Key. Scopes auth config to a PS database. |
WD_BASE_URL |
Base URL, e.g., https://wd2-impl-services1.workday.com/ccx/service/. |
WD_USERID |
Workday user ID for Basic Auth, often in the form SERVICE_ACCOUNT@{tenant}. |
WD_PASSWORD |
Encrypted password. |
The daemon looks up the appropriate base URL and credentials based on the current database name, ensuring each environment only calls its corresponding Workday tenant.
Ad-hoc runner (X_WD_RPTRUN):
{host}/psp/{database}/EMPLOYEE/SA/c/X_WORKDAY.X_WD_RPTRUN.GBLDaemon runner (X_WD_RAAS_D):
PS_X_WD_RAAS_CONF for all active reports and runs them sequentially.The framework separates framework logic (fetching, retrying, logging) from business logic (parsing the XML, deciding what to do with each row). You write a custom processor to implement the business logic.
All processors extend a base class provided by the framework:
import COMPANY_WORKDAY_INTEGRATION:RAASResultProcessor;
class MyRAASProcessor extends COMPANY_WORKDAY_INTEGRATION:RAASResultProcessor
method processRAASData();
end-class;
method processRAASData
Local string &rawXML = %Super.RAASData;
Local Record &config = %Super.RAASRunner;
/* Parse the XML and process each row. */
/* Write to PS tables, call Component Interfaces, etc. */
%super.appendToLog("Processed 42 records");
end-method;
When you extend RAASResultProcessor, you have access to:
| Member | Type | Purpose |
|---|---|---|
%Super.RAASData |
String | The raw XML response from Workday (unparsed). Parse this yourself using PeopleCode’s XML or JSON parsing APIs. |
%Super.RAASRunner |
Record | The config table row that triggered this report run. Useful if you need to read custom fields you added to the config. |
%super.appendToLog(&msg) |
Method | Writes a message to the run log table. Appears in the admin page and in the log record. |
%super.debugLog(&msg) |
Method | Writes a debug message if debug mode is enabled. Useful for verbose output without cluttering the main log. |
%super.convertWorkdayDateStringToDate(&wd_date_str) |
Method | Parses a Workday date string (YYYY-MM-DD format) and returns a Date. Handles edge cases like null values. |
method processRAASData
Local string &xml = %Super.RAASData;
Local XmlDoc &doc = CreateXmlDoc(&xml);
Local XmlNode &root = &doc.DocumentElement;
Local int &entry_count = 0;
For &node In &root.ChildNodes
If &node.NodeName = "wd:Report_Entry" Then
Local string &emp_id = &node.SelectSingleNode("wd:Employee_ID").Value;
Local string &hire_date_str = &node.SelectSingleNode("wd:Hire_Date").Value;
Local date &hire_date = %super.convertWorkdayDateStringToDate(&hire_date_str);
/* Update or insert PS_X_WD_JOB */
Local Record &job = CreateRecord(Record.PS_NWDRAAS_JOB);
&job.EMPLID.Value = &emp_id;
&job.EMPL_RCD.Value = 0;
&job.HIRE_DATE.Value = &hire_date;
&job.InsertOrUpdate();
&entry_count = &entry_count + 1;
End-If;
End-For;
%super.appendToLog("Processed " | &entry_count | " job records");
end-method;
The framework includes several pre-built processors for common scenarios:
For a new integration, you can often start with one of these and extend or clone it rather than writing from scratch.
PS_X_WD_RAAS_LOGOne row per run per report:
| Field | Purpose |
|---|---|
WD_RAAS_GUID |
FK to config table. Links to the report definition. |
RUN_DTTM_START |
When the run started. |
RUN_DTTM_END |
When the run completed. |
ENTRY_COUNT |
Number of rows processed. |
STATUS |
SUCCESS, ERROR, PARTIAL, SKIPPED. |
LOG_MSG |
Text log messages written by the processor using %super.appendToLog(). |
ERROR_MSG |
If status is ERROR, the exception message. |
PS_X_WD_API_LOGRaw HTTP request and response details:
| Field | Purpose |
|---|---|
API_LOG_ID |
Unique key. |
RUN_DTTM |
When the API call was made. |
METHOD |
HTTP method (GET, POST, etc.). |
URL |
The full URL that was called (with parameters substituted). |
REQUEST_BODY |
For POST calls, the request body. For GET, usually empty. |
HTTP_STATUS |
HTTP response code (200, 401, 404, 500, etc.). |
RESPONSE_BODY |
The raw response. Useful for debugging malformed XML or auth errors. |
The framework provides a component where admins can:
Navigate to: {host}/psp/{database}/EMPLOYEE/SA/c/X_WORKDAY.X_WD_RAAS_LOG.GBL
The team chose a pull architecture (PeopleSoft polls Workday) rather than push (Workday sends events to PeopleSoft).
Reasons:
Trade-off: Data latency. With polling every 30 minutes, job changes in Workday may not appear in PeopleSoft for up to 30 minutes. For most HR use cases, this is acceptable.
RAAS is simpler than Workday’s newer REST APIs.
RAAS advantages:
Workday API advantages:
For this integration, the simplicity of RAAS outweighed the API advantages.
Rather than creating a separate Process Scheduler job for each report, the team built a single Daemon that polls the config table and runs all active reports.
Advantages:
Trade-off: All reports run sequentially. If one report takes 10 minutes and the daemon heartbeat is 15 minutes, reports will queue up. Mitigation: tune the heartbeat interval based on the longest-running report, or restructure the daemon to run reports in parallel (more complex).
RAAS has no pagination. Reports return all matching rows in one response.
Mitigation: Use date-range parameters in the query string so that each report run only fetches “recent” data.
For example, instead of fetching all job records and diffing against the prior run, the report is configured to fetch job records with an entry date between {{LASTRUNDATE}} and {{DATE}}. Workday indexes these reports on entry date, so filtering at query time is efficient.
First-run caveat: The {{LASTRUNDATE}} token defaults to 30 days ago on first run. If your Workday instance has 2 years of historical data and the report returns 100,000 rows, the first run will be slow. Plan for this — run it during a maintenance window, or manually set LASTRUNDATE in the config to a more recent starting point.
Configuration is scoped by database name (DATABASE_NAME column in config tables). This is a pattern discussed in PeopleSoft as an HTTP Client.
Benefit: Dev and test environments can run the same App Engine code and config setup without any risk of hitting production Workday. The daemon looks up which environment it’s running in, and only calls the corresponding Workday tenant.
Implementation: At startup, the daemon reads DATABASE_NAME from the PeopleTools configuration and uses it as a filter key when querying PS_X_WD_RAAS_CONF and PS_X_WD_INT_CONF.
Before building the full framework, test with a single, stable Workday report (e.g., a count of active employees). This proves out:
Only after validating the basics should you build the processor class library and multi-report scheduling.
The {{LASTRUNDATE}} token defaults to 30 days in the past on the first run. If your Workday instance has a large backlog of historical data, this can cause the first run to fetch 100,000+ rows and take hours.
Mitigation:
LASTRUNDATE in the config to a more recent starting point.Workday RAAS has no built-in error envelope. A 200 HTTP response with an empty <wd:Report_Data/> is technically valid but may indicate a misconfigured report or broken data extraction.
Best practice: Always log the row count in the processor. If it ever drops to zero unexpectedly, the admin can investigate the Workday report and the raw API log to see what went wrong.
The daemon heartbeat interval (how frequently it checks for active reports) should match your data freshness requirements.
Not all reports need to run at the same frequency. If you need different intervals, the daemon can be extended to read a RUN_INTERVAL_MINUTES column in the config and schedule reports independently.
Workday is a multi-tenant SaaS, and credentials can be rotated by the HR team without IT’s knowledge. If the X_WD_INT_CONF table contains expired credentials, every RAAS call will return HTTP 401.
Mitigation:
FAILURE_NOTIFY_EMAIL immediately on the first failure, not after three retries.Chris Malek is a PeopleTools® Technical Consultant with over two decades of experience working on PeopleSoft enterprise software projects. He is available for consulting engagements.
Work with ChrisSWS turns SQL into production REST APIs — ready for AI, modern apps, and partner integrations. One install, unlimited potential.
A powerful PeopleSoft bolt-on that makes REST web services easy. You bring the SQL, SWS handles the rest.
Traditional PeopleSoft web services cost $3,600–$13,000 each to develop. SWS deploys production REST APIs in under 5 minutes through configuration alone.
Turn PeopleSoft data into clean REST APIs for AI integrations, modern applications, and vendor data feeds. Configuration-driven — no PeopleCode required.