Language implementation v0.9, documented against the v1.0 target specification. No prior DSL experience required.
RuleDSL is a purpose-built language for writing business decision rules. For the published DET corpus, v1.0.2 produced byte-identical decisions on Windows x86_64 and Linux x86_64 when the engine/ABI, bytecode, canonical input, and evaluation options matched. This does not formally prove every possible rule and input.
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. Shipped v0.9 applies dynamic truthiness. |
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 |
==, <, <=, >, and >= return false; != returns true. Validate types in the host or an input schema before evaluation. See the v0.9 conformance status.
| 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
and and or evaluate both operands. divisor != 0 and amount / divisor > 1 is therefore not a safe division guard. Validate the divisor in the host or use separate rules. See the v0.9 conformance status.
| 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.
Return a velocity-cap decision for the host to enforce. 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 can emit runtime diagnostics and AX_ERR_RUNTIME; see Missing Values. |
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)
1e-9 × max(1, |a|, |b|). Do not treat a threshold as exact IEEE equality at that boundary. Represent financial amounts in integer minor units where practical and add boundary acceptance tests. See the v0.9 conformance status.
A 3-letter currency tag on a numeric literal is metadata in shipped v0.9. The engine compares the numeric value and does not automatically filter by currency, so add an explicit host-supplied currency condition:
when amount >= 10000 USD and currency == "USD"; when amount >= 8000 EUR and currency == "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). Ensure the matched field is a string.
The limit decision expresses a velocity cap (an amount and a per window). The engine is stateless: the host computes counters, aggregates, and time-window fields, supplies any values the rule evaluates, and enforces the returned cap. now_utc_ms is validated as present but is not used computationally in v0.9:
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 as an explicit field. The shipped Python and C# wrappers accept either their explicit clock argument or the documented explicit field, but not both. Omitting both for a time-based rule raises MISSING_NOW_UTC_MS.
If a referenced field is not provided in the input, it resolves to missing. This is not a normal "safe skip" contract:
missing is a distinct value — not null, not zero, not empty string.missing can evaluate to false while emitting a runtime diagnostic; arithmetic can emit TYPE_MISMATCH and produce another missing value.UNKNOWN_PATH, TYPE_MISMATCH, and ASSIGN_VALUE_MISSING map to AX_ERR_RUNTIME=11. The decision struct can still be populated and evaluation can continue.code != AX_ERR_OK and handle the call fail-closed, regardless of a populated decision.Validate required fields before evaluation; do not rely on missing-value behavior as control flow. See the v0.9 conformance status.
# Host must require customer.loyalty_tier before evaluating this rule rule vip_bypass { when customer.loyalty_tier == "platinum"; then allow; }
The v0.9 language implementation controls implicit time, evaluation order, numeric mode, and locale inputs. For the published DET corpus, the shipped v1.0.2 Windows and Linux x86_64 binaries produced matching decisions under the same engine/ABI, bytecode, canonical input, and equivalent-option conditions. This is scenario evidence, not a formal proof over all rules and inputs:
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.
Arithmetic uses binary64 and comparisons use v0.9's documented relative epsilon. Fast-math is rejected at build time; NaN and Inf follow the documented rejection paths.
Identifiers use raw UTF-8 octet matching. No Unicode normalization. No collation surprises.
Verified scope: same shipped v1.0.2 engine/ABI + same bytecode + canonical input + equivalent options produced byte-identical decisions on Windows x86_64 and Linux x86_64.
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; }
The host supplies transaction, device, aggregate, and time-window values; these rules evaluate those inputs but do not maintain a counter, window, or database.
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 { # Host supplies a normalized domain for exact matching when email_domain == "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 |