Payrun Model
The creation of payrun objects requires that the case model is in place. The following procedure is recommended for modeling:
-
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 Resultsare also generated automatically — one per case value time split — whenclusterSetWageTypePeriodis 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 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
PayrunIdandUserIdhave been removed. Existing integrations must switch toPayrunNameandUserIdentifier.
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:
-
No retro pay calculation. Preview accepts any
RetroPayMode(including the defaultValueChange) 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. -
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 Process → Complete (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) |
1–N |
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.
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:
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
- Regulation Design guidelines
- Lookups design guidelines
- Case Model design guidelines