Building integrations between a classic relational database ERP like PeopleSoft and a modern cloud-based Identity and Access Management (IAM) system like Okta always reveals a clash of architectural eras. PeopleSoft was designed in the early 90s around transactional database commits, sequential batch schedules, and long-lived system sessions. Okta is built around REST APIs, JSON payloads, and dynamic user contexts.
In this case study, I’ll walk through the architectural design of a custom outbound provisioning engine we built to sync user lifecycle events from PeopleSoft Campus Solutions and HCM into Okta.
Instead of writing a simple sequential script, we designed an asynchronous, queue-based architecture using Integration Broker that handles provisioning at scale without blocking batch schedules or falling victim to external API timeouts.
We were working with a large higher education institution where PeopleSoft serves as the system of record for students, faculty, staff, and contractors. When a student is admitted, a staff member is hired, or an employee updates their preferred name, those changes need to reflect in Okta in real time (or very close to it) so that downstream applications (like Office 365, Canvas, and Zoom) stay in sync.
The challenge: How do you make a single-threaded PeopleSoft environment orchestrate outbound REST API calls reliably, securely, and at high volume?
We considered two primary ways to drive the outbound API calls.
The simplest path would be an Application Engine (AE) batch process that queries a view of “changed users,” loops through each record, constructs a REST message, sends it, and waits for the HTTP response.
Instead of making the HTTP calls inside the App Engine loop, we split the work. The App Engine process acts solely as a “publisher.” It queries the changed records and quickly writes “provisioning requests” to custom queue tables.
An Integration Broker (IB) handler subscribes to these requests, picks up the messages asynchronously, and dispatches them.
flowchart TD
AppEngine["App Engine / Component\n(Event Publisher)"] -->|Writes Requests| Queue["Queue Tables\n(PS_CHG_OK_REQ_HDR/DTL)"]
Queue -->|Local-to-Local Routing| IB["Integration Broker\nQueue"]
IB -->|Concurrently Dispatched| Sub1["OnNotify Handler\nUser 1"]
IB -->|Concurrently Dispatched| Sub2["OnNotify Handler\nUser 2"]
IB -->|Concurrently Dispatched| SubN["OnNotify Handler\nUser N"]
Sub1 -->|Apache HttpClient| Okta["Okta API"]
Sub2 -->|Apache HttpClient| Okta
SubN -->|Apache HttpClient| Okta
To implement the async queue, we built two custom tables.
Header Table: PS_CHG_OK_REQ_HDR
CHG_BATCH_ID — Unique key for the batch runCHG_BATCH_STATUS — Batch status enum (NEW, INPR (In Progress), COMP (Complete), ERR (Error))CREATED_DTTM — Timestamp when the batch was queuedLASTUPDDTTM — Last updated timestampDetail Table: PS_CHG_OK_REQ_DTL
CHG_BATCH_ID, LINE_NBR — Compound key linking to the headerOPRID — The PeopleSoft user ID being provisionedCHG_LINE_ACTION — The API action: CR8I (Create Inactive), CR8A (Create Active), GRPA (Group Add), GRPR (Group Remove), USRA (Activate), USRD (Deactivate), USAU (Update Attributes)CHG_LINE_STATUS — Status enum (NEW, INPR, COMP, ERR, WARN)ERROR_MSG — Stack trace or error response body from OktaCHG_OKTA_ID — The cached Okta GUID (populated after successful creation)Every user in Okta is identified by a unique, random 20-character GUID (e.g., 00u2lvzhsoCLMZSGDJBY). Okta’s API endpoints require this GUID in the URL path for all update, activation, or group operations:
POST /api/v1/users/{okta_id}/lifecycle/activate
PUT /api/v1/users/{okta_id}
If PeopleSoft only knows a user’s local identifier (like EMPLID or email) and needs to update their record, it must first execute a search query to Okta (GET /api/v1/users?filter=profile.email eq "user@univ.edu") to retrieve the GUID.
Doing a search before every write doubles your API call volume, increases latency, and burns through your Okta rate limits.
Our Solution: Cache the Okta GUID locally in a custom cross-reference table the first time the account is created or matched.
Table: PS_CHG_OPR_OKTA_ID
OPRID — PeopleSoft Operator IDCHG_IDP_UTYPE — Identity Type (Student, Faculty, Staff, Adjunct)CHG_OKTA_ID — The cached Okta GUIDCHG_OKTA_LOGIN_ID — The primary login emailLASTUPDDTTM — Timestamp of last syncSubsequent runs join directly against this cross-reference table, retrieving the cached GUID so we can call the update endpoints directly.
A one-size-fits-all provisioning strategy does not work in an enterprise. Different populations require different login formats, email sync policies, and group memberships. We defined these rules in a custom setup table PS_CHG_IDP_UTYPE:
| Affiliation | Login Format | Primary Email Type | Sync Phone | Init State | Okta Group Rules |
|---|---|---|---|---|---|
| Student | [EMPLID]@student.univ.edu |
Home / Preferred | No | STAGED |
Add to Student Cohort |
| Staff | [First].[Last]@univ.edu |
Business | Yes | ACTIVE |
Add to Staff General |
| Faculty | [First].[Last]@univ.edu |
Business | Yes | ACTIVE |
Add to Faculty General |
| Adjunct | [First].[Last]-ADJ@univ.edu |
Personal | No | STAGED |
Add to Adjunct Pool |
When the sync engine builds details for a user, it queries the setup table to dynamically determine the payload structure and target state.
During the implementation, we hit several significant limitations in the PeopleTools platform.
The Problem: The native Integration Broker Document Technology JSON parser has a critical bug: it silently discards null values. If you attempt to parse {"email": null, "phone": "555-1234"}, the parser strips the email property from the structure entirely. In provisioning, setting a field to null is a deliberate command to clear it; stripping the field makes it look like the attribute was omitted, preventing updates.
The Workaround: We bypassed the Document Technology parser. Instead, we used the native PeopleTools JsonParser and JsonObject classes introduced in PeopleTools 8.56, which preserve null values correctly. We also noted that the delivered Campus Community JSON wrapper (SCC_COMMON:JSON:JSONObject) in Campus Solutions is useful but has its own bugs, so we stuck to standard, native JsonParser methods.
The Problem: When you make an outbound REST call using the standard %IntBroker.ConnectorRequest wrapper, if the server returns a non-2xx error code (like 400 Bad Request or 429 Too Many Requests), the wrapper throws a generic exception. It does not return the response body containing Okta’s detailed error payload (e.g., "login: login must be in the form of an email address"). This makes troubleshooting impossible.
The Workaround: We bypassed Integration Broker’s outbound target connector for the REST API calls. Instead, we called the Java Apache Commons HttpClient library directly from our PeopleCode Application Classes. Because this library runs directly in the JVM of the application server, we can retrieve both the raw status code and the error response body on failure.
The Problem: Some developers believe PeopleTools cannot parse top-level JSON arrays (like [{...}, {...}] returned by search endpoints) because older PeopleSoft classes or legacy Campus Community scripts struggled with them. In the past, this led to bizarre workarounds like deploying external web services (such as a Go microservice on Heroku to parse JSON and translate it to XML).
The Workaround: We proved this translation step was an unnecessary security risk and performance bottleneck. The native JsonParser class in PeopleTools 8.56+ handles top-level JSON arrays natively. If the root of the JSON payload is an array, you parse it, retrieve the root object, and call GetJsonArray(""):
Local JsonParser &parser = CreateJsonParser();
If &parser.ParseJSONString(&jsonResponse) Then
Local JsonObject &root = &parser.GetRootObject();
Local JsonArray &userList = &root.GetJsonArray("");
/* Loop through users natively */
End-If;
This eliminated the external Heroku dependency entirely, keeping all sensitive user data inside the PeopleSoft security boundary.
The Problem: During JIT (Just-in-Time) provisioning, we needed to pass a user’s initial password to Okta. However, PeopleSoft secures the password input on the standard sign-in page, encrypting it before it can be read in Signon PeopleCode.
The Workaround: A custom JavaScript hook was added to the signin.html page template to copy the plain text password into a temporary, hidden form field (chgPasswordHidden) right at the moment of submission. This field was read during the sign-on event handler to provision the password to Okta.
Important Security Caveat: While this workaround functionalized password sync, it introduces a security risk by exposing plaintext passwords to the browser DOM. A far better, modern approach is to configure Okta as the primary Identity Provider (IdP) and use SAML or OpenID Connect (OIDC) redirection. This allows users to log in directly through Okta, eliminating the need for PeopleSoft to ever touch or store their passwords.
The Problem: Okta enforces tenant-wide API rate limits. During a large-scale student registration batch, if PeopleSoft fires API requests too aggressively, it can exhaust the tenant’s API limits. This does not just throttle the provisioning sync—it can block active end-user logins across the entire organization.
The Workaround: Our Application Class wrappers read the rate limit headers (X-Rate-Limit-Limit, X-Rate-Limit-Remaining, X-Rate-Limit-Reset) returned in the response of every Apache HttpClient call. If the remaining API quota drops below a safe percentage (e.g., 20%), the code dynamically throttles execution, sleep-waiting in PeopleCode until the reset window clears.
The provisioning engine is built as a reusable SDK inside a PeopleSoft Application Package (CHG_OKTA):
CHG_OKTA:USER:userModel — Represents the Okta user object schema, mapping PeopleSoft fields to JSON properties.CHG_OKTA:USER:userOperations — Implements the API methods using direct Apache HttpClient calls (createUser, updateUser, deactivateUser, getUserByEmail).CHG_OKTA:UTILS:helper — Handles credential decryption and environment checks. It looks up the API keys indexed by database name (%DbName), ensuring a DEV database refreshed from PROD automatically switches to the DEV Okta tenant and API keys.One of the biggest wins of this architecture was the custom User Provisioning Dashboard page we built. Because we write every transaction state to PS_CHG_OK_REQ_DTL, system administrators do not need access to the Integration Broker Message Monitor to troubleshoot failures.
The dashboard allows admins to:
Complete, In-Process, Error).NEW to trigger a retry.Apache Commons HttpClient directly in PeopleCode.JsonParser is highly capable.%DbName in your configuration tables. This prevents refreshed non-production databases from talking to your production cloud tenants.Chris Malek is a PeopleTools® Technical Consultant with over two decades of experience. 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.
Look up any record, field, page, or component, audit security, and monitor Integration Broker across every database — in seconds.
A web console built for the PeopleSoft community — operational monitoring, security auditing, and metadata browsing in one tool.
On-demand security and operational reports for your PeopleSoft environment — no client install required.
Research any PeopleSoft object and monitor system health from a single browser tab — no App Designer, no SQL.