Skip to content
syntaqx

Workforce, chapter 35 of 63

The HRIS as Record of Truth

The employee record and its history: effective dating, future-dated changes, corrections, positions, org units, and joiner, mover and leaver events to payroll.

15 min readUpdated

Every payroll, benefits enrollment, identity account and headcount report starts from the same question: who works here, in what job, at what pay, reporting to whom, as of which date. The human resources information system (HRIS) is where an employer answers it. Payroll reads the answer every period, the benefits platform reads it to decide eligibility, the identity provider reads it to create and disable accounts, and finance reads it to plan headcount. When the HRIS and payroll are one product, the answer is shared; when they are separate, every change has to cross an integration before a cutoff, and the two systems have to agree afterward on what was true.

The hard part is time. A raise approved on November 20 can be effective November 9, inside a period already paid. A transfer can be entered in October to take effect January 1. A termination can be keyed with the wrong date and corrected a week later, after payroll, benefits and identity have all acted on it. Each of those is a different operation: a change that takes effect later, a change that took effect earlier, and a correction of what the record said. An HRIS that stores only "the current value" can't tell them apart, and every downstream system inherits the confusion. This chapter covers the employee record and its history, effective dating and future-dated changes, positions and org structures, joiner, mover and leaver events to downstream systems, and the HRIS-to-payroll integration. The generic pattern is the playbook's Temporal Data; this chapter is what HR adds to it.

How the Platforms Do It#

What HRIS and HCM vendors publish about history, effective dates and change events, as of November 2026:

Platform Record model History and effective dating Change events
BambooHR Employees with fields, plus tabular datasets per employee "(e.g., jobInfo, compensation, customTabularField)" Job and compensation history live as rows in those tables Webhooks that "monitor a predefined set of fields," signed with an X-BambooHR-Signature HMAC SHA-256 header, retried "up to 5 times," and possibly batching several employees in one POST
HiBob People with work, employment, lifecycle, salary and variable pay history tables The work table's entries are keyed by "the site and effective date"; variable pay "is a multi-current history table" Lifecycle webhooks such as employee.joined are "triggered automatically at midnight on the effective date" in the site's time zone, with a payload that carries only the employee id
Oracle HCM Cloud Date-effective objects: "one or more physical or change records, each of which has an effective start date and may have an effective end date" The REST API takes an Effective-Of header with a RangeMode of UPDATE (which splits rows in the range) or CORRECTION (which rewrites a range), and supports several changes on one day as sequenced rows Retrieval of "past or future" worker details by effective date
Paylocity A developer portal for "3rd party integrators" with WebLink, Webhooks and Partner Onboarding APIs Not described on the public portal Webhooks API listed
Personio v2 persons with employments nested under them, "with the most recent employments appearing first," and compensations Not described publicly Webhooks management API, with a 7-day grace window when a signing secret is rotated
Rippling A worker "represents the period of work for a user," and "one user has multiple worker records if they have been re-hired" Each change carries effective_on ("past, present, or future") and applied_on ("When Rippling saved or approved the record") Partner webhooks employee.hired and employee.terminated; the change feed warns that filtering by effective_on "will not include changes scheduled at a future date"
UKG Pro HCM, Pro WFM and Ready on one developer portal Not described on the portal's landing pages Webhooks: "Modern, event-driven notifications for changes related to UKG data"
Workday Web services including Human_Resources, Staffing, Compensation and Payroll, at "v47.1 (2026R2)" Operation-level documentation isn't publicly reachable The Staffing service exposes "Human Capital Management Business Services and data"

