What Cases Are

Before a single wage type runs, the payroll engine needs to know about the employee: their monthly salary, their contracted working hours, their start date, their tax class, whether they opted into a company pension. This information lives in Cases and CaseFields — the structured data containers that feed every downstream calculation.

A Case is a logical grouping of related fields. An “Employment” case might contain salary, working hours, and start date. A “TaxSettings” case might contain tax class, church tax state, and a personal tax allowance. A “Pension” case might hold the opt-in flag, the contribution percentage, and the provider name. Each Case groups fields that belong together conceptually and that an HR administrator would typically enter at the same time.

A CaseField is a single data point within a Case. It has a name, a value type (Money, Decimal, Percent, String, Boolean, Integer, Date), and a time type that governs how the value behaves across payroll periods. Every CaseField carries metadata — display labels, formatting hints, validation constraints — that the Payroll Engine WebApp uses to render input forms automatically.

Here is a minimal Employment case definition in regulation JSON:

{
  "name": "Employment",
  "caseType": "Employee",
  "fields": [
    {
      "name": "Salary",
      "valueType": "Money",
      "timeType": "CalendarPeriod"
    },
    {
      "name": "WorkingHours",
      "valueType": "Decimal",
      "timeType": "Period"
    },
    {
      "name": "StartDate",
      "valueType": "Date",
      "timeType": "Timeless"
    }
  ]
}

The engine resolves these fields by namespace. If the regulation declares "namespace": "DE", the field Salary is stored as DE.Salary. Inside the same regulation, you reference it as Salary (short name). From a different regulation layer, you use the fully qualified DE.Salary. This namespace mechanism prevents collisions when multiple regulation layers contribute fields to the same payroll.

Key principle: Cases define what data can exist in the system. They do not calculate anything. Wage types read Case values; Cases do not read wage type results. This separation — data model vs. calculation model — is fundamental to the composable architecture.

Case Types and Their Scope

Not all payroll data lives at the same level. An employee’s salary is personal. A company’s Umlage contribution rate applies to all employees. A country’s minimum wage applies nationwide. The Payroll Engine distinguishes these levels through Case Types:

Case Type Scope Lifecycle Examples
Employee Per employee, per period Created when employee is onboarded; values change over employment lifecycle Salary, tax class, church tax, pension opt-in, working hours
Company Per employer Set once during company setup; updated when company-wide policy changes Umlage rates, employer pension contribution, company car policy
National Country-wide Set by the regulation; rarely changed by providers Minimum wage, statutory holidays, standard working days per month

When a wage type calculation requests a Case value, the engine resolves it through the type hierarchy. An Employee case is always employee-specific — each employee has their own salary, their own tax class. A Company case is shared across all employees of that employer — the Umlage rate is the same for everyone. A National case is shared across all employers in that country regulation.

The practical consequence for regulation developers: most Cases you create will be Employee type. These are the fields that HR administrators interact with daily — new hires, salary changes, tax card updates. Company cases are less frequent but important for parameters that vary by employer (e.g., an industry-specific contribution supplement). National cases are rare in regulation overlays because statutory parameters are better handled through data regulation satellites with versioned lookups.

Provider-built regulations almost always add Employee cases. If you are building a provider overlay for travel expense management, your “TravelSettings” case with fields for home office days and commute distance is an Employee case — every employee has different values.

Time Types: When Values Change

Payroll is inherently temporal. An employee earns 3,000 EUR per month until June 15, then 3,500 EUR from June 16 onward. What is the correct salary for June? The answer depends on the field’s time type — a property that tells the engine how to handle values that change within a payroll period.

This is where many payroll systems get it wrong. A salary is not the same kind of value as a tax class. A salary of 3,000 EUR that applies for half the month should contribute roughly half of 3,000 to that month’s payroll. But a tax class that changes mid-month should use the new class for the entire second portion — it is not averaged or prorated. The Payroll Engine models this distinction through four time types:

Time Type Mid-Period Behavior The 0.4 Test Example Fields
CalendarPeriod Scaled by sub-period days: value × subDays / periodDays 0.4 from mid-month → ~0.2 for the second half (proportional) Monthly salary, housing allowance, employer pension contribution amount
Period Factor/tariff from a date — not scaled 0.4 from mid-month → 0.4 for every day in the second half Working hours per week, part-time percentage, contribution rate
Timeless Contract-fixed, never time-segmented 0.4 always returns 0.4, regardless of period Contract type, IBAN, employee ID, seniority rate
Moment Point-in-time event, no period distribution Not applicable — exists at a single instant Termination date, one-time bonus trigger

The 0.4 test is the modeling heuristic every regulation developer should apply when defining a new CaseField. Ask: “If this value is 0.4 from mid-month, what is one day in the second half worth?” If the answer is proportional to the number of days (0.4 × subDays / periodDays), it is CalendarPeriod. If the answer is simply 0.4 regardless of days, it is Period. If the value never changes mid-period in practice, consider Timeless.

Tip — time type and arithmetic. When a wage type multiplies two CaseField values, at most one operand should be CalendarPeriod. If both are CalendarPeriod, the scaling is applied twice and the result is silently too small. The other operand should be Period (factor from a date) or Timeless (contract-fixed rate). This constraint is documented in the No-Code development patterns and affects both No-Code and Custom Action calculations.

Getting the time type wrong is one of the most common regulation development errors. A salary field marked as Period will not be prorated for mid-month changes — an employee who starts on the 16th gets a full month’s salary instead of half. A part-time percentage marked as CalendarPeriod will be scaled when multiplied with salary, producing a double-scaled result. The 0.4 test catches both errors at design time.

Progressive Disclosure with availableActions

A country regulation for Germany might define fifty CaseFields across employment, tax, social security, company car, occupational pension, and supplementary insurance cases. Showing all fifty fields to an HR administrator entering a new hire is overwhelming and error-prone. The availableActions mechanism solves this by conditionally showing fields based on what other fields contain.

Consider a travel expense case with two fields: TravelKm (daily commute distance) and TravelRate (per-kilometer reimbursement rate). If the employee does not commute by car, TravelKm is empty and TravelRate is irrelevant. With availableActions, the rate field only appears when the distance field has a value:

{
  "name": "TravelExpense",
  "caseType": "Employee",
  "fields": [
    {
      "name": "TravelKm",
      "valueType": "Decimal",
      "timeType": "Period"
    },
    {
      "name": "TravelRate",
      "valueType": "Money",
      "timeType": "Period",
      "availableActions": [
        "? ^^TravelKm.HasValue"
      ]
    }
  ]
}

The syntax follows the No-Code action pattern: every line in availableActions must start with ?, which marks it as a condition expression. The ^^ token references another CaseField’s current value. .HasValue checks whether the field contains any value at all.

More complex conditions combine multiple fields. A German occupational pension case might show the employer contribution field only when the employee has opted in and selected a pension provider:

{
  "name": "EmployerPensionContribution",
  "valueType": "Money",
  "timeType": "CalendarPeriod",
  "availableActions": [
    "? ^^PensionOptIn.Value == true && ^^PensionProvider.HasValue"
  ]
}

Rule: Every line in availableActions must start with ?. There are no instruction lines — only conditions. The field is visible when all conditions evaluate to true. A common compiler error (CS0201) results from omitting the ? prefix.

Progressive disclosure scales well. A regulation with fifty fields might show only eight on initial data entry — the rest appear as the administrator fills in relevant information. This keeps the UI clean without reducing the regulation’s capability. The WebApp renders available fields automatically based on the current state; no frontend customization is needed.

Validation: buildActions and validateActions

Entering payroll data incorrectly is expensive. A negative salary silently propagates through tax and social security calculations, producing an entire payslip of wrong numbers. A working-hours value of 200 per week passes through unchecked, inflating hourly rate derivations. Cases prevent these errors through two action mechanisms: buildActions and validateActions.

buildActions run when the user saves the case. They set defaults, auto-calculate derived values, and normalize inputs. If an employee’s working hours field is left empty, a buildAction can default it to the standard 40 hours. If a travel distance is entered as a negative number, a buildAction can take the absolute value.

{
  "name": "TravelKm",
  "valueType": "Decimal",
  "timeType": "Period",
  "buildActions": [
    "SetFieldMinValue('TravelKm', 0)"
  ]
}

