Calling a Component Interface inside an Application Engine batch program or inbound integration requires careful transaction management. If you process 500 rows in a single loop without isolating commits and capturing row-level errors, a single bad row—or a single SaveEdit error—can abort the entire batch job or leave your database in an inconsistent state.
In this walkthrough, we build a complete batch import solution around the delivered USER_PROFILE Component Interface (USERMAINT component) that:
NEW hires and TER terminations) into a staging table.AccountLocked = "1").EmailAddresses, IDTypes, and Roles collections, and generates a random password via ResetPassword().Manager).CHG_UP001_IMP)Whenever you process batch records through a Component Interface, stage the data in a database table first rather than calling the CI directly from a flat-file read loop.
Key design elements of the staging table (CHG_UP001_IMP):
PROCESS_INSTANCE (Key 1) and OPRID (Key 2) allow multiple batches to reside in the staging table simultaneously and allow an administrator to search by batch or user ID.CHG_IMPORT_STATUS): Uses five translate values to track each row’s lifecycle:
RFL — Ready for Load (initial state after file import)DNL — Do Not Load (manually skipped by an administrator)SUC — Success (CI Save() succeeded)WAR — Warning On Load (CI Save() succeeded, but non-fatal warnings were logged in PSMessages)ERR — Error On Load (CI failed; row requires investigation and re-processing)ERROR_MESSAGE_TXT): A Long (LONG VARCHAR) column that stores the full text of any caught PeopleCode exception plus all messages from %Session.PSMessages.Exposing this staging table on a simple PIA inquiry component gives functional administrators a self-service workbench where they can filter by Import Status = Error On Load, read the exact CI validation message, correct a bad email or role value on the staging row, flip the status back to Ready for Load, and re-run the load without re-importing the file.
Why does an Application Engine program that calls a Component Interface need a specific section structure? Because when a Component Interface fails midway through populating buffers or executing Save(), any dirty changes in the CI or database session must not pollute subsequent rows, and each successful row must be committed independently.
In the CI section, configure Step01 with Commit After: After Step:
This guarantees that every iteration of the USER_PRC loop processes one user, updates that user’s staging row to SUC, WAR, or ERR, and immediately issues a database COMMIT before moving to the next user.
FILE_IMP)The FILE_IMP section reads the inbound CSV file (OPRID,ACTION,USER_TYPE,EMAIL), splits each line on commas, and inserts rows with CHG_IMPORT_STATUS = "RFL" into PS_CHG_UP001_IMP:
Local File &fImport;
Local string &sLine;
Local array of string &aLine;
Local Record &rImport;
&fImport = GetFile(CHG_UP001_AET.ATTACHUSERFILE, "R", %FilePath_Absolute);
&rImport = CreateRecord(Record.CHG_UP001_IMP);
While &fImport.ReadLine(&sLine);
&aLine = Split(&sLine, ",");
&rImport.SetDefault();
&rImport.PROCESS_INSTANCE.Value = CHG_UP001_AET.PROCESS_INSTANCE;
&rImport.OPRID.Value = &aLine [1];
&rImport.CHG_IMPORT_STATUS.Value = "RFL"; /* Ready for Load */
&rImport.CHG_LOAD_ACTION.Value = &aLine [2];
If &aLine.Len >= 3 Then
&rImport.CHG_USER_TYPE.Value = &aLine [3];
End-If;
If &aLine.Len >= 4 Then
&rImport.EMAIL_ADDR.Value = &aLine [4];
End-If;
&rImport.LASTUPDDTTM.Value = %Datetime;
&rImport.CREATEDTTM.Value = %Datetime;
&rImport.Insert();
End-While;
CI Section)Inside the CI section, we fetch the current staging row using a standalone Record object (SelectByKey), instantiate CompIntfc.USER_PROFILE, and branch on CHG_LOAD_ACTION.
InsertItem(1) in CollectionsWhen Create() initializes a new USER_PROFILE instance, its EmailAddresses, IDTypes, and Roles collections each contain one pre-allocated dummy row at Item(1):
Employee), we grab .Item(1) directly and populate its properties.CHG_USER_TYPE = "MGR") and needs a second role (Manager), we must call &oRolesCollection.InsertItem(1) to insert a new row into the collection.InsertItem(1) Works: Calling &oRolesCollection.InsertItem(1) inserts a new Role item relative to index 1 and returns a reference to the newly inserted ApiObject item. If you work with CurrentItem instead of explicit item references, InsertItem also shifts CurrentItem to the newly inserted row.What happens if JTOMSON was already created as a regular employee (EMP) in an earlier batch, and HR later sends a NEW row for JTOMSON with MGR because she was promoted to Manager?
&oUserProfile.Create() for an existing OPRID returns False and logs Row already exists with the specified keys. {USER_PROFILE} (91,49) to %Session.PSMessages..Create() returns False, we clear %Session.PSMessages, populate the GetKeys, and fall back to .Get()!.Get(), their Roles collection is not empty—it already contains their existing roles (Employee, etc.). Therefore, instead of blindly overwriting Item(1) or blindly calling InsertItem(1), we loop through 1 To &oRolesCollection.Count to check whether Employee or Manager is already present, and only call InsertItem(1) for missing roles.Here is the complete, production-ready PeopleCode for the CI section:
Local ApiObject &oSession, &oUserProfile;
Local ApiObject &oEmailAddressesCollection, &oEmailAddresses;
Local ApiObject &oIdTypesCollection, &oIdTypes, &oAttributesCollection, &oAttributes;
Local ApiObject &oRolesCollection, &oRoles;
Local Record &recImport;
Local string &error_message;
Local boolean &bNewPassword, &bFoundEmpRole, &bFoundMgrRole;
Local integer &i;
Function errorHandler() Returns string
Local ApiObject &oPSMessageCollection, &oPSMessage;
Local number &j;
Local string &sErrMsgSetNum, &sErrMsgNum, &sErrMsgText, &sReturn;
&oPSMessageCollection = &oSession.PSMessages;
For &j = 1 To &oPSMessageCollection.Count
&oPSMessage = &oPSMessageCollection.Item(&j);
&sErrMsgSetNum = &oPSMessage.MessageSetNumber;
&sErrMsgNum = &oPSMessage.MessageNumber;
&sErrMsgText = &oPSMessage.Text;
&sReturn = &sReturn | "<<<<< (" | &j | ") " | &sErrMsgText | " [" | &sErrMsgSetNum | "," | &sErrMsgNum | "] >>>>>" | Char(13);
End-For;
&oPSMessageCollection.DeleteAll();
Return &sReturn;
End-Function;
/* Load current staging row */
&recImport = CreateRecord(Record.CHG_UP001_IMP);
&recImport.PROCESS_INSTANCE.Value = CHG_UP001_AET.PROCESS_INSTANCE;
&recImport.OPRID.Value = CHG_UP001_AET.OPRID;
If Not &recImport.SelectByKey() Then
throw CreateException(0, 0, "Logic error: could not select staging row.");
End-If;
try
&oSession = %Session;
&oSession.PSMessagesMode = 1;
&oUserProfile = &oSession.GetCompIntfc(CompIntfc.USER_PROFILE);
If &oUserProfile = Null Then
throw CreateException(0, 0, "Could not instantiate USER_PROFILE CI.");
End-If;
&oUserProfile.InteractiveMode = True;
&oUserProfile.GetHistoryItems = True;
&oUserProfile.EditHistoryItems = False;
/* Set Key property (used by both Get and Create) */
&oUserProfile.UserID = &recImport.OPRID.Value;
Evaluate &recImport.CHG_LOAD_ACTION.Value
When = "TER" /* Terminate / Lock Out User */
If Not &oUserProfile.Get() Then
throw CreateException(0, 0, "Could not get User Profile on Search Record.");
End-If;
&oUserProfile.AccountLocked = "1";
Break;
When = "NEW" /* Create or Update User */
If &oUserProfile.Create() Then
/* --- BRAND NEW USER --- */
&bNewPassword = True;
&oUserProfile.PrimaryPermissionList = "ALLPAGES";
&oUserProfile.ProcessProfilePermissionList = "ALLPAGES";
&oUserProfile.RowSecurityPermissionList = "ALLPAGES";
/* Populate Dummy Row Item(1) on EmailAddresses */
&oEmailAddressesCollection = &oUserProfile.EmailAddresses;
&oEmailAddresses = &oEmailAddressesCollection.Item(1);
&oEmailAddresses.EmailType = "BUS";
&oEmailAddresses.EmailAddress = &recImport.EMAIL_ADDR.Value;
&oEmailAddresses.PrimaryEmail = "Y";
/* Populate Dummy Row Item(1) on IDTypes */
&oIdTypesCollection = &oUserProfile.IDTypes;
&oIdTypes = &oIdTypesCollection.Item(1);
&oIdTypes.IDType = "NON";
/* Populate Dummy Row Item(1) on Roles */
&oRolesCollection = &oUserProfile.Roles;
&oRoles = &oRolesCollection.Item(1);
&oRoles.RoleName = "Employee";
If &recImport.CHG_USER_TYPE.Value = "MGR" Then
&oRoles = &oRolesCollection.InsertItem(1);
&oRoles.RoleName = "Manager";
End-If;
Else
/* --- CREATE FAILED: FALL BACK TO GET() FOR EXISTING USER --- */
&oSession.PSMessages.DeleteAll();
&oUserProfile.UserID = &recImport.OPRID.Value;
If Not &oUserProfile.Get() Then
throw CreateException(0, 0, "Could not Create or Get User Profile.");
End-If;
/* Inspect existing roles before inserting */
&oRolesCollection = &oUserProfile.Roles;
&bFoundEmpRole = False;
&bFoundMgrRole = False;
For &i = 1 To &oRolesCollection.Count
&oRoles = &oRolesCollection.Item(&i);
If &oRoles.RoleName = "Employee" Then
&bFoundEmpRole = True;
End-If;
If &oRoles.RoleName = "Manager" Then
&bFoundMgrRole = True;
End-If;
End-For;
If Not &bFoundEmpRole Then
&oRoles = &oRolesCollection.InsertItem(1);
&oRoles.RoleName = "Employee";
End-If;
If (&recImport.CHG_USER_TYPE.Value = "MGR") And (Not &bFoundMgrRole) Then
&oRoles = &oRolesCollection.InsertItem(1);
&oRoles.RoleName = "Manager";
End-If;
End-If;
Break;
End-Evaluate;
/* Save the Component Interface */
If Not &oUserProfile.Save() Then
throw CreateException(0, 0, "Could not save User Profile.");
End-If;
/* Execute Custom Method ResetPassword() only for newly created users */
If &bNewPassword Then
If Not &oUserProfile.ResetPassword() Then
throw CreateException(0, 0, "Could not reset password.");
End-If;
&recImport.ERROR_MESSAGE_TXT.Value = "New Password=" | &oUserProfile.GeneratedPassword;
End-If;
/* Check if any non-fatal warnings were logged to PSMessages */
If &oSession.PSMessages.Count > 0 Then
&recImport.CHG_IMPORT_STATUS.Value = "WAR";
&recImport.ERROR_MESSAGE_TXT.Value = &recImport.ERROR_MESSAGE_TXT.Value | Char(13) | errorHandler();
Else
&recImport.CHG_IMPORT_STATUS.Value = "SUC";
End-If;
&recImport.LASTUPDDTTM.Value = %Datetime;
&recImport.Update();
catch Exception &caught
/* Capture both the Exception and PSMessages onto the staging row */
&error_message = "Error Caught: [[ " | &caught.ToString() | " ]]" | Char(13) | errorHandler();
&recImport.ERROR_MESSAGE_TXT.Value = &error_message;
&recImport.CHG_IMPORT_STATUS.Value = "ERR";
&recImport.LASTUPDDTTM.Value = %Datetime;
&recImport.Update();
/* Cancel CI buffer and clean up reference */
If &oUserProfile <> Null Then
&oUserProfile.Cancel();
End-If;
end-try;
Because our catch block writes both &caught.ToString() and the formatted errorHandler() output (%Session.PSMessages) into CHG_UP001_IMP.ERROR_MESSAGE_TXT while allowing the Application Engine step to complete and commit cleanly:
Success, meaning the batch job processed every valid user in the file without aborting on the first bad row.Import Status = Error On Load, and immediately see both where the PeopleCode failed and why the Component Processor rejected the row:
Success, always add a final notification step (or PSQuery / Workcenter alert) that checks SELECT COUNT(*) FROM PS_CHG_UP001_IMP WHERE PROCESS_INSTANCE = :1 AND CHG_IMPORT_STATUS IN ('ERR', 'WAR') and emails or alerts the administrator if any rows require attention.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.
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.