Two Paths to Extending Country Logic
A well-designed country regulation does not force providers to modify its calculations. Instead, it exposes extension points — wage types that are structurally complete (they have a number, a name, collector memberships, and a value expression) but produce zero until external input activates them.
There are two types of extension points, and they require fundamentally different activation strategies:
Data stubs are wage types that read a CaseField and return its value. When the CaseField is empty, the wage type produces zero. When you enter a value into the CaseField, the wage type activates. No regulation overlay is needed — you activate the feature by entering data.
Logic stubs are wage types that contain a placeholder formula (typically returning zero or a fixed value). The calculation logic is intentionally absent because it varies by provider. To activate the feature, you must create a provider regulation that overrides the wage type with the actual formula.
| Type | Activation | Overlay Required | Effort |
|---|---|---|---|
| Data stub | Enter value in CaseField | No | Minutes |
| Logic stub | Override wage type in provider regulation | Yes | Hours (formula development + testing) |
Every country regulation documents its extension points in a ProviderStubs.md file. This file lists each stub, its type (data or logic), the CaseFields involved, and the expected behavior when activated. Reading this file is the first step before planning any provider customization.
Data Stubs: Configuration Without Code
Data stubs are the simplest extension mechanism. The country regulation defines a wage type that reads a CaseField using a No-Code token. When the CaseField has no value, the token resolves to zero. When you enter a value, the wage type picks it up on the next payrun.
Consider the German regulation’s holiday bonus (Urlaubsgeld). WT 1100 is defined in the country regulation:
{
"wageTypeNumber": 1100,
"name": "Urlaubsgeld",
"description": "Holiday bonus - data stub, activated by CaseField entry",
"valueActions": [
"^^Urlaubsgeld"
],
"collectors": ["Gesamtbrutto"]
}
The ^^Urlaubsgeld token reads the CaseField DE.Urlaubsgeld. If no value has been entered for the employee, the token returns zero. The wage type executes, contributes zero to Gesamtbrutto, and has no effect on the payslip. To activate it, you enter a value in the employee’s case data — say, 2400.00 EUR for an annual holiday bonus paid in June. The next payrun in June picks up the value, and WT 1100 contributes 2400.00 to gross income.
The same pattern appears across multiple German compensation components:
| WT | Name | CaseField | Activation |
|---|---|---|---|
| 1100 | Urlaubsgeld | DE.Urlaubsgeld |
Enter bonus amount |
| 1200 | Firmenwagen | DE.FwBruttoListenpreis |
Enter car list price |
| 3100 | U1Umlage | DE.U1UmlageSatz |
Enter employer’s U1 contribution rate |
Data stubs are maintenance-free. When the country regulation updates — new tax brackets, revised collector logic — the data stub continues to work because its formula is just a CaseField read. The value flows into whatever collectors the country regulation has wired, and downstream calculations adapt automatically.
Logic Stubs: Provider Override Required
Some calculations cannot be reduced to a CaseField read. Pension contributions in the Netherlands, for example, depend on the pension fund, the employee’s age, the franchise amount, the pensionable salary cap, and the contribution percentage — all of which vary by employer. The country regulation cannot anticipate every possible pension formula.
Instead, the Dutch regulation defines WT 5200 as a logic stub:
{
"wageTypeNumber": 5200,
"name": "PensioenWerknemer",
"description": "Employee pension contribution - logic stub, requires provider override",
"valueActions": [
"0"
],
"collectors": ["WerknemerInhoudingen"]
}
This wage type returns zero. It occupies the correct position in the execution sequence (after gross income, before net pay), it feeds the correct collector (WerknemerInhoudingen — employee deductions), and it has the correct name for payslip rendering. But the formula is absent.
To activate it, a provider creates a regulation overlay with the same wage type number:
{
"name": "ACME.Provider",
"namespace": "ACME",
"baseRegulations": ["NL.Loonheffing"],
"wageTypes": [
{
"wageTypeNumber": 5200,
"name": "PensioenWerknemer",
"description": "Employee pension contribution - ABP fund",
"valueActions": [
"CalculateAbpPension('NL.PensioenGrondslag', 'NL.Franchise')"
],
"collectors": ["NL.WerknemerInhoudingen"]
}
]
}
The provider’s WT 5200 completely replaces the country’s WT 5200. This is a full override — the country stub is invisible once the provider layer is active. The provider’s formula calls a Custom Action (CalculateAbpPension) that implements the ABP pension fund’s specific contribution rules.
Logic stubs require more effort than data stubs: you need to develop the formula, write a Custom Action if the calculation is complex, create test cases, and maintain the override through country regulation updates. But they provide full flexibility — the provider controls every aspect of the calculation.
Override by Key: How PE Resolves Conflicts
When two regulations in the same payroll layer stack define the same object, Payroll Engine uses a deterministic resolution rule: the higher layer wins. But what counts as “the same object” depends on the object type. Each object type has its own identity key:
| Object Type | Identity Key | Example |
|---|---|---|
| Case | Name | DE.Employment |
| CaseField | Name | DE.Salary |
| Collector | Name | DE.Gesamtbrutto |
| WageType | Number | 5200 |
| Lookup | Name | DE.LStTarif |
| Report | Name | DE.Payslip |
The critical entry in this table is WageType. Unlike every other object type, wage types are identified by number, not by name. When a provider regulation defines WT 5200, it takes priority over the country regulation’s WT 5200 regardless of name, namespace, or any other attribute. The number is the sole identity.
But “takes priority” does not mean “replaces unconditionally.” The WageType Value function uses Top-Down override: the engine executes the highest layer’s script first. If it returns a value, that value is used. If it returns null, the engine falls through to the next lower layer’s script. This means an override can be conditional — you only take over the calculation when your specific logic applies, and let the country regulation handle everything else.
For cases, case fields, collectors, lookups, and reports, the identity key is the fully qualified name (namespace + short name). A provider regulation in namespace ACME cannot accidentally override DE.Gesamtbrutto by defining a collector called Gesamtbrutto — it would become ACME.Gesamtbrutto, a completely separate object. To override a country collector, you would need to be in the same namespace, which is architecturally prevented by the layer model.
ACME overrides WT 5200 in namespace NL. This is by design — it is the mechanism that makes stub overrides work. But it also means you must never use a number that belongs to the country range (1–7999) unless you are intentionally overriding a stub.
The Extend-First Rule
Override is powerful, but it comes in two forms — and understanding the difference is critical for long-term maintenance.
Conditional Override: Return Null to Fall Through
The Top-Down override mechanism means your provider script runs first. If it returns null, the country regulation’s script runs instead. This enables a conditional override pattern: your logic executes only when a specific condition is met, and the country’s logic handles everything else.
[WageTypeValueAction("ACMEPensionOverride")]
public ActionValue ACMEPensionOverride()
{
var fundType = GetCaseValue<string>("ACME.PensionFundType");
if (fundType != "BPV")
return ActionValue.None; // null → country script runs
// BPV-specific calculation
var grondslag = (decimal)WageType[2990];
var rate = GetLookupField<decimal>("BPVRates", PeriodStart.Year, "EmployeeRate");
return new(Math.Round(grondslag * rate, 2));
}
In this pattern, the provider’s override runs first and checks whether the employee belongs to the BPV fund. If not, it returns null, and the country regulation’s pension calculation takes over. If yes, the provider’s BPV formula produces the result. This is the recommended practice: the overriding logic runs conditionally first.
WageType Value functions, the engine calls the highest layer first. If it returns a non-null value, that value is used. If it returns null, the engine falls through to the next lower layer. This applies to all scripting functions with Top-Down override behavior — including Case Available, Case Build, Case Validate, and Collector Apply. See the Scripting Functions reference for the full table.
Full Override vs. Extension
When you always return a value (never null), the override is unconditional — the country’s script never executes. This is appropriate for logic stubs that return zero, where you are replacing a placeholder with a real formula.
The alternative to both override patterns is extension: adding a new wage type with a provider-range number (9100–9399) that contributes to the country’s collectors. Your wage type runs in addition to the country’s calculations, not instead of them.
The decision tree:
| Question | Answer | Action |
|---|---|---|
| Is the country WT a stub (returns 0)? | Yes | Override (unconditional) — the stub exists for this purpose |
| Do you need different logic only for specific employees? | Yes | Override (conditional) — return null to fall through |
| Is the country WT a live calculation you want to keep? | Yes | Extend — add WT 9xxx that feeds the same collectors |
| Do you need to add a new compensation component? | Yes | Extend — create WT 9xxx in your namespace |
Extension is the safest default. A new wage type at 9120 that calculates a shift allowance and feeds DE.Gesamtbrutto is completely independent of the country regulation’s internals. When the country regulation updates its tax algorithm, your allowance flows into the updated calculation automatically.
Conditional override is the middle ground: you take control only when needed, and the country logic handles the standard case. This is particularly useful when a sector-specific rule applies to some employees but not others.
null fallback is the best of both worlds when the use case supports it.
What Happens When the Country Regulation Updates
Country regulations update at least annually. Tax brackets change, contribution ceilings are revised, new statutory requirements emerge. Understanding how these updates interact with your override and extension decisions is essential for long-term maintenance planning.
If you have overridden unconditionally: The country regulation’s new version of that wage type is invisible to your payroll. Your override always returns a value, so the engine never reaches the country’s script. If the country vendor added a new parameter, improved its collector wiring, or fixed a rounding issue — none of that reaches your calculation. You must review the country’s changelog, determine whether the changes affect your override, and update your formula manually.
If you have overridden conditionally (with null fallback): The country regulation’s update flows through for all employees where your override returns null. Only the cases where your condition matches are affected by your formula — and those are the cases where you intentionally deviate from the country logic. This significantly reduces the maintenance surface compared to unconditional override.
If you have extended with a new wage type: The country regulation’s update flows through automatically. Your WT 9120 reads the updated gross income collector, which now reflects the new tax calculation. Your extension does not need to change — it sits on top of a foundation that updates itself.
| Approach | Country Update Impact | Maintenance Effort |
|---|---|---|
| Unconditional override | Country changes invisible — manual review required | High: review changelog, update formula, re-test |
| Conditional override (null fallback) | Country changes flow through for non-matching employees | Medium: only review changes relevant to your condition |
| Extension (new WT number) | Country changes flow through automatically | Low: verify your extension still produces correct results |
The three approaches form a spectrum of control vs. maintenance. Unconditional override gives you full control at the cost of ongoing maintenance. Conditional override gives you targeted control with automatic fallback. Extension gives you automatic updates at the cost of limited control — you can add to the country’s calculation but not change it.
For organizations managing multiple countries, the maintenance impact compounds. A provider that overrides five wage types across four countries has twenty override points to review with every annual update. A provider that extends with new wage types has zero override points and only needs to verify that its extensions still produce correct results against the updated country calculations.
The practical recommendation: treat overrides as technical debt. Every override is a commitment to track the country regulation’s evolution for that specific calculation. Document each override with the reason it was necessary, the country WT it replaces, and the conditions under which it could be removed (for example, “remove when the country regulation supports configurable franchise amounts”). Review the list before every annual update cycle.
For the full picture of how regulation layers compose at runtime — including how baseRegulations, payroll layers, and namespace isolation work together — see The Four-Layer Regulation Model and The Composable Regulation Model.
Find your extension points
Every country regulation documents its stubs. Explore the ProviderStubs catalog for your country — or get in touch to plan your overlay.
Get in Touch →