Three things stand out. Rippling's split of effective_on from applied_on and Oracle's split of UPDATE from CORRECTION are the same idea in two vocabularies: a record has the time a fact is true and the time the system learned it, and the two must be kept apart (the playbook's Two Kinds of Time). HiBob and Rippling show the two ways to announce a future change: fire the event when the change becomes effective, or fire it when it is recorded and let the consumer schedule it; a consumer that only polls by effective date misses future-dated changes entirely, which is Rippling's own warning. And Rippling's worker-per-stint model is the core model's employments: a rehire is a new row on the same person, not a reactivated one. Workday does not publish its operation-level documentation openly, so its model is described here only at the level of its service list.

The Employee Record#

The record an HRIS keeps is wider than what payroll needs and narrower than what HR would like. The core model splits it into the person, the worker, the employment and the job assignment (The Core Data Model), and the split matters here more than anywhere:

  • Person. Legal name, the tokenized SSN or ITIN, date of birth and home address history (persons, person_addresses). A person exists once across every employer in a company and across rehires. Preferred names, pronouns and emergency contacts are HR fields on the person; the legal name stays as on the Social Security card because the W-2 and the I-9 need it.
  • Worker. The person at one employer (workers), with the worker number, the type (employee or contractor) and a status. A transfer between two EINs in the same company is two workers, because each EIN files its own returns.
  • Employment. One stint from hire to termination (employments), with the original hire date that service-based benefits and seniority count from. A rehire is a new employment.
  • Job assignment. What the employment is, over time (job_assignments): title, department, manager, work location, FLSA status, pay basis and standard hours, each row effective-dated. Pay rates hang off the assignment (pay_rates).

What is not on the record matters too. Medical information, I-9 documents, background check reports and EEO self-identification have their own homes with their own access rules (Employee Records, Privacy & Retention covers why), and the HRIS should link to them, not copy them into the employee's main profile.

Effective Dating and Future-Dated Changes#

Every field a calculation or an eligibility rule reads is effective-dated: one closed-open valid_during DATERANGE column per row, never overwritten (Effective-Dated Rows). Four operations follow, and an HRIS has to support each one explicitly because they mean different things downstream:

Operation What happened What the rows do What downstream must do
Change, effective now A raise approved today, effective today Close the current row at today; open a new row from today Payroll uses the new rate from today
Change, effective in the future A transfer approved in October, effective January 1 Close the current row at January 1; open the new row from January 1; nothing visible changes until then Payroll, benefits and identity learn now and act on the date
Change, effective in the past A raise approved November 20, effective November 9 Close the row at November 9; open the new row from November 9 Payroll computes retro pay for paid periods after November 9
Correction The record was wrong: the raise was keyed as $25.00 instead of $25.50 Replace the value for its range, keeping the wrong row in history with when it was recorded and replaced Downstream recomputes anything that read the wrong value, and the audit shows both

The difference between the third and fourth rows is the one most systems lose. A backdated change says the business decided something late; a correction says the record was wrong. Both reach back in time, but a correction may also mean a W-2 was wrong, a benefits eligibility decision was made on bad data, or an access grant should never have happened, and an auditor will want to see what the record said on the day payroll ran. That is why the core model's audit_events carries before and after values on every effective-dated table, and why the playbook keeps recorded time separately from effective time. Oracle's API makes the two explicit (RangeMode=UPDATE splits a range, RangeMode=CORRECTION rewrites it); Rippling exposes both clocks on every change (effective_on, applied_on).

Future-dated changes are scheduled, not pending. The row already exists with its range, and an as-of query on any date returns the right answer without a job having to "apply" it (Scheduling Future Changes). What does need a schedule is the announcement: a downstream system that acts on effective events needs to hear on the day, and one that plans needs to hear on the day the change was recorded. My default is to emit both: a worker_change.recorded event when the change is saved and a worker_change.effective event when its date arrives, each carrying the effective date, so a consumer can pick the one it needs and neither has to poll.

Worked Numbers: A Raise Inside a Paid Period

The numbers are invented. An hourly employee earns $24.00 an hour on a biweekly schedule; the period runs Sunday, November 1 through Saturday, November 14, 2026, with a check date of Friday, November 20. The employee works 40 hours in each week. On Monday, November 23, after the run is paid, the manager enters a raise to $25.50 effective Monday, November 9.

Week Hours Rate as paid Paid Rate as now known Owed Difference
Nov 1 to 7 40 $24.00 $960.00 $24.00 $960.00 $0.00
Nov 8 to 14 40 $24.00 $960.00 $25.50 $1,020.00 $60.00

The HRIS change closes the $24.00 pay_rates row at November 9 and opens a $25.50 row from November 9. Payroll re-reads the paid period as of the new data, finds $60.00 more owed, and pays it on the next run as a retro earning with the work period it belongs to; overtime weeks would also need their regular rate recomputed. Retro Pay, Voids & Corrections covers the calculation. Had the same change been entered on November 6, before the run, it would have been a future-dated change, and the run would have paid $1,980.00 the first time.

A salaried change inside a period is a proration policy. An invented employee moves from $78,000 to $83,200 a year effective Monday, November 9, in the same period. At $3,000.00 and $3,200.00 per biweekly period, a calendar-day proration over the 14 days gives 8 days at the old salary and 6 at the new: $3,000.00 x 8 / 14 + $3,200.00 x 6 / 14 = $1,714.29 + $1,371.43 = $3,085.72, rounding each part to the cent (my arithmetic). A working-day proration over the period's 10 weekdays gives 5 old and 5 new: $1,500.00 + $1,600.00 = $3,100.00. Both are defensible; which one the employer uses is a rule, effective-dated like any other, and the statement should say which one produced the amount.

Positions and Org Structures#

HR systems model the organization in two ways, and many support both.

Job management. The worker is the anchor. A job assignment has a title, department and manager, and the organization is whatever the assignments add up to. Hiring needs only a requisition, and a reorganization is a batch of assignment changes. It suits small and fast-changing employers.

Position management. The position is the anchor: a seat in the organization that exists whether or not anyone fills it, with its own job code, department, cost center, location, FTE, pay grade and budget. A worker fills a position; a vacancy is a position with no incumbent; a requisition is a request to fill one. Finance can plan headcount by position before anyone is hired, and an EEO-1 job category, a pay range for a posting and an FLSA classification can be set on the job the position belongs to rather than on each person. Public employers and large enterprises use it because their budgets are by position.

Either way, the structures are effective-dated too. An org unit (a department, cost center, division or legal entity grouping) has a parent and a range; a reorganization on January 1 closes the old parent link and opens a new one, and a headcount report as of December 31 must still show the old tree. Manager relationships are the most-read part of the structure (approvals, access reviews, org charts) and the most fragile: when a manager leaves, every report's assignment needs a new manager on the same effective date, or approvals route to a terminated person.

Two rules keep the structures usable downstream. Codes, not names. Payroll and the general ledger key on cost center and department codes; a renamed department keeps its code, and a split creates new codes with ranges. Job architecture drives compliance. Pay ranges, EEO categories, FLSA status defaults and pay equity comparison groups hang off the job (family, level, code), which is why Pay Transparency, Pay Equity & EEO-1 depends on a clean job catalog more than on anything in the employee's own record.

Joiner, Mover and Leaver Events#

Workplace & Collaboration covers what identity systems do with HR events (Joiner, Mover, Leaver): creating an account before the start date, swapping groups on a transfer, and revoking everything at a termination. From the HRIS side, the job is to emit events that are complete, ordered per worker and honest about time.

Event Typical trigger Who consumes it What they need in it
Hire An accepted offer handed from the ATS, or an HR entry Payroll, benefits, identity, onboarding (I-9, new hire report) Person, employer, start date, job, location, pay, FLSA status, manager
Rehire A returning former employee The same, plus continuity rules The prior employment and worker ids, so year-to-date and wage bases continue
Transfer or promotion A change of job, manager, location or employer Payroll (rate, tax location), benefits (eligibility class), identity (groups) Effective date, old and new values
Pay change A raise, a pay basis change Payroll, compensation planning Effective date, earning code, old and new rate
Leave start and return A leave case approved Payroll (pay status), benefits (premium arrears), time Leave type, dates, paid or unpaid
Termination A resignation, discharge, layoff, death Payroll (final pay), benefits (end dates, COBRA), identity (revocation) Termination date, last day worked, reason, rehire eligibility
Rescind or correct A cancelled hire or a wrong date Everyone who acted on the original The event it reverses or corrects

The rehire case is where the core model's split pays off. A rehire is a new employments row on the existing workers row, so the Social Security wage base and year-to-date accumulators continue (Successor Employers and Wage Base Transfers covers the cases where they cross employers). Rippling models the same thing, one user with several worker records; Workplace & Collaboration notes that Microsoft's Workday connector re-enables the existing directory account when the old employee id comes back. Federal new hire reporting treats a returning employee as newly hired when they were separated "for at least 60 consecutive days" (Onboarding: I-9, E-Verify & New Hire Reporting covers the rest).

The rescind case is where most integrations break. An offer accepted, a hire event sent, an account created and a W-4 collected, and then the candidate doesn't start. The HRIS needs a rescind event that points at the hire, and each consumer needs to undo what it did: payroll removes a worker who was never paid, identity deletes an account never used, benefits cancels an enrollment that never started. A termination with a termination date equal to the hire date is not the same thing, because it produces a W-2-shaped worker with no wages and a separation in every report.

HRIS-to-Payroll Integration#

When the HRIS and payroll are separate products, the integration is the most consequential thing either vendor builds. Three shapes exist:

  • One product. HCM suites run HR and payroll on one record. There is no integration to fail, only the effective-dating rules inside one database.
  • Event or API sync. The HRIS emits changes and the payroll system applies them, or payroll polls a change feed. HiBob's lifecycle webhooks fire on the effective date; Rippling's change feed is filtered by applied_on to catch future-dated changes; BambooHR's webhooks may batch several employees in one request. Unified HR APIs sit in this layer for many systems at once (Unified HR & Payroll APIs covers them).
  • File. A scheduled export from the HRIS, a mapping step and an import into payroll, usually before each run. Still common between an enterprise HRIS and a separate payroll or a PEO.

Whatever the shape, the same rules make it reliable:

  • Cutoffs are part of the contract. A change effective in the current period must reach payroll before the run's approval deadline, or it becomes retro pay. The integration should report, per run, which changes arrived late and what they will cost.
  • Codes map once, centrally. HR job codes, departments and earning types map to payroll's earning codes, cost centers and work locations in one table with effective dates, and an unmapped value stops the change with an error, never a default.
  • Payroll owns pay data after the run. The HRIS owns the job, pay rate and status; payroll owns the statements, year-to-date and tax elections it computed with. Writing payroll results back into the HRIS (as some suites do for display) is a copy, not a second source.
  • Every change is idempotent and ordered per worker. A webhook retried five times must apply once (Consumers Are Idempotent), and a transfer and a pay change on the same day must apply in a stable order (Ordering). Several changes on one day are real (Oracle models them as sequenced rows), so the order key is the effective date plus a sequence, not a timestamp alone.
  • Reconcile, don't trust. A daily comparison of active workers, rates, locations and statuses between the HRIS and payroll catches the change that never arrived. A worker active in payroll and terminated in the HRIS is overpaid; the reverse is unpaid.

How to Build It#

These are deltas against the core data model. workers, employments and job_assignments already exist; this chapter adds positions and org units, the change events every downstream system reads, and the delivery record per consumer.

CREATE TABLE org_units (                   -- departments, cost centers, divisions, over time
  id                  UUID PRIMARY KEY DEFAULT uuidv7(),
  company_id          UUID NOT NULL REFERENCES companies (id),
  kind                TEXT NOT NULL CHECK (kind IN ('department', 'cost_center', 'division',
                        'location_group', 'other')),
  code                TEXT NOT NULL,         -- stable; payroll and the GL key on it
  name                TEXT NOT NULL,
  valid_during        DATERANGE NOT NULL,
  EXCLUDE USING GIST (company_id WITH =, kind WITH =, code WITH =, valid_during WITH &&)
);

CREATE TABLE org_unit_parents (            -- the tree, effective-dated, so old trees replay
  org_unit_id         UUID NOT NULL REFERENCES org_units (id),
  parent_org_unit_id  UUID NOT NULL REFERENCES org_units (id),
  valid_during        DATERANGE NOT NULL,
  EXCLUDE USING GIST (org_unit_id WITH =, valid_during WITH &&),
  CHECK (org_unit_id <> parent_org_unit_id)
);

CREATE TABLE positions (                   -- a seat that exists with or without an incumbent
  id                  UUID PRIMARY KEY DEFAULT uuidv7(),
  employer_id         UUID NOT NULL REFERENCES employers (id),
  position_number     TEXT NOT NULL,
  job_code            TEXT NOT NULL,         -- the job catalog: family, level, EEO category
  org_unit_id         UUID NOT NULL REFERENCES org_units (id),
  work_location_id    UUID REFERENCES work_locations (id),
  fte                 NUMERIC(4, 2) NOT NULL DEFAULT 1.00,
  flsa_status_default TEXT CHECK (flsa_status_default IN ('exempt', 'nonexempt')),
  status              TEXT NOT NULL CHECK (status IN ('proposed', 'approved', 'frozen',
                        'eliminated')),
  valid_during        DATERANGE NOT NULL,
  EXCLUDE USING GIST (employer_id WITH =, position_number WITH =, valid_during WITH &&)
);

ALTER TABLE job_assignments
  ADD COLUMN position_id UUID REFERENCES positions (id),   -- null under job management
  ADD COLUMN org_unit_id UUID REFERENCES org_units (id),
  ADD COLUMN job_code    TEXT,
  ADD COLUMN change_sequence SMALLINT NOT NULL DEFAULT 0,  -- several changes on one day
  ADD CONSTRAINT job_assignments_no_overlap EXCLUDE USING GIST (
    employment_id WITH =, valid_during WITH &&) WHERE (is_primary);

ALTER TABLE employments
  ADD COLUMN last_day_worked  DATE,          -- can precede the termination date
  ADD COLUMN rehire_eligible  BOOLEAN,
  ADD COLUMN prior_employment_id UUID REFERENCES employments (id);

CREATE TABLE worker_change_events (        -- every joiner, mover and leaver fact, append-only
  id                  UUID PRIMARY KEY DEFAULT uuidv7(),
  company_id          UUID NOT NULL REFERENCES companies (id),
  worker_id           UUID NOT NULL REFERENCES workers (id),
  employment_id       UUID REFERENCES employments (id),
  event_type          TEXT NOT NULL CHECK (event_type IN ('hire', 'rehire', 'transfer',
                        'promotion', 'pay_change', 'manager_change', 'location_change',
                        'leave_start', 'leave_return', 'termination', 'rescind', 'correction')),
  effective_on        DATE NOT NULL,         -- when it is true
  sequence            SMALLINT NOT NULL DEFAULT 0,
  recorded_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),   -- when the HRIS learned it
  source              TEXT NOT NULL CHECK (source IN ('hris_ui', 'ats', 'import', 'api',
                        'self_service', 'system')),
  actor               TEXT NOT NULL,
  changes             JSONB NOT NULL,        -- table, row, field, before, after
  reverses_event_id   UUID REFERENCES worker_change_events (id),   -- rescind and correction
  idempotency_key     TEXT NOT NULL,
  UNIQUE (company_id, idempotency_key),
  CHECK ((event_type IN ('rescind', 'correction')) = (reverses_event_id IS NOT NULL))
);

