The Five Tokens
The Payroll Engine No-Code system is built on five token prefixes. Each token resolves to a runtime value — a case field, a wage type result, a collector balance, a lookup entry, or a direct numeric value. Combined with standard arithmetic operators and conditions, these five tokens cover the vast majority of provider wage type logic without requiring a single line of C#.
| Token | Meaning | Example | Returns |
|---|---|---|---|
^^ |
CaseField value | ^^Salary |
Current period value of the named CaseField |
^$ |
WageType result | ^$5100 |
Result of wage type 5100 (current period) |
^& |
Collector value | ^&GrossIncome |
Current aggregated value of the named collector |
^# |
Lookup value | ^#Rates(2026, 'BonusCap') |
Value from a lookup table by key and field |
^| |
Numeric literal | ^|0.13 |
The literal decimal value 0.13 |
Tokens are combined into action lines — each line is a string in the valueActions array. The engine evaluates these lines sequentially. If any line produces a non-null result, that result becomes the wage type’s value. This sequential evaluation means you can use conditions to guard calculations and fall through to alternatives.
For referencing objects in a foreign namespace (e.g., reading a country case field from a provider regulation), use the fully qualified form: ^^DE.Salary instead of ^^Salary. Within your own namespace, the short form is sufficient. This follows the same namespace isolation rules described in The Four-Layer Regulation Model.
Building Your First Wage Type
Let’s build a concrete provider wage type from scratch: an annual bonus (WT 9300) that reads a percentage from a case field, applies it to the employee’s gross salary, and feeds the result into the country’s gross income collector for downstream tax and social security calculations.
{
"wageTypeNumber": 9300,
"name": "AnnualBonus",
"description": "Annual performance bonus based on percentage of gross salary",
"valueActions": [
"? ^^BonusPct.HasValue",
"^^DE.Salary * ^^BonusPct / ^|100"
],
"collectors": [
"DE.Gesamtbrutto"
]
}
Let’s walk through each element:
Line 1: ? ^^BonusPct.HasValue — The ? prefix marks this as a condition. The wage type only executes if the employee has a BonusPct case field value set for the current period. If the condition is false, the entire wage type is skipped and produces no result.
Line 2: ^^DE.Salary * ^^BonusPct / ^|100 — The actual calculation. It reads the country salary field (DE.Salary, fully qualified because we’re in the ACME namespace), multiplies by the bonus percentage, and divides by 100 to convert the percentage to a factor. Standard operator precedence applies.
collectors array: After the wage type produces its result, the value is automatically added to the DE.Gesamtbrutto collector (fully qualified — it belongs to the DE namespace). This means the bonus automatically participates in all downstream tax and social security calculations without any additional wiring.
collectors array is separate from valueActions — it runs after the value is determined. You never need to manually add a value to a collector in your action lines.
Conditions with ?
Conditions are the primary control flow mechanism in No-Code wage types. Any line prefixed with ? is evaluated as a boolean expression. If it returns false, the wage type stops executing — no further lines are processed and the wage type produces no result (effectively zero).
Here’s a more complex example with multiple conditions and a cap:
{
"wageTypeNumber": 9310,
"name": "CappedBonus",
"description": "Capped annual bonus — max 5000 EUR",
"valueActions": [
"? ^^BonusPct.HasValue",
"? ^^DE.ContractType == 'Permanent'",
"^^DE.Salary * ^^BonusPct / ^|100",
"? ^$9310 > ^|5000",
"^|5000"
]
}
Lines 1–2: Two guard conditions. The wage type only runs for permanent employees who have a bonus percentage set. Both must be true.
Line 3: The base calculation — same as before. At this point the wage type has a preliminary result.
Lines 4–5: The capping pattern. Line 4 checks if the current result (^$9310 — referencing the wage type’s own number) exceeds 5000. If it does, line 5 overrides the result with the cap value. If the condition is false (result is already at or below 5000), the existing result stands.
This sequential evaluation with self-reference is a powerful pattern. The wage type can examine its own intermediate result and adjust it. Note that ^$9310 returns the value computed by the preceding action lines, not the final committed result — this is how mid-calculation adjustments work.
valueActions, conditions after the first calculation line use a different semantic: if the condition is true, the next line replaces the result. If false, execution continues with the existing result. This differs from guard conditions at the top, which abort entirely on false.
Feeding Country Collectors
One of the most powerful aspects of the layer model is how seamlessly provider wage types participate in country-level calculations. When your provider wage type feeds a country collector, the entire downstream chain — tax calculation, social security contributions, net pay derivation — automatically incorporates your value.
{
"wageTypeNumber": 9320,
"name": "CompanyCar",
"description": "Company car benefit — monetary advantage per month",
"valueActions": [
"? ^^CarBenefit.HasValue",
"^^CarBenefit"
],
"collectors": [
"DE.Gesamtbrutto",
"DE.SvBrutto"
],
"collectorGroups": [
"DE.Steuerpflichtig"
]
}
This wage type feeds three different targets: it adds its value directly to DE.Gesamtbrutto (total gross) and DE.SvBrutto (social security gross), and it also participates in the DE.Steuerpflichtig collector group (taxable income group). The collector group mechanism means this value flows to every collector that is a member of that group — without you needing to enumerate them individually.
Why does this matter? Because the country regulation’s Lohnsteuer (income tax) calculation at WT 5100 reads from the taxable income collector. The social security calculations at WT 6000+ read from the SV gross collector. By feeding these collectors, your provider wage type is automatically included in all statutory calculations. You don’t need to modify the country regulation or even know the internal details of how it calculates tax — you just feed the correct collectors.
The collectors and collectorGroups arrays can be combined on the same wage type without risk of double-application. The engine de-duplicates: if a collector appears both directly and via a group, the value is applied once.
Reading Other Wage Types and Collectors
The ^$ token reads the result of any wage type that has already executed in the current period. Since execution follows numeric order, any wage type with a lower number is available. This is the primary mechanism for building derived calculations.
{
"wageTypeNumber": 9330,
"name": "NetBonusRatio",
"description": "Ratio of net pay to gross — for reporting",
"valueActions": [
"? ^$5100 > ^|0",
"^$7000 / ^$5010 * ^|100"
]
}
Here ^$7000 reads the country’s net pay result and ^$5010 reads the taxable gross. Since both are in the 1000–7999 range (country level), they have already executed by the time WT 9330 runs.
The ^& token provides access to collector balances. This is particularly useful for YTD (year-to-date) calculations using the .Cycle suffix:
{
"wageTypeNumber": 9340,
"name": "YtdGross",
"description": "Year-to-date gross income — cumulative",
"valueActions": [
"^&DE.Gesamtbrutto.Cycle"
]
}
The .Cycle suffix on a collector reference returns the aggregated value across all periods in the current cycle (typically the calendar year). Without the suffix, ^&DE.Gesamtbrutto returns only the current period’s aggregated value. This distinction matters for calculations like annual caps, progressive thresholds, or cumulative reporting.
You can also read collector values without the suffix to check the current period’s running total. For example, a wage type that adds a supplement only if the current period gross exceeds a threshold:
"valueActions": [
"? ^&DE.Gesamtbrutto > ^|4000",
"^^HighEarnerSupplement"
]
When Tokens Aren’t Enough
The token system covers a wide range of calculations, but it has boundaries. The most important limitation involves the ^# lookup token: it fails silently when used as an operand in arithmetic expressions.
// WRONG — ^# as operator argument fails silently:
"valueActions": [
"^^DE.Salary * ^#ContributionRates(2026, 'BonusRate')"
]
// CORRECT — use a Custom Action:
"valueActions": [
"CalculateRatedBonus('ACME.Salary', 'ACME.BonusType')"
]
The ^# token works reliably as a standalone expression (the entire action line is just the lookup) or when the lookup returns a value used directly. But the moment you try to multiply, divide, or otherwise combine it with another value using standard operators, the resolution fails without an error message. The wage type simply returns zero or null.
GetLookup<T>() / GetLookupField<T>() API has no such limitation.
Other scenarios that require moving beyond pure tokens:
- Complex conditional logic — nested if/else chains, switch-like patterns with more than two branches
- Date arithmetic — calculating days between dates, pro-rata based on start/end within period
- Iterative calculations — progressive tax brackets, tiered contribution rates across multiple thresholds
- Cross-period reads — accessing values from prior periods (retro scenarios, YTD resets)
- String manipulation — parsing composite field values, building lookup keys dynamically
For these cases, you implement a Custom Action: a C# method decorated with [WageTypeValueAction("ActionName")] that receives the full scripting API. The wage type calls it by name in the valueActions array, just like a token expression. The Custom Action returns an ActionValue and participates in the same sequential evaluation as token lines.
For the complete token reference and more advanced patterns, see No-Code Regulation Development. For how Custom Actions integrate with the layer model and override patterns, see The Composable Regulation Model.
Build your first No-Code wage type
See how token-based wage types work in a live regulation — or get in touch to discuss your provider overlay.
Get in Touch →