Skip to content

Payrun Model

The creation of payrun objects requires that the case model is in place. The following procedure is recommended for modeling:

  1. Determine output values:

    • Payslip data
    • Legally required data (monthly, annual)
    • Data for downstream systems such as financial accounting
    • Break down output values into:

    • Wage types: each represents one calculation step for an output value

    • Collectors: aggregation values derived from wage types
    • Determine wage types with their processing order
    • Determine collectors and assign wage types to them
    • Determine clusters and assign them to wage types (optional)

Contents

Section Description
Regulation Design
Wage Types and Collectors Processing order, number ranges, assignment matrix
Sub Wage Types Year-to-date and derived sub-calculations
Payrun with Multiple Calendars Calendar assignment per wage type
Additional Payroll Results Custom attributes, custom results, payrun results
Payroll Results Result object types and cluster control
Job Lifecycle
Payrun Job Job status lifecycle and webhook events
Payrun Job Types Preview, Forecast and Legal job comparison
Payrun Job Invocation Name-based invocation fields
Payrun Job Preview Preview API — business scenarios, restrictions, example
Execution
Asynchronous Payrun Job Processing Background queue, HTTP 202, client polling
Parallel Employee Processing MaxParallelEmployees settings and thread safety
Payrun Restart Per-employee restart and run counter
Incremental Payrun Delta storage across multiple runs per period
Retroactive Calculation Steps, retro period limit, manual retro trigger
Diagnostics
Employee Processing Timing Per-employee duration logging
Processing Pipeline Logging Log levels per phase, trace override

Wage Types and Collectors

Wage types are typically executed in the following order: 1. Income and benefits 2. Gross salary 3. Deductions and expenses 4. Net salary 5. Consolidated values (Collectors)

To simplify access to payroll results, step 5 additionally maps collector results as wage types.

The next step is to define the wage type number ranges:

Wage Type Range Scope
1000 - 4999 Income and benefits
5000 - 5009 Gross salary
5010 - 6499 Deductions and expenses
6500 - 6599 Net salary
6600 - 9099 Consolidated values

To document wage types and collectors, it is advisable to create an assignment matrix. The following example shows such a matrix:

Wage Type # Wage Type Name Gross Salary Collector Withholding Tax Collector
1000 Monthly wage yes yes
... ... . .
1005 Hourly wage yes yes
... ... . .
1980 Further education yes no
... ... . .
5000 Gross salary no no
... ... . .
6500 Net salary no no
... ... . .
9070 Withholding tax no no

In this example, the withholding tax collector result is exposed as wage type 9070.

The matrix can be extended for special cases:

  • Additional clustering columns
  • Collectors can be grouped for complex scenarios

Sub Wage Types

Wage types are calculated in numerical order, which allows sub-wage types to be defined.

In the following example, sub wage type 1000.1 calculates the year-to-date value of wage type 1000:

Wage Type # Wage Type Name Calculation Formula
1000 Monthly wage ...
1000.1 Monthly wage year-to-date Total of wage type 1000 across all cycle periods

Sub wage types with complex data queries can be excluded using clustering.

Payrun with Multiple Calendars

The calculation of wage data is based on the calendar assigned to the employee, division or tenant. In situations where a payrun must handle different calendars, this can be configured per wage type:

Wage Type # Wage Type Name Calendar
1000 Monthly wage Monthly payroll calendar
1001 Bi-week wage Bi-week payroll calendar

Additional Payroll Results

Additional payroll results can be stored as:

  • Wage type result attributes — best performance
  • Custom wage type results — easy to query
  • Payrun results — for non-numerical or arbitrary data

Low-code example: set a custom attribute in the wage type value expression.

SetResultAttribute("MyAttribute", 2560)

Low-code example: add a custom result in the wage type value expression.

AddCustomResult("MySource", 9217)

No-code example: set a payrun result using a wage type value action.

^|MyPayrunResult = PeriodStartDate

Payroll Results

The results of the payrun are stored in the following objects:

Object Description
Collector Result The aggregated decimal result of a collector
Collector Custom Result User-defined collector result (decimal)
Payrun Result Payrun-specific result, including non-numerical values
Wage Type Result The wage type result (decimal) including custom attributes
Wage Type Custom Result User-defined wage type result (decimal)

Cluster sets in the payroll configuration control which results are generated (see Clusters).

Wage Type Custom Results are also generated automatically — one per case value time split — when clusterSetWageTypePeriod is configured on the payroll. See Wage Calculation Traceability.


