Decision tables
DECISION defines a pure callable with named rules and an explicit hit policy. It can return a scalar, record, collection, Option, Result or CHOICE value. Declaring a table performs no…
Decision tables
DECISION defines a pure callable with named rules and an explicit hit policy.
It can return a scalar, record, collection, Option, Result or CHOICE value.
Declaring a table performs no evaluation and creates no mutable instance.
shipping = DECISION(weight: number, express: boolean) HIT UNIQUE
RULE express_rate WHEN express THEN {price: 25, service: :express}
RULE small WHEN NOT express AND weight <= 5 THEN {price: 5, service: :standard}
RULE large WHEN NOT express AND weight > 5 THEN {price: 10, service: :standard}
END
quote = shipping(3, FALSE)
explanation = shipping.REPORT(3, FALSE)
DECISION shipping(weight, express) HIT UNIQUE … END is the equivalent
declaration form. Both forms work at module scope and inside DO, including
a named function's body. Type annotations are optional; normal callable
inference checks parameters and unifies rule outputs and the default.
Selecting rules
| Policy | Evaluation and result |
|---|---|
FIRST |
Test rules in order; stop at the first true predicate and evaluate that output. |
UNIQUE |
Test every predicate. Exactly one match selects its output; multiple matches produce DECISION_CONFLICT before any output runs. |
ANY |
Test every predicate, then evaluate matched outputs in order. Return the original first output when all outputs agree. |
COLLECT |
Test every predicate, then return matched outputs in authored order as a native VECTOR. Nested collections remain nested. No matches produce an empty vector. |
Predicates must be booleans, including in loose coercion mode. A predicate
error stops matching before any output is evaluated. A selected output error
propagates its original Grid code and message. ERR(data) is an ordinary Result
value, not a Grid evaluation error.
For FIRST, UNIQUE and ANY, an optional final ELSE expression supplies the
no-match result. Without it, no match produces DECISION_NO_MATCH. COLLECT
does not accept ELSE.
rate = DECISION(weight) HIT FIRST
RULE light WHEN weight <= 5 THEN 5
RULE medium WHEN weight <= 20 THEN 10
ELSE 25
END
Composing policies
Pass a decision to ordinary higher-order functions, partially apply its arguments, or invoke it from another pure function. Immutable lexical captures are allowed. Pass cell values, inputs and state explicitly instead of capturing them. Pure helpers are checked transitively; recursive or effectful helpers are refused.
doubled = DECISION(value) HIT FIRST
RULE positive WHEN value > 0 THEN value * 2
ELSE 0
END
results = MAP(VECTOR(1, 2, 3), doubled)
A decision can also accept an inferred, statically checked callable parameter. The callback must be pure, deterministic and terminating:
qualify = DECISION(test, amount) HIT FIRST
RULE accepted WHEN test(amount) THEN amount
ELSE 0
END
qualified = qualify((amount) => amount > 0, 3)
Parameters can carry records, native collections and declared CHOICE values.
For example, items: List<number> accepts the native vector carrier; nested
collection annotations retain their element contracts. Explicit annotations are
checked after forward CHOICE declarations and imports have been resolved, even
when the decision is never called. Unknown families, unknown nested arguments
and unsupported arities are errors. Tuple<T> describes homogeneous elements;
Tuple<T, U> is not a supported parameter contract. Imported nominal contracts
retain their declaring schema identity. A boolean decision can
be called directly in a machine's WHEN guard. Its returned data does not
perform a transition or send an event.
An imported decision carries its statically bound .REPORT companion.
USE "policies.grid" AS policies exposes policies.shipping and
policies.shipping.REPORT. EXPOSING (shipping AS delivery) exposes
delivery and delivery.REPORT. Immutable captures retain their declaring
module's meaning, including when an imported helper calls a decision or its
report companion. Pass the report callable separately when a helper takes it
as an argument.
ANY agreement
Agreement is noncoercing. Numbers compare exactly within their native numeric type; signed zero agrees. Strings are case-sensitive. Tagged scalars require the same semantic tag and payload, without unit conversion. Records compare field names and values regardless of insertion order; duplicate field names are outside the agreement profile. Arrays and native vectors, tuples and deques compare logical shape, kind and ordered elements. Options and Results retain their constructors. CHOICE values follow the existing nominal family/constructor identity and admitted type-argument rules.
Nonfinite numbers, opaque handles, executable values, Tensor/Frame values and
other collection kinds produce DECISION_UNCOMPARABLE, including when nested.
Project such values to comparable data before using ANY. Other policies do
not inherit this restriction.
ANY continues evaluating selected outputs after discovering disagreement.
A later evaluation or comparison-admission error takes precedence over
DECISION_CONFLICT. It retains the first value and current comparison value,
not a vector of every output.
Explaining one evaluation
shipping.REPORT(3, FALSE) returns a native record containing:
| Field | Meaning |
|---|---|
schema |
"grid.decision.report.v1" |
definition |
Definition name and callable semantic identity |
policy |
:first, :unique, :any or :collect |
status |
:matched, :default, :empty, :no_match, :conflict or :evaluation_error |
outcome |
OK(value) or ERR(detail) |
evaluated_rules |
Attempted predicate labels in order, including a failing predicate |
matched_rules |
Labels whose predicates returned true |
evaluated_outputs |
Attempted output labels, including a failing output |
matching_complete |
Whether the policy finished its required matching work |
default_evaluated |
Whether ELSE was attempted |
Error detail has code, message, phase, optional rule, and optional
cause. In schema v1, cause is always NONE: the current Grid error owner
supplies a code and message, not a structured causal chain. It is not a second
copy of the message or evidence that no deeper cause exists. Phases distinguish
input, predicate, output, default, agreement and budget failures. Pattern rows
also distinguish subject and guard failures. Reports identify the rule label;
they do not expose private payload bindings. Successful blank values and empty
collections remain successful outcomes.
The report comes from that invocation's actual selection and evaluation. Calling the value and report APIs separately makes two calls. Successful value calls do not construct or discard a report.
Admission diagnostics
The compiler warns about provable uncovered input cases, overlapping UNIQUE
rules, and unreachable FIRST rows. It understands finite boolean combinations,
closed nominal CHOICE constructor tests through MATCH, and simple numeric
comparisons against literal boundaries. For example, x <= 5 and x >= 5
overlap at 5; x < 5 and x > 5 leave that boundary uncovered.
These are conservative diagnostics. Arbitrary helpers, payload-dependent tests,
unannotated domains, and numeric representations outside finite scalar witnesses
remain unknown. Absence of a warning is not proof of complete or disjoint
coverage. COLLECT may legitimately return an empty vector, and ELSE handles
an otherwise uncovered case. Runtime matching, conflict and agreement checks
always remain active.
Analysis runs only when a model admits a decision. It examines at most 4,096 combined input cases per table, one million predicate/scan steps per selected model pass, and depth 64. An exhausted analysis leaves the table unknown and never rejects or truncates its execution. At most 16 overlap and 16 unreachable notices are emitted per table.
Row-scoped MATCH bindings
A rule can match a subject and bind its payload for both its guard and output:
CHOICE Payment = Paid(amount: number) | Cancelled
rebate = DECISION(event: Payment) HIT FIRST
RULE large MATCH event WITH Payment.Paid(amount)
WHEN amount >= 100 THEN amount * 0.02
RULE ordinary MATCH event WITH Payment.Paid(amount) THEN amount * 0.01
ELSE 0
END
amount = rebate(Payment.Paid(200))
explanation = rebate.REPORT(Payment.Paid(200))
MATCH subject WITH pattern reuses ordinary MATCH patterns: declared CHOICE
constructors, SOME(value), NONE(), OK(value), ERR(reason), error-kind, literal or
reference patterns, and _. An optional WHEN guard must return a boolean.
Pattern rows and ordinary WHEN rows may appear in the same table under any
hit policy.
Each tested row evaluates its subject once. A mismatch skips its guard and
output. The guard and selected output use the same retained subject; the
output does not evaluate the subject again. FIRST stops after its first
match; the other policies finish matching before any selected output runs.
Payload names are immutable and local to that row's guard and output. They
may shadow outer names, including parameters; the subject still reads the
outer scope. They never extend into another row or ELSE. Duplicate payload
names within a pattern are errors. _ ignores the whole subject or an
individual constructor field.
Ordinary MATCH expressions still compose with decisions. A binding inside
WHEN MATCH(...) ends with that MATCH arm and does not extend into THEN.
Use a row pattern when its payload belongs to both the guard and output, or
keep MATCH in an enclosing function when several policies share its payload.
Constructor fields use the existing name-or-underscore binding profile. Nested
record or constructor destructuring remains separate MATCH work.
Bounds and compatibility
A table has 1–1,024 rules and at most 64 parameters. Labels are unique without
regard to case and contain at most 128 bytes. ANY data admission/comparison has
a shared one-million-unit work allowance and a maximum nesting depth of 64.
Pattern subjects have an 8 MiB accounted-data allowance across retained matches,
with a shared one-million-unit inspection allowance and nesting depth 64. The
accounting conservatively counts referenced data per row; it is not a process
RSS limit. Oversized or unsupported subject data returns DECISION_BINDING_LIMIT.
Reports have a conservative 1 MiB retained-data allowance and bounded traversal.
Exceeding report construction limits returns an ordinary Grid error, never a
truncated success. Existing compiler and callable budgets also apply.
Only a plain named = binding or declaration is accepted; coordinate/range
targets and assignment decorators are not decision definitions. Malformed
rules after a complete HIT header produce a located diagnostic.
This notation is separate from existing DECISION PACKAGE metadata. It uses
ordinary source callables and the shared compiled evaluator. The owning artifact
retains parameters, rule labels/order, subjects, patterns, guards, outputs and
default for source-free admission, canonical rendering and checked editing.
See the canonical example, row-pattern example, and named functions.
Comparing policies in studies
A decision value call or its statically bound .REPORT companion can be an
EVALUATE target in EXPERIMENT. All hit policies and row-pattern rules retain
their existing semantics and limits. The study's complete transitive admission
still rejects effects, mutable outer captures and unsealed calls; admitting a
decision operation does not exempt its predicates or outputs from that scan.
Use explicit scalar observations to retain prices, services, rule labels or
projected report errors. Direct value errors fail a case; .REPORT lets the
author deliberately compare a conflict/no-match result as data. Reading the
definition creates no job. See policy comparisons
and the canonical model.
Importing external decision tables
The explicit DMN 1.5 importer maps a bounded scalar table and closed FEEL unary tests into these existing callables. It retains source-row identity, admits FIRST/UNIQUE/ANY, checks non-null exact-scalar inputs and exposes missing results as Options. Full FEEL and BPMN are separate profiles.