Effective dating

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:

FlagMeaning
isCurrentIn effect right now. At most one entry per timeline is current
isHistoricHas already taken effect and has since ended
isFutureScheduled, 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.

  • dateEffective is when the entry starts applying.
  • dateUnassign is the end date you asked for. It is inclusive, and null when you did not ask for one.
  • endDate is 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:

CodeCause
DUPLICATE_DATE_EFFECTIVETwo entries on one timeline share a dateEffective
DATE_UNASSIGN_BEFORE_DATE_EFFECTIVEThe end date falls before the start date
DATE_UNASSIGN_OVERLAPS_DATE_EFFECTIVEThe entry would run past the following entry's start
DATE_EFFECTIVE_OVERLAPS_DATE_UNASSIGNThe 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.