Payrun Job

The payrun job starts the payrun for a pay period and stores the results in the payroll result. The underlying payroll uses the employee's division assignment to determine whether the employee is included.

A payrun job defines the purpose of execution:

  • Statutory payroll run
  • Forecast analysis of payroll data for projections, scenario planning, etc.

The payrun job is controlled by its job status:

Job Status Type Description Webhook
* New payrun job
Draft Working Statutory payrun job for preview
Release Working Statutory payrun job released for processing
Process Working Statutory payrun job in processing PayrunJobProcess
Complete Final Statutory payrun job successfully completed PayrunJobFinish
Forecast Final Forecast payrun job
Abort Final Statutory payrun job aborted before release
Cancel Final Statutory payrun job failed during processing PayrunJobFinish

For statutory payruns, only one job in Draft status is allowed per payrun type and pay period. Multiple jobs with Release or Process status are possible. Forecast payruns can be executed any number of times.

Payrun Job Status

Payrun Job Types

The Payroll Engine supports three distinct payrun job types, each designed for different stages of the payroll workflow.

Aspect Preview Job Forecast Job Legal Job
Use Cases Pre-payroll validation, what-if, onboarding, testing Budget planning, predictive analytics Legally binding payroll calculation
Persistence Non-persistent — API response only Persistent — separate from legal results Persistent — legally binding results
API Endpoint POST .../payruns/jobs/preview POST .../payruns/jobs/start POST .../payruns/jobs/start
Endpoint Return PayrollResultSet (synchronous) Payrun Job Id (asynchronous) Payrun Job Id (asynchronous)
Invocation Identifier Forecast name set Forecast field empty
Retro Pay HTTP 422 if retro required Full retro support (forecast scope) Full retro support
Number of Employees Exactly one One or more One or more
Job Status Start — (no persisted job) Draft
Job Status End — (no persisted job) Forecast (immediate) Complete (after approval process)
Jobs per Period Unlimited Unlimited One active job at a time
Case Data Legal and forecast case data Forecast-specific case data Legal case data
Visible in Reports No Yes (forecast reports) Yes (legal reports)

Legal jobs go through a multi-stage approval process. As long as this process is not completed, parallel legal jobs are not possible. Preview and forecast jobs have no such restriction.

Choosing the Right Job Type

Use a preview job when you need immediate calculation results for a single employee without any side effects — ideal for validation, testing, and interactive what-if scenarios in the UI.

Use a forecast job when you need to simulate payroll across multiple employees with persistent results that can be analyzed later — ideal for budget planning and business case evaluation.

Use a legal job for the actual, legally binding payroll run that produces the official payslip results.

See Payrun Job Preview for detailed preview scenarios and examples. See Forecasts for forecast-specific workflows.

Payrun Job Invocation

The PayrunJobInvocation uses name-based references to identify the payrun and the executing user:

Field Description
PayrunName Name of the payrun to execute
UserIdentifier Identifier of the executing user

Breaking Change (v0.9.0-beta18): The previous id-based properties PayrunId and UserId have been removed. Existing integrations must switch to PayrunName and UserIdentifier.

Payrun Job Preview

The preview API executes a payrun for a single employee and returns the calculation results without persisting anything to the database. This is useful for several real-world scenarios where you need to see payroll results before committing them.

Business Scenarios

Pre-Payroll Validation

Before running the official monthly payroll, HR can preview each employee's payslip to verify that recent case changes (salary adjustments, employment level changes, new bonuses) produce the expected results. If something looks wrong, the data can be corrected before the actual payrun.

What-If Simulation

Managers planning a salary increase or a change in employment level can preview the impact on gross and net pay, social insurance contributions, and withholding tax — without creating draft payrun jobs or polluting the payroll history.

Onboarding Verification

When setting up a new employee with their initial case values (monthly wage, employment level, location, etc.), the preview confirms that all wage types calculate correctly before the first real payrun.

Automated Testing

Preview enables fast, side-effect-free payrun tests in CI/CD pipelines. Tests run against the preview endpoint, verify results, and leave the database untouched. See Payroll Testing for details.

API Endpoint

The preview endpoint is synchronous and returns the complete PayrollResultSet in the response body:

POST /api/tenants/{tenantId}/payruns/jobs/preview

The request body is a standard PayrunJobInvocation with exactly one employee identifier.

Restrictions

