Everything you need to write deterministic business rules. No prior DSL experience required.
RuleDSL is a purpose-built language for writing business decision rules. It compiles to bytecode that evaluates deterministically: same input, same bytecode, same decision — every time, on every platform.
A rule file contains one or more named rules. Each rule has a condition (when) and an action (then). Rules are evaluated in priority order. The first rule whose condition matches produces the decision.
Every rule follows this pattern:
rule rule_name priority 100 { when <condition>; then <actions>; }
| Part | Required | Description |
|---|---|---|
rule | Yes | Keyword. Starts a rule block. |
rule_name | Yes | Identifier. Appears in decision results. |
priority N | No | Higher number = evaluated first. Default is 0. |
when | Yes | Condition expression. Must resolve to true/false. |
then | Yes | One or more actions, separated by commas. |
Rules are evaluated by priority (highest first). Among rules with equal priority, source order applies. The first match wins — no further rules are evaluated after a decision is reached.
| Operator | Meaning | Example |
|---|---|---|
== | Equal | currency == "USD" |
!= | Not equal | status != "blocked" |
< | Less than | amount < 100 |
<= | Less than or equal | amount <= 500 |
> | Greater than | amount > 1000 |
>= | Greater than or equal | risk_score >= 80 |
| Operator | Meaning | Example |
|---|---|---|
and | Both must be true | amount > 1000 and currency == "USD" |
or | Either can be true | country == "US" or country == "GB" |
not | Negation | not is_verified |
Use parentheses to control evaluation order: (a or b) and c
| Operator | Meaning | Example |
|---|---|---|
+ | Addition | amount + fee |
- | Subtraction | balance - amount |
* | Multiplication | rate * 100 |
/ | Division | amount / 100 |
Standard precedence: * and / bind tighter than + and -. Use parentheses when in doubt.
| Level | Operators | Associativity |
|---|---|---|
| 1 | not, unary - | Right |
| 2 | * / | Left |
| 3 | + - | Left |
| 4 | < <= > >= | Left |
| 5 | == != | Left |
| 6 | and | Left |
| 7 | or | Left |
Approve the request. Transaction proceeds.
Reject the request. Transaction is blocked.
Flag for manual review. Requires human decision.
Apply a velocity cap. E.g., max 200 USD per day.
# Simple decisions then allow; then decline; then review; # Velocity limit (requires now_utc_ms in input) then limit 200 USD per 1 D;
| Type | Literal Example | Description |
|---|---|---|
| Number | 1200.0, 1e6, 100 USD | IEEE 754 binary64. Integer, decimal, and exponent forms. Optional currency tag. |
| String | "USD", "hello" | Double-quoted. Used in comparisons and MATCH. |
| Boolean | true, false | Used in conditions directly. |
| Identifier | amount, customer.level | References input fields. Dotted paths supported. |
| Missing | — | Field not provided. Operations on missing safely fail. |
Numbers support integer, decimal, and scientific notation:
when amount > 10000; # integer when rate > 0.05; # decimal when amount > 1e6; # scientific notation (1,000,000) when rate > 2.5e-3; # decimal + exponent (0.0025)
Append a 3-letter currency code to a numeric literal for currency-aware comparisons:
when amount >= 1e4 USD; # 10,000 USD when amount >= 8000 EUR; # 8,000 EUR
Access nested fields with dot notation:
when customer.risk_level > 70 and merchant.category == "gambling";
The then clause can set output fields alongside the decision. These appear in the result and can be used by downstream systems.
rule fraud_signal priority 100 { when amount > 3000 and ip_country in [NG, RU]; then risk_score = 95, reason = "multi_signal_fraud", category = "high_risk", decline; }
Assignments support arithmetic expressions:
then risk_score = (amount / 100) + velocity_1h, review;
Check if a value is a member of a list:
when ip_country in [TR, NG, RU, KP]; when channel in [ECOM, APP, WEB];
List items can be identifiers (bare words) or strings.
Check if a text field contains a substring:
when email match "@example.com"; when card_bin match "41111";
MATCH_EXPECTS_TEXT). Always ensure the matched field is a string.
The limit decision enforces velocity caps over time windows:
then limit 500 USD per 1 D; # max 500 USD per day then limit 3 TRY per 1 H; # max 3 transactions per hour
| Code | Unit |
|---|---|
S | Second |
M | Minute |
H | Hour |
D | Day |
now_utc_ms in the input context. The Python and C# bindings auto-inject this; the C API requires explicit injection.
If a referenced field is not provided in the input, it resolves to missing.
missing is a distinct value — not null, not zero, not empty string.missing produce a runtime diagnostic and the condition evaluates to false.This means you do not need defensive null checks. If a field might not exist, the rule safely skips:
# If customer.loyalty_tier is not in the input, this rule is skipped rule vip_bypass { when customer.loyalty_tier == "platinum"; then allow; }
RuleDSL is deterministic by contract. These are not aspirational goals — they are enforced by the engine architecture:
The engine never reads the system clock. Time is explicitly injected via now_utc_ms.
Rules are evaluated by priority, then source order. No randomness, no hash-map iteration artifacts.
All arithmetic uses binary64. No fast-math. No platform-dependent rounding. NaN and Inf are rejected.
Identifiers use raw UTF-8 octet matching. No Unicode normalization. No collation surprises.
The result: same bytecode + same input + same engine version = same decision. Always. On every platform.
rule block_sanctioned priority 200 { when ip_country in [KP, IR, SY]; then reason = "sanctioned", decline; } rule high_risk_geo priority 100 { when amount > 5000 and ip_country in [NG, RU]; then risk_score = 90, reason = "geo_high", decline; } rule review_medium priority 50 { when amount > 1000 and amount <= 5000; then risk_score = 60, reason = "medium_amount", review; } rule allow_default { when true; then risk_score = 10, reason = "baseline", allow; }
rule block_new_device_high priority 100 { when is_new_device and amount > 2000; then reason = "new_device_risk", decline; } rule limit_daily priority 50 { when amount > 1000; then reason = "daily_cap", limit 500 USD per 1 D; } rule trusted_device priority 10 { when card_present and not is_new_device; then reason = "trusted", allow; } rule default_review { when true; then review; }
rule flag_internal_email priority 100 { when email match "@internal\\.corp$"; then reason = "internal_email", review; } rule block_risky_channel priority 50 { when channel in [ECOM, APP] and amount > 1000; then reason = "risky_channel", decline; } rule allow_other { when true; then allow; }
| Keyword | Context |
|---|---|
rule | Starts a rule block |
priority | Sets evaluation order (optional) |
when | Begins condition expression |
then | Begins action clause |
and or not | Logical operators |
in | List membership check |
match | Pattern match on text fields |
allow decline review | Decision outcomes |
limit per | Velocity limit decision |
true false | Boolean literals |
Keywords are case-insensitive: WHEN, When, and when are equivalent.
| Field | Type | Required For |
|---|---|---|
now_utc_ms | Number (epoch milliseconds) | limit ... per rules |