DMN Hit Policies: FIRST, COLLECT, and When to Use Which

When a set of inputs hits a decision table with multiple matching rules, hit policy determines which rules fire and how their outputs combine.

Choosing the wrong hit policy is the most common DMN mistake. Here's how to pick the right one.

Quick Reference

Hit PolicyMatchesOutputUse When
FIRSTFirst matchSingle row outputPriority-ordered rules
UNIQUEExactly oneSingle row output (or error)Non-overlapping rules
ANYAny matchSingle row (all must agree)Overlapping but consistent rules
COLLECTAll matchesList of outputsScoring, aggregation
RULE ORDERAll matchesList (in table order)Ordered multi-output

FIRST: Priority Ordering

Use when: Rules overlap and order matters. The first matching rule wins.

Example — risk classification:

hit_policy: FIRST
rules:
  # Rule 1: Specific override (highest priority)
  - when: { country: "sanctioned_list", amount: "> 0" }
    then: { risk: "blocked", action: "reject" }

  # Rule 2: High-risk pattern
  - when: { amount: "> 100000", new_customer: true }
    then: { risk: "high", action: "escalate" }

  # Rule 3: Medium risk
  - when: { amount: "> 50000" }
    then: { risk: "medium", action: "review" }

  # Rule 4: Default (lowest priority)
  - when: {}
    then: { risk: "low", action: "approve" }

Input: { country: "US", amount: 75000, new_customer: false }

  • Rule 1: No match (not sanctioned)
  • Rule 2: No match (not new customer)
  • Rule 3: Match{ risk: "medium", action: "review" }
  • Rule 4: Skipped (FIRST already matched)

FIRST is the most common hit policy. When in doubt, start with FIRST.

UNIQUE: No Overlap Allowed

Use when: Every possible input should match exactly one rule. If two rules match, it's a bug.

Example — tax bracket:

hit_policy: UNIQUE
rules:
  - when: { income: "< 10000" }
    then: { rate: 0, bracket: "exempt" }
  - when: { income: "[10000..40000]" }
    then: { rate: 0.12, bracket: "low" }
  - when: { income: "[40001..85000]" }
    then: { rate: 0.22, bracket: "medium" }
  - when: { income: "[85001..165000]" }
    then: { rate: 0.24, bracket: "upper" }
  - when: { income: "> 165000" }
    then: { rate: 0.32, bracket: "high" }

If the ranges accidentally overlap (e.g., both ≤ 40000 and ≥ 40000 match 40000), the engine returns an error. This catches rule conflicts at evaluation time.

COLLECT: Aggregate All Matches

Use when: You need to combine results from all matching rules. Common for scoring systems.

Example — risk scoring with weighted factors:

hit_policy: COLLECT
aggregation: SUM
output_column: score
rules:
  - when: { credit_score: "< 600" }
    then: { score: 30, factor: "Low credit" }
  - when: { debt_ratio: "> 0.4" }
    then: { score: 25, factor: "High debt ratio" }
  - when: { employment_years: "< 2" }
    then: { score: 15, factor: "Short employment" }
  - when: { bankruptcy_history: true }
    then: { score: 40, factor: "Bankruptcy record" }
  - when: { loan_amount: "> 500000" }
    then: { score: 10, factor: "Large loan" }

Input: { credit_score: 580, debt_ratio: 0.45, employment_years: 5, bankruptcy_history: false, loan_amount: 200000 }

  • Rule 1: Match → 30 points
  • Rule 2: Match → 25 points
  • Rule 3: No match
  • Rule 4: No match
  • Rule 5: No match

Result: { score: 55, matched_factors: ["Low credit", "High debt ratio"] }

COLLECT supports aggregations: SUM, MIN, MAX, COUNT.

Real-World Example: Multi-Table Decision

In practice, you often chain multiple tables:

┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│  risk_scoring    │     │  eligibility     │     │  pricing         │
│  COLLECT (SUM)   │────▶│  FIRST           │────▶│  UNIQUE          │
│  → risk_score    │     │  → eligible,     │     │  → rate,         │
│                  │     │    reason         │     │    term          │
└──────────────────┘     └──────────────────┘     └──────────────────┘

Table 1 calculates a risk score (COLLECT, summing all matching factors). Table 2 uses that score to determine eligibility (FIRST, priority-ordered). Table 3 maps the eligibility tier to pricing (UNIQUE, non-overlapping tiers).

Common Mistakes

1. Using FIRST when you need UNIQUE

If your rules shouldn't overlap, use UNIQUE. FIRST silently picks the first match — you won't notice that two rules match the same input.

2. Using COLLECT when order matters

COLLECT returns results in match order, which may not be table order. If you need results in the order rules appear in the table, use RULE ORDER.

3. Missing the default rule

With FIRST, if no rule matches, the output is empty. Always add a catch-all default as the last rule:

# Last rule — catches everything
- when: {}
  then: { decision: "manual_review", reason: "No rule matched" }

4. Overlapping ranges in UNIQUE

# Bug: 50000 matches both rules
- when: { amount: "<= 50000" }
  then: { tier: "standard" }
- when: { amount: ">= 50000" }
  then: { tier: "premium" }

Fix: use exclusive boundaries (< 50000 and >= 50000).

Choosing the Right Policy

Ask yourself:

  1. Should multiple rules match? → No: FIRST or UNIQUE. Yes: COLLECT.
  2. Do rules have priority? → Yes: FIRST. No: UNIQUE.
  3. Is overlap a bug or a feature? → Bug: UNIQUE. Feature: COLLECT.
  4. Do you need aggregation? → Yes: COLLECT with SUM/MIN/MAX/COUNT.

Start with FIRST. Move to UNIQUE when you're confident rules don't overlap. Use COLLECT for scoring and multi-factor evaluation.