Preview mode has two important limitations:

  1. No retro pay calculation. Preview accepts any RetroPayMode (including the default ValueChange) to match production behavior. If retroactive calculation is actually triggered during processing — because mutations in prior periods are detected — the endpoint returns HTTP 422 Unprocessable Entity with a descriptive error message including the employee identifier and retro date. This makes it immediately visible that the preview period requires retro calculation, rather than silently producing different results.

  2. No historical result queries. Wage type expressions that query persisted results from prior periods (e.g. GetPeriodWageTypeResults) will only find results from previously persisted payrun jobs — not from earlier preview invocations within the same test run.

Example

The following example demonstrates a preview test with monthly wage, hourly wage, bonus, age supplement, withholding tax, and special bonus calculations.

Payroll Setup

The payroll defines these wage types and collectors:

Wage Type Name Expression Collectors
101 MonthlyWage MonthlyWage × EmploymentLevel SocialInsurance, TaxBase
101.1 MonthlyWage.Credit MonthlyWage × 5% Credit
101.2 MonthlyWage.Debit MonthlyWage × −5% Debit
102 HourlyWage NumberOfHours × HourlyWage SocialInsurance, TaxBase
103 Bonus Bonus case value SocialInsurance, TaxBase
106 AgeSupplement Age ≥ 50 ? MonthlyWage × 7.5% : 0
206 WithholdingTax TaxBase × lookup(WithholdingTax)
207 SpecialBonus WageType[101] × rangeLookup(SpecialBonus, WageType[101])

Employee case values include monthly wage (5000, later 6000), employment level (50%, later 60%, then 50%), hourly wage (50/h, later 75/h), working hours, location, birth date, bonuses, and department assignments — all with different effective dates.

Preview Test Definition

The test file (Test.et.json) defines seven preview invocations for employee jane.doe@foo.com covering January 2019 through January 2020. Each invocation sets retroPayMode to None explicitly:

payrunJobInvocations:
- name: EmployeePreview.TestPayrun1.Jan19
  payrunName: EmployeePreview.TestPayrun1
  payrollName: EmployeePreview.Test
  userIdentifier: john.smith@foo.com
  employeeIdentifiers:
  - jane.doe@foo.com                    # preview requires exactly one employee
  retroPayMode: None                    # required: retro triggers HTTP 422 in preview
  jobStatus: Complete
  periodStart: 2019-01-01T00:00:00.0Z
  evaluationDate: 2019-02-01T00:00:00.0Z  # simulates payrun executed in February
  reason: Demo Payrun Jan 19

Expected Results

Each invocation is matched against expected wage type and collector results:

Period WT 101 WT 102 WT 103 WT 106 WT 206 WT 207 SocialIns TaxBase
Jan 19 2'500 275 99'999 0 5'138.70 125 102'774 102'774
Feb 19 2'925 125 0 0 152.50 146.25 3'050 3'050
Mar 19 3'000 0 0 0 150.00 90 3'000 3'000
Apr 19 3'000 0 5'555 0 427.75 90 8'555 8'555
May 19 3'000 562.5 0 450 178.13 90 3'562.5 3'562.5
Dec 19 3'000 0 0 450 240.00 90 3'000 3'000
Jan 20 3'000 0 0 450 240.00 90 3'000 3'000

The January 2020 invocation also verifies a payrun result:

Payrun Result Value Type Value
MonthlyWage Money 6'000

Running the Preview Test

The Payroll Console command PayrunEmployeePreviewTest executes the test:

PayrunEmployeePreviewTest Test.et.json
PayrunEmployeePreviewTest Test.et.json /showall

See also Payroll Testing for all available test methods.


Asynchronous Payrun Job Processing

For large payrolls (500+ employees), the payrun job endpoint supports asynchronous processing via a background queue. The endpoint returns HTTP 202 Accepted immediately with a Location header for status polling, preventing HTTP timeout errors during long-running payroll calculations.

The processing pipeline works as follows: 1. The payrun job is pre-created and persisted with status Process. 2. The job is enqueued into a bounded channel (capacity: 100) for backpressure control. 3. A background worker dequeues and processes jobs sequentially. 4. On completion or abort, a webhook notification is sent (PayrunJobFinish).

If the background service encounters an unhandled exception or shuts down, running jobs are aborted and the webhook is triggered with the abort status.

The client polling pattern:

POST /api/tenants/{tenantId}/payruns/jobs
→ HTTP 202 Accepted
Location: /api/tenants/{tenantId}/payruns/jobs/{jobId}