validateActions run after buildActions and can reject the save entirely. If a salary is below the legal minimum, the validation blocks the entry and returns an error message. If a start date is in the past beyond a permitted threshold, the validation prevents it.

{
  "name": "Salary",
  "valueType": "Money",
  "timeType": "CalendarPeriod",
  "validateActions": [
    "? ^^Salary.Value > 0 : Salary must be greater than zero",
    "? ^^Salary.Value <= 999999 : Salary exceeds maximum allowed value"
  ]
}

The validate pattern uses a condition followed by a colon and an error message. If the condition is false, the message is returned to the UI and the save is blocked. Multiple conditions can stack — the engine evaluates all of them and returns all failing messages at once, so the administrator can fix everything in a single pass.

Tip: Use buildActions for corrections the system can make automatically (defaults, clamping, normalization). Use validateActions for hard constraints that require human judgment (minimum wage compliance, date plausibility, mutually exclusive options). The distinction keeps the user experience smooth: auto-fixable issues are silent, while genuine errors get clear messages.

A well-designed Case has build and validate actions that cover the most common data entry errors for its domain. German tax settings validate that Steuerklasse is between 1 and 6. Dutch employment cases validate that the BSN (citizen service number) has exactly nine digits. Spanish cases validate that NIF/NIE patterns match the official format. These validations are part of the regulation JSON — they ship with the country regulation and work out of the box for every provider using that regulation.

Adding Provider Cases to a Country Regulation

Country regulations define the statutory Cases: tax settings, social security parameters, employment basics. But every provider has data requirements that go beyond the statutory minimum. A staffing agency needs shift differentials. A logistics company tracks delivery zones. A consulting firm records billable vs. non-billable hours. These provider-specific fields do not belong in the country regulation — they belong in a provider overlay.

The composable regulation model allows a provider to add their own Cases in a Level 2 or higher overlay. The provider regulation declares its own namespace and adds Cases that coexist with the country regulation’s Cases. Both appear in the same UI, resolved through the layer hierarchy.

{
  "name": "ACME.Regulation",
  "namespace": "ACME",
  "baseRegulations": ["DE.Entgeltabrechnung"],
  "cases": [
    {
      "name": "ShiftSettings",
      "caseType": "Employee",
      "fields": [
        {
          "name": "ShiftType",
          "valueType": "String",
          "timeType": "Period"
        },
        {
          "name": "ShiftAllowance",
          "valueType": "Money",
          "timeType": "CalendarPeriod",
          "availableActions": [
            "? ^^ShiftType.HasValue"
          ]
        }
      ]
    }
  ]
}

The ACME regulation declares baseRegulations pointing to the German country regulation. Its Cases use the ACME namespace, so ShiftType is stored as ACME.ShiftType — no collision with any DE.* field. The ACME wage types can reference both their own fields (^^ShiftAllowance) and the country fields (^^DE.Salary) using fully qualified names for cross-namespace access.

When an HR administrator opens the employee data entry screen, they see both the German statutory fields and the ACME-specific fields. The WebApp merges them automatically based on the payroll’s layer configuration. No frontend customization, no API configuration, no manual field registration — declaring the Case in the regulation JSON is sufficient.

Namespace discipline: Never prefix object names with the namespace in the regulation JSON. If your regulation declares "namespace": "ACME", your field is "name": "ShiftType", not "name": "ACMEShiftType". The engine prepends the namespace automatically. Manual prefixing creates double-prefixed names like ACME.ACMEShiftType.

Provider Cases follow the same validation and progressive disclosure patterns as country Cases. A provider can add buildActions that default their shift type based on the employee’s department, validateActions that check shift allowance against a maximum, and availableActions that show advanced fields only when the basic shift configuration is complete. The mechanisms are identical — the only difference is the namespace and the layer level.

This composability means a provider can start with the country regulation as-is, then incrementally add Cases for their industry-specific requirements. The country regulation continues to evolve independently — new statutory fields, updated validations, additional data satellites — and the provider overlay inherits those changes automatically through the baseRegulations chain.

Model your first Case

See how Cases and CaseFields define the employee data model in your regulation overlay — or get in touch to discuss your data requirements.

Get in Touch →
← Back
All Articles