Hierarchies
TIP
View the Job Management API Reference
Job functions, job levels and custom OUs are each arranged in a hierarchy. All three behave the same way, so what follows uses job functions as the example and applies unchanged to /v1/job-levels and to custom OUs under /v1/custom-ou-types/{ouTypeId}/ous.
Two things make the model different from a plain parent pointer. Hierarchies are read one level at a time rather than as a tree, and a record's parent is a timeline, not a field you set.
Walking down the tree
Start at the roots, then follow children:
GET /services/api/x/chr-jobmgmt-app/v1/job-functions?rootsOnly=true
GET /services/api/x/chr-jobmgmt-app/v1/job-functions/{jobFunctionId}/children
The children endpoint returns the same representation, filters, ordering and field selection as the main collection, so you can walk a whole tree without a second call per node to fetch details. Both are token-paged, so a level with many children still needs pageToken handling.
An empty page means the record exists and has no children. An unknown identifier is a 404, so the two cases are distinguishable.
children reflects only the current parent
A child whose parent assignment has ended, or has not yet taken effect, is not listed. So children shows the tree as it is today, never as it was or will be. To see a record's history, read its parents timeline instead.
Archived records are never included anywhere.
The parentJobFunctionId on a record is the current parent, or null when the record is a root. It is read-only and derived from the timeline.
Reading where something has sat over time
GET /services/api/x/chr-jobmgmt-app/v1/job-functions/{jobFunctionId}/parents
This returns the full reparenting history under the standard {results, paging} envelope, with each entry flagged isCurrent, isHistoric or isFuture. At most one entry is current, and it is that entry's parent that the record reports as parentJobFunctionId.
Moving a record
Reparenting is adding a parent assignment, not editing a field:
POST /services/api/x/chr-jobmgmt-app/v1/job-functions/{jobFunctionId}/parents
Content-Type: application/json
{
"parentJobFunctionId": "a1b2c3d4-0000-0000-0000-000000000001",
"dateEffective": "2026-07-01",
"notes": "Reorg"
}
A future dateEffective schedules the move. Adding a successor does not replace its predecessor: the earlier entry stays and its endDate becomes the day before the new dateEffective, so only one parent is ever in effect.
To make a record a root again, end its current parent assignment by patching dateUnassign rather than deleting the entry, which would erase the fact that it ever had that parent.
Rules the API enforces
Beyond the shared date rules:
| Code | Cause |
|---|---|
CHILD_AND_PARENT_ARE_EQUAL | A record cannot be its own parent |
PARENT_TIMELINE_CIRCULAR_REFERENCE_DETECTED | The move would create a cycle |
CUSTOM_OU_PARENT_OU_TYPE_MISMATCH | Custom OUs only: the parent belongs to a different OU type |
PARENT_TIMELINE_RECORD_COUNT_EXCEEDS_MAX_LIMIT_GUARD_RAIL | More than 100 parent assignments on one record |
Cycle detection considers the resulting hierarchy, so a move that is individually valid can still be rejected because of where the subtree currently sits.
Custom OUs cannot cross OU types
A custom OU's parent must belong to the same OU type as the child. The right identifier under the wrong ouTypeId is a 404, and a parent from another type is a 400 CUSTOM_OU_PARENT_OU_TYPE_MISMATCH. Since custom OUs are addressed as /custom-ou-types/{ouTypeId}/ous/{customOuId}, the type is part of the record's address, not just a filter.
Building a hierarchy from scratch
Order matters, because a parent must exist before anything can point at it.
- Create the roots. A record created without a parent assignment is a root.
- Create the next level, then
POSTa parent assignment for each new record. - Repeat downwards.
If you are importing an existing structure, create every record first and then add the parent assignments in a second pass. That avoids having to sort your input topologically, and it means a cycle in the source data surfaces as a PARENT_TIMELINE_CIRCULAR_REFERENCE_DETECTED on one assignment rather than failing a record creation.
Archiving and dependants
A record with children cannot be archived. For custom OUs this is 400 CUSTOM_OU_HAS_CHILDREN, and a custom OU still assigned to jobs is refused with 400 CUSTOM_OU_ASSIGNED_TO_JOBS.
So unwinding part of a hierarchy is bottom-up: reparent or archive the children first, remove any job assignments that point at the record, then archive it. Archiving does not cascade.
Archiving is not reversible
DELETE archives a record. It is retained for reporting but stops appearing in reads, lists and children, and there is no endpoint to restore it.