The client polls the returned location URL to determine when the job has completed. The job status transitions through ProcessComplete (or Cancel on failure).

Breaking Change (v0.9.0-beta14): This endpoint returns HTTP 202 instead of HTTP 201. Clients must poll the status endpoint to determine job completion.

Parallel Employee Processing

The payrun can process employees in parallel to reduce total execution time. The MaxParallelEmployees setting controls the degree of parallelism:

Value Behavior
0 or off Sequential processing (default)
half Half of available CPU cores
max All available CPU cores
-1 Automatic (runtime decides)
1N Explicit thread count

Each employee is processed within an isolated PayrunEmployeeScope that provides mutable state isolation. Progress reporting is thread-safe with batched database persistence (every 10 employees). The payroll calculator cache uses Lazy<T> with a composite key (calendar + culture) for thread-safe reuse across employees.

Sequential processing remains the default for deterministic behavior. Enable parallel processing only after verifying that your regulation scripts do not share mutable state across employees.

Payrun Restart

During the payrun, all wage types are processed in the order of their wage type numbers. In special cases, the payrun can be restarted for an individual employee. Each run is identified in the wage type by a run counter. Runtime values (see Payrun Scripting) can be used to exchange data between runs.

See Testing for how to verify payrun behavior with automated tests.

Incremental Payrun

When multiple payruns are executed within a single period, the engine stores only incremental results — values that have changed since the last payrun. The REST API provides dedicated endpoints to retrieve the currently valid (consolidated) results for a pay period.

Retroactive Calculation

Retroactive calculations are triggered automatically when mutations have occurred after the last payrun that affect prior pay periods.

The retroactive calculation steps are: 1. Calculate the current pay period (accounting for mutations into prior periods)

  • Execution phase: Setup
  • Results are transient
  • Calculate all affected prior periods, starting from the earliest mutation period

  • Store incremental results for each prior period

  • Recalculate the current pay period

  • Execution phase: Reevaluation

  • Consolidated results include prior retroactive results
  • Store final results

For forecasts, prior runs with the same forecast name apply. The number of retroactive periods is unlimited; however, retroactive calculation can be restricted to the current payroll cycle.

Retro Period Limit

The MaxRetroPayrunPeriods setting (default: 0/unlimited) provides a safety guard against runaway retroactive calculations with RetroTimeType.Anytime. When a positive value is set, the engine limits the number of retroactive periods processed per payrun job.

Retroactive Calculation

Manual Retroactive Calculation

In addition to automatic retroactive calculation, a retroactive payrun can also be triggered manually via scripts. Results generated during retroactive calculation are tagged with Payroll Result Tags, which can be used as filter criteria when querying payroll results.

The following scenario shows a current-period payrun triggering two retroactive payruns:

Manual Retroactive Payrun Scenario


Employee Processing Timing

When LogEmployeeTiming is enabled, the engine logs per-employee processing duration and a summary (processing mode, total time, average time per employee) at the Information log level. This is useful for identifying slow employees and tuning payrun performance.

Processing Pipeline Logging

The payrun processor uses a three-tier logging strategy to balance multi-job monitoring with detailed diagnostics.

Log Level Content Use Case
Information Start and completion summary per job (payrun name, tenant, job id, status, employee count, total duration) Multi-job monitoring — 2 lines per job
Debug Job creation/update confirmations in StartJobAsync State change tracking
Trace Phase 1–7 entry and completion with per-phase timing, context details (payroll, division, period, wage type/collector counts, employee source, job status) Single-job diagnostics
Error Job abort with job id and reason All levels

All log messages include a [Preview] prefix when running in preview mode, and a [retro] tag on the start message when processing a retroactive job. The job id is included in all messages from Phase 2 onward.

To enable detailed phase logging for a specific job, set the LogLevel on the PayrunJobInvocation to Verbose.

Enabling Trace Logging in Deployment

The default Serilog configuration uses Information as minimum level. Trace and Debug messages are filtered by Serilog before they reach the log sinks. To enable phase-level diagnostics without lowering the global log level, add a namespace override to the Serilog configuration:

"Serilog": {
  "MinimumLevel": {
    "Default": "Information",
    "Override": {
      "System": "Warning",
      "Microsoft": "Warning",
      "PayrollEngine.Domain.Application": "Verbose"
    }
  }
}

Remove the override after diagnostics are complete to avoid excessive log volume in production.


Next Steps