CREATE INDEX worker_change_events_order
  ON worker_change_events (worker_id, effective_on, sequence, recorded_at);

CREATE TABLE downstream_syncs (            -- one row per event per consumer
  id                  UUID PRIMARY KEY DEFAULT uuidv7(),
  worker_change_event_id UUID NOT NULL REFERENCES worker_change_events (id),
  target              TEXT NOT NULL CHECK (target IN ('payroll', 'benefits', 'identity',
                        'time', 'onboarding', 'unified_api', 'other')),
  target_ref          TEXT,                  -- the connector or partner
  deliver_on          DATE NOT NULL,         -- recorded date or effective date, per target
  status              TEXT NOT NULL CHECK (status IN ('scheduled', 'sent', 'acknowledged',
                        'failed', 'skipped', 'superseded')),
  attempts            SMALLINT NOT NULL DEFAULT 0,
  last_error          TEXT,
  acknowledged_at     TIMESTAMPTZ,
  missed_cutoff_pay_run_id UUID REFERENCES pay_runs (id),   -- arrived after a run's deadline
  UNIQUE (worker_change_event_id, target, target_ref)
);

An as-of query is the same shape everywhere: the assignment, rate and org unit whose ranges contain the date. "Who reported to this manager on December 31, 2026, in which cost center" reads:

SELECT ja.employment_id, ja.title, ou.code AS cost_center
FROM job_assignments ja
JOIN org_units ou ON ou.id = ja.org_unit_id AND ou.valid_during @> DATE '2026-12-31'
WHERE ja.manager_worker_id = $1
  AND ja.is_primary
  AND ja.valid_during @> DATE '2026-12-31';

Design notes:

  • The change event is the integration contract. Payroll, benefits and identity each read worker_change_events through their own downstream_syncs rows, never the HRIS tables directly, so a consumer can be replayed from any point and a new consumer can be backfilled (The Transactional Outbox).
  • Two clocks on every event. effective_on and recorded_at are both required, and deliver_on picks one per target: identity usually wants the effective date, payroll wants the recorded date so it can plan the run and flag retro.
  • Corrections point at what they correct. A correction or rescind event carries reverses_event_id, so each consumer can find what it did and undo or redo it, and the audit shows both values.
  • Late changes are measured. missed_cutoff_pay_run_id records which run a change missed, which is the number an employer needs to fix its process (and the number a provider needs to price its retro work).
  • Map with the neighbors. A hire event becomes an identity provider's account creation and, through SCIM, a members row in a collaboration product (How to Build It); the HRIS worker_id is the employeeNumber or externalId that ties them together.

