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 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]
| 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 |
| 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 |
| 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 |
| 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 |
username@tenant_id -- omitting the tenant suffix causes "Customer ID not specified" errors. [src1]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
| 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 |
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.
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.
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.
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.
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.
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.
# 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")
// 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 || [];
}
# 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"
| 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 |
2026-01-15-08:00) that is not standard ISO 8601. Most JSON parsers will not handle this automatically. [src1]"1" or "0", not native JSON true/false. [src1]| 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 |
Implement a request queue with max 8 req/s throughput and exponential backoff retry. [src6]Implement date-range pseudo-pagination from day one; target <15 min per call. [src1]Audit every report field for XML alias before go-live. [src7]Set ISU to non-expiring passwords or implement automated credential rotation. [src4]Map by business key, not WID. [src2]Always include the currency reference field alongside amount fields. [src2]# 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"]
# 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
# 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())
# 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"})
# BAD -- blasts requests; Workday drops silently
for chunk in date_chunks:
resp = requests.get(f"{BASE_URL}?format=json&Date={chunk}", auth=AUTH)
# 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})
Design for pseudo-pagination from the start using prompt parameters. [src1]Test with production-volume data; target <15 min per call. [src1]Map by business key, never by WID. [src2]Set XML aliases for every field in the report. [src7]Use SOAP for >50 parameter values. [src3]Serialize large report executions via a job queue. [src6]Always include the currency reference field alongside amount fields. [src2]# 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
| 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]
| 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 |