Full transfer
Salesforce EDA models people and relationships, not registrar records. This pathway covers the canonical contact/affiliation mapping and the webhook bridge that keeps EDA current without a nightly batch that is always one day stale.
Salesforce EDA is built around Contacts, Accounts, Affiliations, and Relationships — a constituent model, not a registrar's model. A legacy student ERP thinks in persons, terms, registrations, and holds. Bridging them is a translation of worldview, not just of fields, and the most common failure is a nightly batch that leaves EDA permanently a day behind the system of record.
| Legacy ERP concept | Salesforce EDA | Note |
|---|---|---|
| Person record | Contact | De-duplicate against existing constituents first |
| Program / department | Account (Educational Institution) | Model the hierarchy explicitly |
| Enrolment in a program | Program Enrollment + Affiliation | Two EDA objects, not one |
| Advisor link | Relationship | Directional — set the reciprocal role |
A one-time load is straightforward. Keeping EDA live as the ERP changes — near-real-time, idempotent, and safe to replay — is the middleware that determines whether the CRM is trusted or quietly abandoned. That bridge is a self-contained deliverable we can stand up in weeks.
Scope the bridge →Wrap the legacy ERP in a thin change-capture layer that publishes person and enrolment changes as events. The bridge transforms each event into the canonical contact/affiliation shape and upserts idempotently into EDA, so a replayed event is harmless and EDA is never more than seconds behind the source of truth.
A working piece from this integration — no sign-up. The full build handles the edge cases, safeguards, and cutover.
// EDA Canonical Mapping: Legacy Student Record → EDA Objects // Legacy ERP fields map to Contact, Affiliation, ProgramPlan, CourseConnection legacyStudent = { student_id: "STU-2024-00142", first_name: "Maya", last_name: "Rodriguez", email: "[email protected]", program: "BS Computer Science", enrollment_status: "active", // → Affiliation.Status cohort_year: 2024, gpa: 3.71 } // Webhook payload sent to Salesforce EDA on record change POST /services/apexrest/eda/webhook/studentSync { "event": "student.updated", "timestamp": "2024-03-15T09:22:11Z", "source": "legacy-erp", "payload": { // Contact (primary person record) "Contact": { "Email": "[email protected]", "FirstName": "Maya", "LastName": "Rodriguez", "University_ID__c": "STU-2024-00142" }, // Affiliation: links Contact → Account (Academic Program) "Affiliation": { "Account__c": "acct_id_for_CS_program", // lookup to Account "Role__c": "Student", "Status__c": "Current", // map from enrollment_status "StartDate__c": "2024-08-20" }, // ProgramPlan: captures declared program with cohort "ProgramPlan": { "Account__c": "acct_id_for_CS_program", "EnrollmentStatus__c": "Full-Time", "ClassYear__c": 2024 } } } // Apex trigger handler receiving the webhook @RestResource(urlMapping='/eda/webhook/studentSync') global class EDA_StudentWebhook { @HttpPost global static void handleStudentEvent() { Map<String, Object> req = (Map<String, Object>) JSON.deserializeUntyped(RestContext.request.requestBody.toString()); Map<String, Object> pl = (Map<String, Object>) req.get('payload'); Map<String, Object> c = (Map<String, Object>) pl.get('Contact'); Map<String, Object> aff= (Map<String, Object>) pl.get('Affiliation'); // Upsert Contact by external ID Contact con = [SELECT Id FROM Contact WHERE University_ID__c = :c.get('University_ID__c')]; con.Email = (String) c.get('Email'); con.FirstName = (String) c.get('FirstName'); con.LastName = (String) c.get('LastName'); update con; // Upsert Affiliation (replaces stale nightly batch) Affiliation__c affil = new Affiliation__c( Contact__c = con.Id, Account__c = (String) aff.get('Account__c'), Role__c = (String) aff.get('Role__c'), Status__c = (String) aff.get('Status__c'), StartDate__c = Date.valueOf((String) aff.get('StartDate__c')) ); upsert affil Contact__c Account__c; // composite key } }
How we'd take this from discovery to a production-safe cutover — the phases, the canonical mapping, and the edge cases that bite.
Salesforce EDA models people and their academic relationships as a constellation of Contacts, Accounts, and Affiliations—each affiliation record capturing a role (Student, Applicant, Faculty) against an Account that represents a program, department, or institution. The legacy Student ERP, by contrast, stores enrollment as rows in a relational schema with student IDs, program codes, and term keys. This integration replaces a stale nightly batch with a webhook-driven bridge that maps registrar records to the EDA affiliation graph in near-real time.
STUDENT_ID, PROGRAM_CD, ENROLL_STATUS, TERM_ID, ADVISOR_ID, and MATRICULATION_DT field semantics. Identify which columns are nullable, which codes are active vs. historical, and whether the ERP uses surrogate keys or natural identifiers. This profile informs the mapping layer and surfaces hidden gaps (e.g., missing birth dates, ambiguous name prefixes).PROGRAM_CD. Each student's active enrollment becomes an Affiliation record linking their Contact to the appropriate Program Account with Role__c = 'Student' and Status__c set from ENROLL_STATUS. Advisor assignments become a second Affiliation on the same Program Account with Role__c = 'Faculty Advisor', referencing the advisor's Contact record.STUDENT_ID and a change-type indicator (CREATED, UPDATED, WITHDRAWN).EmployeeNumber for Contact lookup, match on Contact+Account+Role for Affiliation lookup. Use LastModifiedDate from the webhook payload to prevent stale writes. Transform legacy codes (e.g., ENROLL_STATUS = 'A') to EDA picklist values (Active).ERROR_REASON__c. Implement deduplication using a custom Legacy_Event_Key__c field (composite of STUDENT_ID + TERM_ID + CHANGE_TS) so re-delivered webhooks do not create duplicate Affiliations.EmployeeNumber), then upsert Affiliations (matching on Contact ID + Account ID + Role). Set IsActive__c on current-term affiliations only.StartDate__c, and duplicate Contacts.| Legacy ERP Field | EDA Object | EDA Field | Transformation Notes |
|---|---|---|---|
STUDENT_ID |
Contact | EmployeeNumber |
Used as primary lookup key for upserts. Ensure leading zeros preserved. |
FIRST_NAME, LAST_NAME |
Contact | FirstName, LastName |
Direct map; handle null FIRST_NAME by setting FirstName = 'Unknown'. |
BIRTH_DT |
Contact | Birthdate |
Convert from YYYYMMDD string to Date. Reject future dates. |
PROGRAM_CD |
Account + Affiliation | Account.Id (lookup) + Affiliation__c |
Look up Program Account by Program_Code__c = PROGRAM_CD. Create Account if missing. |
ENROLL_STATUS |
Affiliation | Status__c |
Map codes: A → Active, W → Withdrawn, G → Graduated, L → Leave of Absence. |
ADVISOR_ID |
Affiliation (secondary) | Contact__c (advisor) + Role__c = 'Faculty Advisor' |
Look up advisor Contact by EmployeeNumber = ADVISOR_ID. Create Affiliation only if advisor Contact exists. |
TERM_ID |
Term + Affiliation | Term__c + StartDate__c |
Parse TERM_ID (e.g., 202401) to derive Term__c lookup and StartDate__c of the first day of term. |
STUDENT_ID to a returning student (re-admit), the bootstrap load will create a second Contact. Mitigation: match on Email or Birthdate + LastName in addition to EmployeeNumber during initial load.ADVISOR_ID for a Contact that does not exist in EDA must be held in a pending-lookup state or logged to a custom Pending_Advisor_Relationship__c object for manual resolution.PROGRAM_CD = 'UNDECLARED' before the corresponding Program Account is created. Route to dead-letter queue; create the Account manually or via a separate provisioning webhook, then reprocess the held event.UPDATED webhook arrives out-of-order (e.g., Withdrawn sent before Active). Use CHANGE_TS as the system-of-record timestamp; discard events older than the current LastModifiedDate on the existing Affiliation.BIRTH_DT. EDA requires a Contact birthdate for some reporting features. If null, set Birthdate = 1900-01-01 as a sentinel and flag Birthdate_Verified__c = false.COUNT(*) of active enrollment rows in the legacy ERP, grouped by PROGRAM_CD and ENROLL_STATUS. Any discrepancy exceeding a 0.1% threshold triggers an alert for investigation before the batch job is decommissioned after the 30-day hold period. During cutover, schedule a 4-hour freeze window where no new ERP enrollments are processed via webhooks; backfill that window immediately after go-live using the bootstrap export. Reading the reference is free. Delivering it under liability — with the safeguards that keep production running through the cutover — is what we do.