Auditing#

The HRIS record is what an employer shows a wage-hour investigator, an EEOC charge, a benefits auditor and its own SOC 1 auditor. Keep:

  • Every effective-dated value with both clocks. What was true on each date and what the record said on each date, so a payroll that ran on bad data can be explained as well as corrected.
  • Every change with its actor, source and reason, including imports and integrations, in audit_events.
  • Every change event and its delivery per consumer, with acknowledgments and failures, so "did payroll get the termination before the run" is a query.
  • Approvals. Who approved each hire, pay change and termination, because segregation of duties over pay changes is a payroll control (Controls, Audits & SOC 1 covers them).
  • Org structures as of each date, so a headcount, an EEO-1 snapshot or a WARN count can be reproduced for a past date.

The mechanics (append-only history, typed actors, tamper evidence) are in Audit Trails.

What This Means for the System You Build#

  • Effective-date every field a calculation or rule reads, with one closed-open range per row, and keep recorded time separately so a correction is distinguishable from a backdated change.
  • Model change, future change, backdated change and correction as four operations, and tell every consumer which one happened.
  • Emit events when a change is recorded and when it takes effect, so no consumer has to poll for future-dated changes.
  • Keep person, worker and employment apart, so a rehire continues year-to-date totals and a transfer between EINs is two workers.
  • Give rescinds and corrections their own events that point at the event they reverse.
  • Map HR codes to payroll codes once, with dates, and fail on unmapped values.
  • Treat the payroll cutoff as part of the integration, and report every change that missed it.
  • Reconcile HRIS and payroll daily on status, rate, location and manager.

References#

Law and regulation

  • 42 USC 653a, a returning employee separated for at least 60 consecutive days is a newly hired employee for new hire reporting

Vendor documentation