This case study describes how a large multi-campus university system replaced a decade of legacy sign-on PeopleCode with an event-driven, asynchronous provisioning pipeline. The redesign moved every side-effect – account creation, role reconciliation, entitlement updates, email sync – off the login path and onto a local-to-local Integration Broker queue fed by inbound webhooks from an external identity manager.
The population is non-trivial: roughly 73,000 active students across four campuses, plus staff, applicants, and parents. The system of record for identity is Oracle IDM. Authentication is SAML via an Appsian/Pathlock firewall product. Boomi sits between PeopleSoft and IDM as a thin HTTP proxy.
This case study does not re-cover the mechanics of local-to-local async queues – see Async Services for Concurrent Processing for the generic pattern. Here, the focus is on what’s specific to inbound event-driven provisioning: webhook receivers, just-in-time reconciliation, producer-agnostic queues, and the deduplication logic you need when a fast-moving upstream system emits bursts of events for the same subject.
In the legacy design, sign-on PeopleCode did everything. On every first login, PeopleCode read SAML headers from the Appsian firewall, then made an outbound LDAP bind to Oracle IDM during the login transaction to fetch the user’s roles, campus affiliation, and entitlements. If IDM answered, the account was provisioned on the fly into PSOPRDEFN, PSROLEUSER, and PS_OPR_DEF_TBL_CS, roles were hard-coded in PeopleCode by institution, and the login completed. If IDM didn’t answer, the user couldn’t log in.
sequenceDiagram
actor User
participant SSO as SSO Server
participant Login as PS Login Page
participant SOPC as Signon PeopleCode
participant LDAP as LDAP / IDM
participant Sec as Security Tables
User->>SSO: Login PeopleSoft
SSO-->>User: Redirect with SAML token
User->>Login: SAML token
Login->>Login: Validate token
Login->>SOPC: Pass SAML assertions
Note over SOPC,Sec: Provisioning during login blocks authentication
SOPC->>LDAP: Fetch roles / affiliation
LDAP-->>SOPC: User attributes
SOPC->>Sec: Create / update account
SOPC->>SOPC: Resolve OPRID
SOPC-->>User: PS_TOKEN cookie
Three problems compounded over the years:
Four parallel authentication paths had accumulated – native PeopleSoft password, SAML/IDM, mobile LDAP, and a deprecated legacy LDAP – each with its own provisioning quirks. Each was a cost center.
The guiding principle of the redesign is simple: the login path does no I/O to external systems and manipulates no security data. Sign-on PeopleCode’s only job is to resolve an OPRID from the SAML assertion. Every lifecycle side-effect is moved to an asynchronous pipeline that runs independently.
sequenceDiagram
actor User
participant SSO as SSO Server
participant Login as PS Login Page
participant SOPC as Signon PeopleCode
User->>SSO: Login PeopleSoft
SSO-->>User: Redirect with SAML token
User->>Login: SAML token
Login->>Login: Validate token
Login->>SOPC: Pass SAML assertions
SOPC->>SOPC: Resolve OPRID from PSOPRDEFN
SOPC-->>User: PS_TOKEN cookie
Note over SOPC: No external calls. No security mutation.
The new SAML sign-on PeopleCode (implemented as an Application Class plugged into the Appsian user-mapping framework) is roughly:
method LookupPeopleSoftUserID
/+ &inNameID as String +/
/+ Returns String +/
&emplid = RTrim(%Request.GetHeader(&c.SAMLAttributeEMPLID));
&idmGUID = RTrim(%Request.GetHeader(&c.SAMLAttributeGUID));
&idmCampus = RTrim(%Request.GetHeader(&c.SAMLAttributeCampus));
Local Record &recOperDefn = CreateRecord(Record.PSOPRDEFN);
&recOperDefn.OPRID.Value = &c.OPRIDPrefix | &emplid;
If &recOperDefn.SelectByKey() And &recOperDefn.ACCTLOCK.Value = 0 Then
If &emplid = &recOperDefn.EMPLID.Value Then
&resolvedOPRID = &recOperDefn.OPRID.Value;
End-If;
End-If;
Return &resolvedOPRID;
end-method;
That’s the whole happy path: read three SAML headers, key into PSOPRDEFN, return the OPRID. No LDAP bind. No outbound HTTP. No role mutation. If the account isn’t provisioned, the login fails fast and the provisioning pipeline – running independently – will have created it (or will create it shortly) via an IDM event.
The trade-off is explicit and worth stating: a brand-new user who authenticates before their provisioning event has been processed will see a login failure. In production this gap is measured in seconds. The design accepts that narrow race window as the cost of never blocking a login on IDM availability.
ApplicantAccountModel class that creates the OPRID on first login. This is documented and scoped – it is not a reversion to the legacy “do everything at login” pattern.The provisioning pipeline has four pieces:
Z_IDM_EVENTS, one row per event.Z_IDM_EVENT_ASYNC.
flowchart LR
subgraph IDMSide["Identity Management"]
IDM["IDM\n(system of record)"]
IDMEvents["IDM Event Push"]
IDMAPI["IDM User GET API"]
IDM --> IDMEvents
IDM --> IDMAPI
end
subgraph BoomiSide["Boomi (HTTP proxy)"]
BoomiIn["Event forwarder"]
BoomiOut["User GET proxy"]
end
subgraph PSSide["PeopleSoft"]
Webhook["Webhook Receiver\n(REST service op)"]
Staging["Staging Table\nZ_IDM_EVENTS"]
Queue["Async Queue\n(local-to-local)"]
Worker["OnNotify Handler"]
OPRID["PSOPRDEFN"]
Roles["PSROLEUSER"]
Prefs["PS_OPR_DEF_TBL_CS"]
Email["PS_EMAIL_ADDRESSES"]
ExtID["PS_EXTERNAL_SYSTEM"]
QryTool["Query Event Creator"]
CompEvents["Component Save-Events"]
end
IDMEvents --> BoomiIn
BoomiIn --> Webhook
Webhook --> Staging
Webhook --> Queue
Queue --> Worker
Worker --> BoomiOut
BoomiOut --> IDMAPI
Worker --> OPRID
Worker --> Roles
Worker --> Prefs
Worker --> Email
Worker --> ExtID
Worker --> Staging
QryTool --> Webhook
CompEvents --> Webhook
The sequence is worth drawing out explicitly because it surfaces the design’s central property: the event producer never waits on PeopleSoft to do any real work.
sequenceDiagram
participant IDM as Oracle IDM
participant Boomi
participant Webhook as PS Webhook
participant Table as Z_IDM_EVENTS
participant Queue as IB Queue
participant Handler as OnNotify Handler
IDM->>Boomi: POST event (GUID)
Boomi->>Webhook: POST Z_IDM_EVENT.v1
Webhook->>Table: INSERT status=NEW
Webhook->>Queue: IntBroker.Publish
Webhook-->>Boomi: 200 eventID
Boomi-->>IDM: 200
Note over Queue,Handler: ...later, on a handler thread...
Queue->>Handler: OnNotify eventID
Handler->>Table: lookup payload
Handler->>Boomi: GET StudentProfile
Boomi->>IDM: GET getuserprofile
IDM-->>Boomi: userProfile + entitlements
Boomi-->>Handler: JSON
Handler->>Handler: reconcile PSOPRDEFN, PSROLEUSER, etc.
Handler->>Table: status=COMP + log
Everything above the dashed note happens in the HTTP round-trip. Everything below it happens independently on an application-server thread. The two halves share nothing except a row in Z_IDM_EVENTS.
The receiver is a REST-provided service operation with one responsibility: persist the event and acknowledge. Here is an illustrative call:
POST https://{host}/PSIGW/RESTListeningConnector/{node}/Z_IDM_EVENT.v1/
Authorization: Basic {token}
Content-Type: application/json
{
"userProfile": {
"userLogin": "6998789647",
"userISISID": "00827280"
}
}
The response is a bare acknowledgment:
HTTP/1.1 200 OK
Content-Type: application/json
{
"eventID": "0abe91b5-6324-42b3-808b-8ca8a051d72e",
"message": "The event was queued."
}
Receiver logic, in order:
Z_IDM_EVENTS with status NEW and the raw JSON.Z_IDM_EVENT_ASYNC local-to-local async service operation with just the event ID.The receiver is deliberately thin. If the only way to reliably acknowledge an event is to keep the acknowledgment path as close to zero work as possible, then that’s where the work shouldn’t be.
Z_IDM_EVENTS is a business-level log that lives alongside – not inside – Integration Broker’s message monitor. It mirrors the pattern from the generic async-processing case study, with identity-specific fields added for audit:
| Field | Purpose |
|---|---|
Z_IDM_EVENT_ID |
Unique event ID (UUID) |
IBTRANSACTIONID |
Links to IB message monitor |
Z_IDM_EVENT_STAT |
NEW, QUED, COMP, ERR, WARN, CANC |
Z_EVENT_PAYLOAD |
Raw inbound JSON |
Z_IDM_DATA |
IDM GET response captured at processing time |
Z_EVENT_LOG |
Long-form processing log |
Z_OPRID_CREATED, Z_ROLES_ADDED, Z_ROLES_REMOVED |
Audit counters |
Z_DRY_RUN |
Event was processed without writes |
Z_IDM_EVENT_SRC |
What created the event (IDM, query tool, save-event, etc.) |
QRYNAME, PROCESS_INSTANCE |
Populated when the event came from the query-event-creator |
Z_IDM_EVENTS is that table.The Z_IDM_EVENT_ASYNC OnNotify handler is where all real work happens. Its first action on waking up is the most important design choice in the entire pipeline: it ignores the event payload and re-fetches current IDM state.
method OnNotify
/+ &_MSG as Message +/
Local string &eventId = %This.GetEventIdFromMessage(&_MSG);
/* Dedup check -- see next section */
If %This.NewerEventExistsForSameSubject(&eventId) Then
%This.LogEvent(&eventId, "CANC", "Duplicate Cancelled");
Return;
End-If;
/* Pull authoritative state from IDM via Boomi, keyed by EMPLID */
Local string &emplid = %This.GetEmplidFromEventRow(&eventId);
Local string &idmJson = %This.FetchIdmUserProfile(&emplid);
/* Reconcile -- system of record is always IDM at the moment of work */
%This.ReconcileOPRID(&emplid, &idmJson);
%This.ReconcileRoles(&emplid, &idmJson);
%This.ReconcileUserDefaults(&emplid, &idmJson);
%This.ReconcileEmails(&emplid, &idmJson);
%This.ReconcileExternalIdGuid(&emplid, &idmJson);
%This.LogEvent(&eventId, "COMP", %This.BuildAuditLog());
end-method;
The outbound GET hits a Boomi proxy that forwards to IDM:
GET {baseURL}/ws/rest/IDM_SA/StudentProfile/{emplid}
Authorization: Basic ...
HTTP/1.1 200 OK
Content-Type: application/json
{
"userProfile": {
"userLogin": "4476900471",
"userISISID": "01183164",
"primaryPL": "SA9SSSTU1",
"processProfilePL": "SA9SSPRCSPRFL",
"navigatorHomePL": "",
"rowSecurityPL": "",
"campusEmailA": "jdoe@student.example.edu",
"campusEmailB": "",
"campusEmailC": ""
},
"entitlements": ["SA9_Self_Service_Student"]
}
The response maps cleanly into PeopleSoft security:
userLogin → PS_EXTERNAL_SYSTEM.EXTERNAL_SYSTEM_ID (the IDM GUID)primaryPL → PSOPRDEFN.OPRCLASSprocessProfilePL → PSOPRDEFN.PRCSPRFCLSnavigatorHomePL → PSOPRDEFN.DEFAULTNAVHPentitlements[] → PSROLEUSER rows (full reconciliation: add missing, remove extras)PS_EMAIL_ADDRESSESIf IDM reports "status": "disabled", the handler removes every PeopleSoft role and optionally every university email address – that’s the deprovisioning path the legacy system never had.
Two reasons. First, events are notifications, not data: the producer’s only promise is “something may have changed for this subject.” Second, bursts of events for the same subject collapse cleanly when the handler always reads current state – the first handler to win the race converges the record, and later handlers find nothing to do. This is what makes the next section possible.
Oracle IDM is a fast-moving system. A student gets admitted, enrolled, and assigned a role – three events, same EMPLID, arriving within seconds. Processing each one independently means three round-trips to IDM and three reconciliation passes, all producing the same final state.
The handler collapses these bursts with a single check at the top of OnNotify:
method NewerEventExistsForSameSubject
/+ &eventId as String +/
/+ Returns Boolean +/
Local SQL &sql = CreateSQL(
"SELECT 'X' FROM PS_Z_IDM_EVENTS newer, PS_Z_IDM_EVENTS current " |
"WHERE current.Z_IDM_EVENT_ID = :1 " |
" AND newer.EMPLID = current.EMPLID " |
" AND newer.SCC_ROW_ADD_DTTM > current.SCC_ROW_ADD_DTTM " |
" AND newer.Z_IDM_EVENT_STAT IN ('NEW', 'QUED')",
&eventId);
Local string &exists;
Return &sql.Fetch(&exists);
end-method;
If a newer pending event exists for the same EMPLID, the current event is marked CANC with reason “Duplicate Cancelled” and the handler returns immediately. The newer event will see the latest IDM state when it runs. Ten events for one student during an admissions burst collapse to one piece of real work.
One of the design’s more subtle wins is that the queue does not care who produced the event. The same Z_IDM_EVENTS table and Z_IDM_EVENT_ASYNC queue accept events from three different producers, all producing identical downstream behavior:
flowchart LR
A["IDM Webhook\n(Real-time lifecycle events)"]
B["Query Event Creator\n(App Engine Z_IDMQRYRUN)"]
C["PS Component Save-Events\n(via Event Mapping)"]
Q["Z_IDM_EVENTS\n(staging table)"]
H["Z_IDM_EVENT_ASYNC\n(OnNotify Handler)"]
A --> Q
B --> Q
C --> Q
Q --> H
H --> I
Z_IDMQRYRUN App Engine) runs any public PeopleSoft query that returns an EMPLID as its first column and generates events for the returned population. This solves three problems that pure event-driven systems always have: backfill after an outage, periodic audit for drift, and targeted remediation of a known bad subset.The third producer matters because identity is not a pure downstream projection of IDM. Faculty status is determined by PS_INSTR_ADVISOR, which IDM doesn’t know about. Applicant-vs-student transitions happen in PS_ACAD_PROG. The handler is authoritative for these decisions; IDM can’t be, because IDM doesn’t have the data.
Three pieces of operational UI were built on top of the staging table:
c/Z_IDM.Z_IDM_EVENTS.GBL) – search by EMPLID, status, date, source; drill into the full processing log; resubmit errored events; cancel queued events.dry run and debug flags. This is what production support uses.c/Z_IDM.Z_IDM_CONFIG.GBL) – feature flags for every write path (update PSOPRDEFN, update/delete emails, update GUID, post-back to IDM) plus a global dry-run switch. Every flag is honored by the handler. This is how risky changes roll out.A set of canned PeopleSoft queries drives the Query Event Creator on a schedule:
Z_IDM_ALERT_ERROR_LAST_DAY – finds ERR events from the last 24 hours and retries them.Z_IDM_STUD_NO_OPRID_NO_EVENT – finds active students in PS_ACAD_PROG with a custom-prefixed OPRID missing and no recent event. This catches the edge case where IDM had a user active before PeopleSoft knew about them, re-enrollment didn’t change IDM state, and no event would otherwise be emitted.The Query Event Creator is also the go-to tool for full-population audit: write a query that returns every active EMPLID, run it once a quarter, let drift correct itself.
Mobile clients can’t send SAML. The native mobile app authenticates via a REST service that accepts Basic auth (email + LDAP password) and returns a PS_TOKEN cookie. The LDAP bind lives in a dedicated PeopleCode module (Z_SIGNON_PCODE:LDAPAuthentication) and is gated by %Request.HTTPMethod being populated – which is true for incoming HTTP requests and false when the Integration Broker itself invokes sign-on PeopleCode. Without that gate, the LDAP path would fire for any IB-initiated authentication and widen the attack surface.
Missing SAML attributes arrive as absent, not blank. The IdP sends nothing – not an empty string – when an attribute can’t be resolved. Code that treats "" as the not-found sentinel will misbehave. Defensive reads:
&idmCampus = RTrim(%Request.GetHeader(&c.SAMLAttributeCampus));
If All(&idmCampus) Then
/* ... */
End-If;
Faculty logic is not IDM-authoritative. Faculty role eligibility is driven by PS_INSTR_ADVISOR inside PeopleSoft. The reconciler queries this during role reconciliation and adds the appropriate campus-specific application-level security (SCRTY_TBL_INST, SCRTY_TBL_SRVC, SAA_SCRTY_AARPT). If you assume IDM is the sole source of truth for all roles, this case will break.
Applicant-to-student transitions need explicit password clearing. An applicant who matriculates gets a new OPRID through the event pipeline, but their legacy applicant account still exists with a password. That’s a backdoor. The reconciler clears the applicant account’s password when the student account activates.
Dry run is not optional. Every write path honors Z_DRY_RUN. During the eight-week rollout, every change to the reconciliation logic was first proven in dry-run mode against a broad population – the logs show what would have changed without actually changing anything. If you skip this during rollout, you will find out what you missed in production, on real accounts.
After go-live:
Z_IDM_INTEGRATION) with configuration tables where roles used to be hard-coded constants.Testing changed as much as anything: changes to reconciliation logic are tested in isolation against the handler and the IDM GET, without involving the login path. What was once a high-risk deployment touching critical authentication code is now a low-risk deployment touching a scoped integration.
Chris Malek s a PeopleTools® Technical Consultant with over two decades of experience working on PeopleSoft enterprise software projects. He is available for consulting engagements.
Work with Chris