Workday RaaS (Report as a Service) Integration
What are Workday RaaS capabilities - 30-minute timeout, no pagination, 2GB output limit, workarounds?
TL;DR
- Bottom line: RaaS exposes Workday custom reports as REST/SOAP web services for outbound data extraction. Best for scheduled report pulls under 50K rows; use WQL with native pagination for anything larger.
- Key limit: 30-minute execution timeout + 2 GB output cap + zero native pagination. All three constraints interact -- a large report that exceeds any one of these fails completely.
- Watch out for: No pagination means Workday attempts to generate the entire result set in memory at once. Reports over ~50K rows routinely timeout or hit the 2 GB wall. You must implement pseudo-pagination using prompt parameter date-range chunking.
- Best for: Scheduled batch extraction of HR, payroll, and financial data under 50K rows per report run, with prompt-based filtering.
- Authentication: ISU (Integration System User) + WS-Security for SOAP; ISU + Basic Auth or OAuth 2.0 for REST. RaaS is included in all Workday subscriptions -- no additional license required.
System Profile
Workday RaaS (Reports as a Service) converts Advanced custom reports into web service endpoints accessible via both REST and SOAP protocols. It is included in every Workday subscription at no additional cost. RaaS is outbound-only -- it reads data from Workday but cannot write, update, or delete records. For write operations, use Workday SOAP Web Services or REST API.
This card covers the RaaS surface specifically. Workday offers four data access strategies: SOAP Web Services (full CRUD), REST API (modern JSON-based subset), RaaS (report-driven extraction), and WQL (Workday Query Language with native pagination). RaaS is the most commonly used for integration data extraction because it leverages existing report definitions. [src5]
| Property | Value |
|---|---|
| Vendor | Workday |
| System | Workday HCM / Financials (all modules) |
| API Surface | RaaS (Report as a Service) -- REST + SOAP |
| Current API Version | v45.0+ |
| Editions Covered | All editions -- RaaS included in standard subscription |
| Deployment | Cloud (SaaS only) |
| API Docs | Workday Community (login required) |
| Status | GA (Generally Available) |
API Surfaces & Capabilities
| API Surface | Protocol | Best For | Max Output | Pagination | Real-time? | Bulk? |
|---|---|---|---|---|---|---|
| RaaS REST | HTTPS/JSON,CSV,XML | Scheduled report extraction <50K rows | 2 GB | No | No | Yes (batch) |
| RaaS SOAP | HTTPS/XML | Multi-instance reports, large parameter sets | 2 GB | No | No | Yes (batch) |
| WQL | HTTPS/JSON | Large dataset queries with pagination | Paginated | Yes (limit/offset) | Yes | Yes |
| SOAP Web Services | HTTPS/XML (WSDL) | Full CRUD operations, complex transactions | Per-operation | Yes | Yes | No |
| REST API | HTTPS/JSON | Modern lightweight CRUD, OAuth-native apps | Per-operation | Yes | Yes | No |
RaaS REST and RaaS SOAP share the same underlying report engine and constraints. The key difference: SOAP allows request parameters in the message body (no URL length limits), while REST passes parameters as URL query strings (capped at ~2,083 characters). [src3]
Rate Limits & Quotas
Per-Request Limits
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max output size | 2 GB | All RaaS endpoints | Report terminated if output exceeds this |
| Execution timeout | 30 minutes | All RaaS endpoints | Long-running reports killed after 30 min |
| REST URL length | ~2,083 characters | RaaS REST only | Limits number of prompt parameter values via GET |
| SOAP body size | No documented hard limit | RaaS SOAP only | Practical limit is memory/timeout |
| Row threshold for reliability | ~50,000 rows | All RaaS endpoints | Reports above this frequently timeout |
Rolling / Daily Limits
| Limit Type | Value | Window | Notes |
|---|---|---|---|
| API request rate | ~10 requests/second | Per tenant, rolling | Excess requests dropped silently -- no 429 returned |
| Concurrent report executions | Not officially documented | Per tenant | Heavy concurrent RaaS loads degrade tenant performance |
| No daily API call cap | N/A | N/A | Workday does not publish a daily call limit for RaaS |
Workday-Specific Constraints
| Limit Type | Value | Notes |
|---|---|---|
| Report type restriction | Advanced custom reports only | Standard and matrix reports cannot be web-service-enabled |
| Web service enablement | Must be explicitly enabled per report | Check "Enable As Web Service" in report definition |
| XML alias requirement | Required for all report fields | Defines JSON/XML keys in output; missing aliases cause empty fields |
| Prompt parameter filtering | URL query string (REST) or SOAP body | Prompts are the only mechanism for runtime data filtering |
Authentication
| Flow | Use When | Credential Type | Refresh? | Notes |
|---|---|---|---|---|
| ISU + Basic Auth (REST) | Simple REST integrations | Username@tenant + password | N/A | Username format: ISU_username@tenant_name |
| ISU + WS-Security (SOAP) | SOAP-based integrations, Studio | UsernameToken in SOAP header | N/A | WS-Security OASIS standard |
| OAuth 2.0 (REST) | Modern SaaS integrations | Client ID + Client Secret + tokens | Yes (refresh token) | Requires API client registration in Workday |
Authentication Gotchas
- ISU username must include the tenant name in the format
username@tenant_id-- omitting the tenant suffix causes "Customer ID not specified" errors. [src1] - The ISU must be added to an ISSG with explicit domain permissions for every data source the report accesses. Missing a single domain causes "Access Denied" with no indication of which domain is missing. [src2]
- OAuth refresh tokens in Workday are non-expiring by default, but an admin can revoke them. Build token refresh logic regardless. [src4]
- Sandbox and production use different ISU credentials and different tenant endpoints. Hardcoding either will break promotion between environments. [src4]
Constraints
- 30-minute timeout: Workday terminates any RaaS report execution that exceeds 30 minutes. There is no way to extend this.
- 2 GB output cap: The maximum response payload for any RaaS endpoint is 2 GB. Reports approaching this limit often timeout before reaching it due to memory pressure.
- No native pagination: RaaS generates the entire result set in a single response. There is no page, offset, limit, or cursor parameter.
- ~10 req/s tenant-wide throttle: Workday drops requests exceeding approximately 10 per second across the entire tenant. No HTTP 429 is returned.
- REST URL length limit: GET requests via REST are constrained to ~2,083 characters.
- Advanced report type required: Only Advanced custom reports with "Enable As Web Service" checked are eligible.
- Memory pressure on tenant: Large RaaS reports consume tenant memory. Running multiple large reports concurrently degrades performance for all users.
Integration Pattern Decision Tree
START -- User needs to extract data from Workday via reports
|-- How many rows does the report return?
| |-- < 10,000 rows
| | |-- Simple filters? --> RaaS REST with JSON format
| | |-- Many filter values (>50)? --> RaaS SOAP (no URL length limit)
| | +-- Need incremental loads? --> RaaS REST + Effective_Date prompt
| |-- 10,000-50,000 rows
| | |-- Can filter by date range? --> RaaS REST with date-range pseudo-pagination
| | |-- Need full snapshot? --> RaaS SOAP (single call, monitor for timeout)
| | +-- Approaching timeout? --> Split into org-based chunks via prompt parameters
| |-- > 50,000 rows
| | |-- WQL available? --> Use WQL with native limit/offset pagination
| | |-- Must use reports? --> Mandatory pseudo-pagination (date + org chunking)
| | +-- > 200,000 rows? --> WQL pagination + parallel processing
| +-- > 1,000,000 rows
| +-- RaaS is NOT viable --> Use WQL with parallel paginated extraction
|-- What output format?
| |-- JSON --> ?format=json (recommended)
| |-- CSV --> ?format=csv (best for data lake loads)
| |-- XML --> ?format=simplexml or default
| +-- RSS/GData --> Rarely used
|-- SOAP vs REST?
| |-- Few prompt values + JSON needed --> REST
| |-- Many prompt values (>50 IDs) --> SOAP (parameters in body)
| +-- Need multi-instance in single call --> SOAP (dramatically faster)
+-- Error tolerance?
|-- Zero-loss required --> Retry with exponential backoff + dead letter queue
+-- Best-effort --> Retry 3x with 30s delays, log failures
Quick Reference
| Operation | Method | Endpoint Pattern | Format | Notes |
|---|---|---|---|---|
| Fetch report (REST/JSON) | GET | /ccx/service/customreport2/{tenant}/{owner}/{Report}?format=json | JSON | Most common pattern |
| Fetch report (REST/CSV) | GET | /ccx/service/customreport2/{tenant}/{owner}/{Report}?format=csv | CSV | Best for bulk loads |
| Fetch report (REST/XML) | GET | /ccx/service/customreport2/{tenant}/{owner}/{Report} | XML | Default if no format specified |
| Fetch with prompts | GET | .../{Report}?format=json&{Prompt}={value} | JSON | Prompts filter at runtime |
| Fetch with WID filter | GET | .../{Report}?format=json&{Field}!WID={id} | JSON | Filter by Workday ID reference |
| Fetch via SOAP | POST | /ccx/service/customreport2/{tenant}/{owner}/{Report} | XML | Parameters in SOAP body |
| WQL query (alternative) | POST | /api/wql/v1/{tenant}/data?query={WQL} | JSON | Native pagination via limit/offset |
Step-by-Step Integration Guide
1. Create and enable the custom report
Build an Advanced custom report in Workday with the required data sources, columns, and filters. Set XML aliases for every field. Enable "Web Service" in the report's Advanced tab. [src1]
Verify: Navigate to the report > Related Actions > Web Service > View URLs. You should see REST and SOAP endpoint URLs listed.
2. Configure ISU and security permissions
Create an ISU and ISSG. Add the ISU to the ISSG. Grant domain permissions (GET access) for every data source. Share the report with the ISSG. [src2]
Verify: Log in as the ISU and run the report in Workday UI. If you see data, the ISU has correct permissions.
3. Authenticate and fetch via REST
Construct the RaaS REST URL and call it with Basic Auth. [src1, src7]
curl -u "ISU_User@tenant_name:password" \
"https://wd2-impl-services1.workday.com/ccx/service/customreport2/tenant_name/report_owner/Report_Name?format=json"
Verify: HTTP 200 with JSON body containing Report_Entry array.
4. Add prompt parameters for filtering
Pass prompt values as URL query parameters to filter the report at runtime. [src7]
curl -u "ISU_User@tenant_name:password" \
"https://wd2-impl-services1.workday.com/ccx/service/customreport2/tenant_name/owner/Report?format=json&Effective_Date=2026-01-01-08:00"
Verify: Response contains only records matching the filter criteria.
5. Implement pseudo-pagination for large datasets
Since RaaS lacks native pagination, chunk requests using date-range prompts. [src1, src5]
import requests
from datetime import datetime, timedelta
BASE_URL = "https://wd2-impl-services1.workday.com/ccx/service/customreport2/tenant/owner/Report"
AUTH = ("ISU_User@tenant", "password")
def fetch_raas_chunked(start_date, end_date, chunk_days=7):
all_rows = []
current = start_date
while current < end_date:
chunk_end = min(current + timedelta(days=chunk_days), end_date)
params = {
"format": "json",
"Start_Date": current.strftime("%Y-%m-%d-08:00"),
"End_Date": chunk_end.strftime("%Y-%m-%d-08:00"),
}
resp = requests.get(BASE_URL, auth=AUTH, params=params, timeout=1800)
resp.raise_for_status()
rows = resp.json().get("Report_Entry", [])
all_rows.extend(rows)
current = chunk_end
return all_rows
Verify: Total row count matches expected count from the full report run in Workday UI.
6. Implement retry logic with throttle handling
Workday silently drops requests exceeding ~10/second. Build in delays and retries. [src6]
import time
import requests
def fetch_with_retry(url, auth, params, max_retries=5, base_delay=30):
for attempt in range(max_retries):
try:
resp = requests.get(url, auth=auth, params=params, timeout=1800)
if resp.status_code == 200:
return resp.json()
elif resp.status_code in (500, 502, 503):
delay = base_delay * (2 ** attempt)
time.sleep(delay)
else:
resp.raise_for_status()
except Exception as e:
delay = base_delay * (2 ** attempt)
time.sleep(delay)
raise Exception(f"Failed after {max_retries} retries")
Verify: Function returns JSON data. Check logs for retry attempts.
Code Examples
Python: Fetch RaaS report with Basic Auth
# Input: ISU credentials, tenant, report details
# Output: Parsed JSON report data as list of dicts
import requests # requests==2.31.0
WORKDAY_HOST = "https://wd2-impl-services1.workday.com"
TENANT = "your_tenant"
REPORT_OWNER = "ISU_Report_Owner"
REPORT_NAME = "Active_Workers_Report"
ISU_USER = f"ISU_User@{TENANT}"
ISU_PASS = "your_password"
url = f"{WORKDAY_HOST}/ccx/service/customreport2/{TENANT}/{REPORT_OWNER}/{REPORT_NAME}"
params = {"format": "json"}
response = requests.get(url, auth=(ISU_USER, ISU_PASS), params=params, timeout=1800)
response.raise_for_status()
data = response.json()
rows = data.get("Report_Entry", [])
print(f"Fetched {len(rows)} records")
JavaScript/Node.js: Fetch RaaS report
// Input: ISU credentials, tenant, report details
// Output: Parsed JSON report data
const axios = require("axios"); // [email protected]
const WORKDAY_HOST = "https://wd2-impl-services1.workday.com";
const TENANT = "your_tenant";
const ISU_USER = `ISU_User@${TENANT}`;
const ISU_PASS = "your_password";
async function fetchRaaSReport(reportOwner, reportName, promptParams = {}) {
const url = `${WORKDAY_HOST}/ccx/service/customreport2/${TENANT}/${reportOwner}/${reportName}`;
const params = { format: "json", ...promptParams };
const response = await axios.get(url, {
auth: { username: ISU_USER, password: ISU_PASS },
params,
timeout: 1800000,
});
return response.data.Report_Entry || [];
}
cURL: Quick RaaS test
# Fetch report as JSON
curl -u "ISU_User@tenant:password" \
"https://wd2-impl-services1.workday.com/ccx/service/customreport2/tenant/owner/Report?format=json"
# Fetch with prompt filter
curl -u "ISU_User@tenant:password" \
"https://wd2-impl-services1.workday.com/ccx/service/customreport2/tenant/owner/Report?format=json&Hire_Date_From=2026-01-01-08:00"
Data Mapping
Field Mapping Reference
| RaaS Output Field | External System Target | Type | Transform | Gotcha |
|---|---|---|---|---|
| Worker (WID) | External employee ID | String (32 char) | Direct or lookup | WIDs are Workday-internal; use Employee_ID for external mapping |
| Hire_Date | Hire date | DateTime | Parse ISO 8601 with timezone | Format: 2026-01-15-08:00 (non-standard ISO) |
| Worker_Type (WID) | Employment type | Reference | Map WID to enum | Different tenants have different WIDs for same type |
| Annual_Rate | Salary | Decimal | Convert currency if multi-currency | Amount in worker's local currency; no currency code by default |
| Supervisory_Organization | Department/Org | Reference | Resolve WID to name | Nested reference; requires joining to org hierarchy |
| Email_Address | Contact email | String | Filter by usage type | Report may return multiple emails; filter by Usage_Type |
Data Type Gotchas
- Workday dates include a timezone offset suffix (e.g.,
2026-01-15-08:00) that is not standard ISO 8601. Most JSON parsers will not handle this automatically. [src1] - WID references are opaque 32-character hex strings. Stable within a tenant but not portable across tenants. Map to business keys for external systems. [src2]
- Boolean fields in RaaS JSON output are returned as string
"1"or"0", not native JSON true/false. [src1] - Multi-valued fields are returned as nested arrays. A report with 10,000 workers each having 3 emails produces 30,000 email sub-records. [src7]
- Currency amounts do not include currency code by default. For multi-currency tenants, add the currency field explicitly. [src2]
Error Handling & Failure Points
Common Error Codes
| Code / Error | Meaning | Cause | Resolution |
|---|---|---|---|
| HTTP 401 | Authentication failure | Wrong ISU credentials or missing @tenant suffix | Verify username format: ISU_User@tenant_name |
| HTTP 403 | Access denied | ISU lacks domain permissions | Add ISSG to all required domain security policies |
| HTTP 404 | Report not found | Report not web-service-enabled or wrong URL | Verify "Enable As Web Service" is checked |
| HTTP 500 | Server-side failure | Report timed out or memory exceeded | Reduce data volume via filters |
| SOAP Fault: validationError | Invalid SOAP request | Malformed XML | Validate SOAP envelope against WSDL |
| SOAP Fault: Customer id not specified | Missing tenant in credentials | Username missing @tenant suffix | Format as ISU_User@tenant_name |
| Empty Report_Entry | No data returned | Prompts filtering out all records | Test in Workday UI as ISU; verify prompt values |
| Connection timeout | Report exceeds 30-min limit | Dataset too large | Implement pseudo-pagination with smaller chunks |
Failure Points in Production
- Silent request drops at rate limit: Workday drops requests exceeding ~10/second with no HTTP error. Fix:
Implement a request queue with max 8 req/s throughput and exponential backoff retry. [src6] - 30-minute timeout on growing datasets: Reports that work today timeout in months as data grows. Fix:
Implement date-range pseudo-pagination from day one; target <15 min per call. [src1] - Missing XML aliases cause silent data loss: Fields without XML aliases are silently omitted from output. Fix:
Audit every report field for XML alias before go-live. [src7] - ISU password expiration: Password rotation policies break unattended integrations. Fix:
Set ISU to non-expiring passwords or implement automated credential rotation. [src4] - Tenant-specific WID values: WIDs differ between sandbox and production. Fix:
Map by business key, not WID. [src2] - Multi-currency amounts without currency codes: Silent data corruption when amounts lack currency indicators. Fix:
Always include the currency reference field alongside amount fields. [src2]
Anti-Patterns
Wrong: Fetching entire dataset without filters
# BAD -- fetches all workers in one call; will timeout for orgs with >50K workers
response = requests.get(f"{BASE_URL}?format=json", auth=AUTH, timeout=1800)
all_workers = response.json()["Report_Entry"]
Correct: Pseudo-paginate with date-range chunking
# GOOD -- fetches in weekly chunks; each stays under timeout
all_workers = []
current = datetime(2026, 1, 1)
end = datetime(2026, 3, 1)
while current < end:
chunk_end = min(current + timedelta(days=7), end)
params = {"format": "json", "Hire_Date_From": current.strftime("%Y-%m-%d-08:00")}
resp = requests.get(BASE_URL, auth=AUTH, params=params, timeout=1800)
all_workers.extend(resp.json().get("Report_Entry", []))
current = chunk_end
Wrong: Looping REST calls for each worker
# BAD -- one REST call per worker burns through rate limit in seconds
for worker_id in worker_ids: # 5,000 IDs
resp = requests.get(f"{BASE_URL}?format=json&Worker!WID={worker_id}", auth=AUTH)
results.append(resp.json())
Correct: Single SOAP call with all worker IDs in body
# GOOD -- one SOAP call passes all IDs in body (no URL length limit)
# Reduces run time from hours to minutes
soap_body = build_soap_envelope(worker_ids)
resp = requests.post(SOAP_URL, data=soap_body, headers={"Content-Type": "text/xml"})
Wrong: No rate limiting or retry logic
# BAD -- blasts requests; Workday drops silently
for chunk in date_chunks:
resp = requests.get(f"{BASE_URL}?format=json&Date={chunk}", auth=AUTH)
Correct: Rate-limited requests with retry
# GOOD -- respects ~10 req/s limit with built-in retry
for i, chunk in enumerate(date_chunks):
if i > 0 and i % 8 == 0:
time.sleep(1.5) # Stay under 10 req/s
resp = fetch_with_retry(BASE_URL, AUTH, {"format": "json", "Date": chunk})
Common Pitfalls
- Assuming RaaS supports pagination: The most expensive mistake. Teams discover in production that a single call must return all data. Fix:
Design for pseudo-pagination from the start using prompt parameters. [src1] - Ignoring timeout during development: Reports run fast in sandbox (small data) but timeout in production. Fix:
Test with production-volume data; target <15 min per call. [src1] - Hardcoding WIDs across environments: Sandbox and production have different WIDs. Fix:
Map by business key, never by WID. [src2] - Not setting XML aliases: Fields without aliases are silently dropped from output. Fix:
Set XML aliases for every field in the report. [src7] - Using REST when SOAP would be faster: One client reduced run time from 1+ hour to 7 minutes by switching to single multi-instance SOAP call. Fix:
Use SOAP for >50 parameter values. [src3] - Running concurrent large reports: Multiple large reports compete for tenant memory, causing cascading timeouts. Fix:
Serialize large report executions via a job queue. [src6] - Omitting currency codes: Monetary fields return raw numbers without currency indicators. Fix:
Always include the currency reference field alongside amount fields. [src2]
Diagnostic Commands
# Test RaaS authentication (expect 200, 401 = bad creds, 403 = no access)
curl -s -o /dev/null -w "%{http_code}" \
-u "ISU_User@tenant:password" \
"https://wd2-impl-services1.workday.com/ccx/service/customreport2/tenant/owner/Report?format=json"
# Fetch report and check row count
curl -s -u "ISU_User@tenant:password" \
"https://wd2-impl-services1.workday.com/ccx/service/customreport2/tenant/owner/Report?format=json" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Rows: {len(d.get(\"Report_Entry\",[]))}')"
# Measure report execution time (watch for >15 min)
time curl -s -u "ISU_User@tenant:password" \
"https://wd2-impl-services1.workday.com/ccx/service/customreport2/tenant/owner/Report?format=json" \
-o /dev/null
# Verify output size (watch for approaching 2GB)
curl -s -u "ISU_User@tenant:password" \
"https://wd2-impl-services1.workday.com/ccx/service/customreport2/tenant/owner/Report?format=csv" \
-o report.csv && ls -lh report.csv
Version History & Compatibility
| API Version | Release Date | Status | Breaking Changes | Migration Notes |
|---|---|---|---|---|
| v45.0 | 2025-09 | Current | None | Latest recommended version |
| v43.0 | 2025-03 | Supported | Minor schema changes | Still fully functional for RaaS |
| v40.0 | 2024-03 | Supported | None for RaaS | WQL introduced as alternative |
| v38.0 | 2023-09 | Supported | None for RaaS | Minimum version for OAuth 2.0 |
| v35.0 | 2022-09 | End of Life | N/A | Upgrade to v38.0+ |
Workday follows a release-based versioning model (two major releases per year). API versions are supported for approximately 3 years. RaaS endpoint URLs include the API version implicitly through the tenant configuration. [src4]
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Scheduled batch extraction <50K rows per run | Dataset exceeds 50K rows and cannot be chunked | WQL with native limit/offset pagination |
| Leveraging existing Workday custom reports | Need to create/update/delete records | Workday SOAP Web Services or REST API |
| Simple outbound data feeds (HR, payroll, finance) | Need real-time event-driven notifications | Workday Business Process Events |
| ETL tools with RaaS connectors (Workato, Fivetran) | Need complex joins or aggregations | WQL (supports SQL-like queries) |
| Prompt-based filtered extractions | Need full-text search | Workday Search API |
| Quick ad-hoc data pulls during development | Production workload >10 req/s sustained | Batch via WQL with parallel workers |
Important Caveats
- RaaS limits (timeout, output size, rate) are tenant-wide and not configurable by customers. Workday does not offer enterprise tier overrides.
- The ~10 req/s rate limit is tenant-wide, not per-integration. Multiple integrations sharing a tenant share this budget.
- Sandbox environments may have different data volumes than production, causing reports that work in sandbox to timeout in production.
- Workday releases twice per year (March and September). Each release may change report behavior, field availability, or security model.
- RaaS is read-only. It cannot write data back to Workday. Bidirectional sync needs RaaS + SOAP/REST API.
- The 2 GB limit and 30-minute timeout interact: a report generating 1.8 GB may timeout at 28 minutes due to memory pressure being the practical bottleneck.