KaireonAI includes a safe, sandboxed formula engine for computing personalized values at decision time. The engine follows a strict pipeline:
- Tokenizer — Breaks the formula string into tokens (numbers, strings, identifiers, operators, punctuation)
- Parser — Recursive-descent parser builds an Abstract Syntax Tree (AST) from the token stream
- Evaluator — Walks the AST and computes the result structurally
The engine uses no eval, Function, or vm — all evaluation is structural, making it safe by design. Any formula that fails to parse or evaluate returns null rather than throwing an error.
Formulas are defined on computed custom fields within Categories. At decision time, the Recommend API evaluates each formula per candidate offer and merges the results into the response.
Variable Namespaces
Formulas can reference three namespaces of variables:
| Namespace | Source | Example | When Available |
|---|
fieldName | Other custom field values on the offer | base_rate, offer_price | Always — values come from the offer’s custom fields |
customer.* | Enriched data from schema tables via the Enrich stage | customer.loan_amount, customer.balance | When an Enrich stage is configured in the Decision Flow |
attributes.* | Request-time attributes from the Recommend API body | attributes.tier, attributes.channel | When the caller passes attributes in the Recommend request |
Variables are resolved as a flat key-value map. Dot notation (e.g., customer.loan_amount) is part of the identifier — the engine treats customer.loan_amount as a single variable name, not an object property access.
Operators
Arithmetic Operators
| Operator | Description | Example | Precedence |
|---|
+ | Addition (numbers) or concatenation (strings) | base_rate + 1.5 | 4 |
- | Subtraction | price - discount | 4 |
* | Multiplication | quantity * unit_price | 5 |
/ | Division (returns null on divide by zero) | total / count | 5 |
% | Modulo (returns null on mod by zero) | index % 3 | 5 |
- (unary) | Negation | -score | 6 |
Comparison Operators
All comparison operators return 1 for true and 0 for false. String comparisons support only == and !=.
| Operator | Description | Example | Precedence |
|---|
> | Greater than | customer.age > 18 | 3 |
< | Less than | customer.score < 50 | 3 |
>= | Greater than or equal | customer.age >= 21 | 3 |
<= | Less than or equal | balance <= 0 | 3 |
== | Equal (numbers or strings) | attributes.tier == "gold" | 3 |
!= | Not equal (numbers or strings) | customer.status != "inactive" | 3 |
Precedence Summary (highest to lowest)
| Level | Operators |
|---|
| 6 | Unary - |
| 5 | *, /, % |
| 4 | +, - |
| 3 | >, <, >=, <=, ==, != |
| 1 | ? : (ternary) |
Parentheses () override default precedence.
Functions
The engine includes six built-in functions:
| Function | Signature | Description | Example | Return Type |
|---|
min | min(a, b) | Returns the smaller of two numbers | min(customer.rate, 25.0) | number |
max | max(a, b) | Returns the larger of two numbers | max(customer.score, 0) | number |
round | round(value) or round(value, decimals) | Rounds to nearest integer, or to N decimal places | round(total * 0.0825, 2) | number |
abs | abs(value) | Returns the absolute value | abs(customer.balance) | number |
coalesce | coalesce(a, b, ...) | Returns the first non-null argument (minimum 2 args) | coalesce(customer.rate, base_rate, 5.0) | number or string |
concat | concat(a, b, ...) | Joins all arguments as strings (minimum 2 args) | concat("Hello ", customer.name) | string |
Unknown function names return null. Functions with the wrong number of arguments also return null.
Ternary Expressions
Ternary expressions provide conditional logic:
condition ? valueIfTrue : valueIfFalse
The condition is evaluated first. A condition is truthy if it is a non-zero number or a non-empty string. A condition of 0, "", or null is falsy.
customer.age >= 21 ? "eligible" : "ineligible"
Ternary expressions can be nested using parentheses:
customer.qty > 100 ? 0.50 : (customer.qty > 50 ? 0.75 : 1.00)
If the condition evaluates to null, the entire ternary returns null.
Type Coercion
Computed fields declare an outputType of either number or text. This is a metadata hint used for validation and display — the formula engine itself returns whatever type the expression produces:
- Arithmetic operations return numbers
- String operations (
+ on two strings, concat) return strings
- Comparison operators always return
1 or 0 (numbers)
coalesce returns the type of the first non-null argument
When defining a computed field, choose the outputType that matches your formula’s expected result. The API validation (CreateCategorySchema) enforces that every computed field specifies both a formula and a valid outputType.
Error Handling
The formula engine is designed to fail gracefully. Every error condition returns null rather than throwing:
| Condition | Behavior |
|---|
Division by zero (10 / 0) | Returns null |
Modulo by zero (10 % 0) | Returns null |
| Missing or undefined variable | Returns null (use coalesce to provide a default) |
null variable value | Returns null (null propagates through all operations) |
| Syntax error or unparseable formula | Returns null |
| Mismatched parentheses | Returns null |
| Empty formula | Returns null |
| Unknown function name | Returns null |
| Wrong argument count for a function | Returns null |
| Type mismatch (e.g., arithmetic on strings) | Returns null |
| Unterminated string literal | Returns null |
Null propagation rule: If any operand in a binary operation is null, the entire operation returns null. This means 5 + null is null, not 5. Use coalesce to guard against missing values.
Simple arithmetic
Applies a 10% markup to the offer’s base_rate custom field.
Customer data lookup
Computes 2% of the customer’s balance (loaded via Enrich stage).
Request-time conditional
attributes.tier == "gold" ? 500 : 200
Returns a different reward amount based on the tier passed in the Recommend request.
Fallback with coalesce
coalesce(customer.preferred_rate, base_rate, 5.0)
Uses the customer’s preferred rate if available, falls back to the offer’s base rate, then to a hardcoded default of 5.0.
String personalization
concat("Hello ", customer.first_name)
Builds a personalized greeting from enriched customer data.
Rounded calculation
round(customer.loan_amount * base_rate / 100, 2)
Computes a monthly interest amount rounded to 2 decimal places.
Conditional discount
base_price * (1 - (customer.loyalty_years > 5 ? 0.15 : 0.05))
Applies a 15% discount for customers with more than 5 loyalty years, otherwise 5%.
Clamping with min/max
max(min(calculated_rate, 25.0), 2.5)
Ensures a rate stays within the 2.5 to 25.0 range.
Multi-variable calculation
(customer.income - customer.expenses) * risk_factor
Computes disposable income multiplied by an offer-level risk factor.
Channel-aware text
attributes.channel == "email" ? concat(customer.name, ", check out ", offer_name) : offer_name
Returns a personalized message for email, or just the offer name for other channels.
Tiered pricing with nested ternary
customer.qty > 100 ? 0.50 : (customer.qty > 50 ? 0.75 : 1.00)
Three-tier pricing: high volume at 0.50, medium at 0.75, standard at 1.00.
Scoring with null safety
coalesce(customer.score, 0) * offer.weight + coalesce(customer.bonus, 0)
Builds a weighted score with safe defaults for missing values.
Validation
UI Validation
The Business Hierarchy page (Category editor) includes a Validate button for computed fields. Clicking it runs validateFormula() which tokenizes and parses the formula, returning either { valid: true } or { valid: false, error: "..." } with a descriptive error message.
API Validation
The CreateCategorySchema and UpdateCategorySchema Zod schemas include .superRefine() logic that validates computed fields on every API call:
- Every field with
type: "computed" must have a non-empty formula string
- Every computed field must have an
outputType of either "number" or "text"
Missing either property produces a Zod validation error and a 400 response.