Effective dating
TIP
View the Job Management API Reference
Job Management does not overwrite. When a job changes status, when a job function moves under a new parent, or when a user takes on a job, the change is recorded as a dated entry on a timeline. The previous entry stays. This is what lets you backdate a correction, schedule a change for next quarter, and still answer "what did this look like in March" afterwards.
Every sub-resource ending in /statuses, /parents, and every assignment collection under /jobs/{jobId} and /users/{userId} is a timeline.
Reading a timeline
GET /services/api/x/chr-jobmgmt-app/v1/jobs/{jobId}/statuses
{
"results": [
{
"assignmentId": "7c1f2d4e-9a3b-4c5d-8e6f-0a1b2c3d4e5f",
"active": true,
"dateEffective": "2026-01-01",
"dateUnassign": null,
"endDate": null,
"isCurrent": true,
"isHistoric": false,
"isFuture": false,
"notes": "Approved by HR."
}
],
"paging": { "tokens": { "next": null } }
}
Every entry carries the same three flags, and you should read them rather than comparing dates yourself:
| Flag | Meaning |
|---|---|
isCurrent | In effect right now. At most one entry per timeline is current |
isHistoric | Has already taken effect and has since ended |
isFuture | Scheduled, its dateEffective is still ahead. Does not count yet |
assignmentId identifies the timeline entry itself, not the thing it points at. It is the value you use on the PATCH and DELETE routes.
Three dates, not two
This is the part most integrations get wrong.
dateEffectiveis when the entry starts applying.dateUnassignis the end date you asked for. It is inclusive, andnullwhen you did not ask for one.endDateis when the entry actually stops applying.
endDate differs from dateUnassign when a later entry supersedes this one. It is read-only and derived.
Adding an entry supersedes, it does not replace
When you add an entry that takes effect while another is in effect, the API does not reject it and does not delete the old one. The earlier entry stays on the timeline and its endDate is set to the day before your new dateEffective, so exactly one entry is ever in effect.
So to deactivate a job from 1 July, you do not edit the existing status entry. You add a new one:
POST /services/api/x/chr-jobmgmt-app/v1/jobs/{jobId}/statuses
Content-Type: application/json
{ "active": false, "dateEffective": "2026-07-01", "notes": "Role retired" }
The previously current entry now reports endDate: "2026-06-30" and isHistoric: true, and the new one is current from 1 July. Nothing was lost.
Scheduling is just a future date
A dateEffective in the future schedules the change. The entry comes back with isFuture: true and does not affect anything until that date arrives. There is no separate "schedule" call.
Correcting versus changing
Adding an entry records a change in the real world. Patching an entry corrects a mistake in the record.
PATCH .../statuses/{assignmentId}
Content-Type: application/merge-patch+json
{ "notes": "Corrected reference" }
Send only the members you are changing. An absent member keeps its value, and a nullable member sent explicitly as null is cleared. There is no lost-update guard, so concurrent writes to the same entry are last-write-wins.
An entry cannot cross the present
An entry that has already taken effect cannot be moved into the future (400 HISTORY_TO_FUTURE_FORBIDDEN), and one that has not yet taken effect cannot be moved into the past (400 FUTURE_TO_HISTORY_FORBIDDEN). An end date that has already passed can be neither pushed forward nor cleared.
If you need to undo something that has already taken effect, add a corrective entry rather than trying to rewrite history.
Date rules the API enforces
These apply across every timeline, so it is worth handling them once:
| Code | Cause |
|---|---|
DUPLICATE_DATE_EFFECTIVE | Two entries on one timeline share a dateEffective |
DATE_UNASSIGN_BEFORE_DATE_EFFECTIVE | The end date falls before the start date |
DATE_UNASSIGN_OVERLAPS_DATE_EFFECTIVE | The entry would run past the following entry's start |
DATE_EFFECTIVE_OVERLAPS_DATE_UNASSIGN | The entry would start before the preceding entry's end |
All dates are calendar dates, YYYY-MM-DD, interpreted in the portal's time zone. There is no time component, so an entry effective on a date applies for that whole day.
Deleting an entry
DELETE on a timeline entry removes it from the timeline entirely, which is different from ending it. Ending an assignment is a PATCH of dateUnassign; the entry stays and reports when it applied. Delete only when the entry should never have existed.
Guard rails
Timelines are bounded. A job function or custom OU holds at most 100 parent assignments (PARENT_TIMELINE_RECORD_COUNT_EXCEEDS_MAX_LIMIT_GUARD_RAIL), and a user holds at most 1000 job assignments across all jobs (USER_JOB_TIMELINE_RECORD_COUNT_EXCEEDS_MAX_LIMIT_GUARD_RAIL). If you are approaching either, you are probably writing one entry per sync rather than one entry per real change.
Detecting concurrent change
Job, job function, job level and custom OU records carry a version counter that increments on every persisted change, including changes to their status and parent timelines. Compare it between reads to detect that a record moved under you.