Browse documentation
Docs/Language

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.