v2.0.0

<bmx-decision-table>

Business rules as a DMN decision table: edit the rules in a grid, try inputs and see which rule fired and why, check for gaps and overlaps, keep test cases beside the rules, and move tables in and out as DMN 1.3 XML.

5 properties · 2 events · 11 methods · 5 parts

Example

Pick values under “Try it” to see which rule fires and why. “Check” finds the case no rule covers.

The table as DMN 1.3 XML

Show markup
<div class="row" style="display: block">
  <bmx-decision-table id="ex-dt" style="max-inline-size: 72rem"></bmx-decision-table>
</div>
<p class="note" id="ex-dt-out" role="status">Pick values under “Try it” to see which rule fires and why. “Check” finds the case no rule covers.</p>
<details style="margin-block-start: 12px">
  <summary>The table as DMN 1.3 XML</summary>
  <pre id="ex-dt-dmn" style="max-block-size: 20rem; overflow: auto; font-size: 12px"></pre>
</details>

<script type="module">
  await customElements.whenDefined('bmx-decision-table');
  const table = document.getElementById('ex-dt');
  const out = document.getElementById('ex-dt-out');
  const dmn = document.getElementById('ex-dt-dmn');
  table.table = {
    name: 'Loan pre-approval',
    hitPolicy: 'unique',
    inputs: [
      { key: 'score', label: 'Credit score', type: 'integer', description: 'From the credit bureau, 300 to 850.' },
      { key: 'employment', label: 'Employment', values: ['Employed', 'Self-employed', 'Retired', 'Unemployed'] },
      { key: 'ratio', label: 'Loan to income', type: 'number', expression: '=amount / income', description: 'The amount asked for over the yearly income.' },
    ],
    outputs: [
      { key: 'decision', label: 'Decision', values: ['Decline', 'Refer', 'Approve'], default: 'Refer' },
      { key: 'rate', label: 'Rate', type: 'number' },
    ],
    rules: [
      { inputs: ['< 580', '-', '-'], outputs: ['Decline', ''], note: 'Below the lending floor' },
      { inputs: ['[580..670)', '"Employed", "Self-employed"', '<= 3'], outputs: ['Refer', '0.089'] },
      { inputs: ['[580..670)', '"Employed", "Self-employed"', '> 3'], outputs: ['Decline', ''] },
      { inputs: ['>= 670', '"Employed"', '<= 4'], outputs: ['Approve', '=IF(score >= 760, 0.049, 0.059)'], note: 'Best rate for 760 and up' },
      { inputs: ['>= 670', '"Self-employed"', '<= 3'], outputs: ['Approve', '0.065'] },
      { inputs: ['>= 670', '"Employed", "Self-employed"', '> 4'], outputs: ['Refer', '0.072'] },
      { inputs: ['>= 670', '"Self-employed"', '(3..4]'], outputs: ['Refer', '0.069'] },
      { inputs: ['>= 580', '"Retired"', '<= 2'], outputs: ['Approve', '0.061'] },
      { inputs: ['>= 580', '"Unemployed"', '-'], outputs: ['Decline', ''] },
    ],
    tests: [
      { name: 'Prime borrower', inputs: { score: 790, employment: 'Employed', amount: 90000, income: 60000 }, expect: { decision: 'Approve', rate: 0.049 } },
      { name: 'Thin margin', inputs: { score: 600, employment: 'Self-employed', amount: 200000, income: 50000 }, expect: { decision: 'Decline' } },
      { name: 'Retired, small loan', inputs: { score: 700, employment: 'Retired', amount: 30000, income: 25000 }, expect: { decision: 'Approve', rate: 0.061 } },
    ],
  };
  await table.tryValuesOf({ score: 720, employment: 'Employed', amount: 120000, income: 45000 });
  const showDmn = async () => (dmn.textContent = await table.exportDmn());
  showDmn();
  table.addEventListener('bmxDecisionChange', showDmn);
  table.addEventListener('bmxDecisionEvaluate', e => {
    const { result } = e.detail;
    out.textContent = result.ok ? `Answer: ${JSON.stringify(result.output)} (rule ${result.fired.map(r => r + 1).join(', ') || 'none'})` : result.error;
  });
</script>
<bmx-decision-table id="rules"></bmx-decision-table>
<script>
  rules.table = {
    name: 'Discount',
    hitPolicy: 'unique',
    inputs: [
      { key: 'tier', label: 'Customer tier', values: ['Gold', 'Silver', 'Bronze'] },
      { key: 'amount', label: 'Order amount', type: 'number' },
    ],
    outputs: [{ key: 'discount', label: 'Discount', type: 'number', default: 0 }],
    rules: [
      { inputs: ['"Gold"', '>= 1000'], outputs: ['0.15'] },
      { inputs: ['"Gold"', '< 1000'], outputs: ['0.1'] },
    ],
  };
  const { output } = await rules.evaluate({ tier: 'Gold', amount: 1200 });
</script>

Input cells are tests in the syntax of DMN's simple unary tests - < 18, [18..65), "UK", "IE", not("UK"), - - or BMX formulas (=...) with the inputs as names. Output cells are values or formulas. All seven DMN hit policies, with collect's sum, min, max and count. Nothing runs eval.

The element also works as an evaluator out of sight: hidden, with evaluate and evaluateMany. With the source, the same evaluator runs without it (evaluateDecision in core/decision-model), on a server or in a worker.

Properties

PropertyAttributeTypeDefaultDescription
label label string — The table's accessible name. Default: its name, or "Decision table".
panel panel BmxDecisionTablePanel 'try' The panel shown under the grid, or none to hide the panels.
readonly readonly boolean false Shows the rules without letting them be changed. Try it, Check and Tests still work.
strings strings Partial<BmxDecisionTableStrings> | string — Words to show instead of the English ones, as an object or JSON.
table table BmxDecisionTableData | string { inputs: [], outputs: [], rules: [] } The decision table, as an object or JSON.

Events

EventDetailDescription
bmxDecisionChange BmxDecisionChangeDetail Fired after every change, with the whole table.
bmxDecisionEvaluate BmxDecisionEvaluateDetail Fired when Try it works the table out.

Methods

MethodSignatureDescription
check check() => Promise<{ problems: BmxDecisionProblem[]; analysis: BmxDecisionAnalysis; }> Problems with the cells, and the gaps, overlaps and rules that never fire.
evaluate evaluate(context: Record<string, unknown>, options?: BmxDecisionEvaluateOptions) => Promise<BmxDecisionResult> Works the table out for a context: values by input key, and whatever else formulas read.
evaluateMany evaluateMany(contexts: Record<string, unknown>[]) => Promise<BmxDecisionResult[]> Works the table out for many contexts at once.
explain explain(context: Record<string, unknown>) => Promise<string[]> Why the table answers what it does for a context, in sentences.
exportDmn exportDmn() => Promise<string> The table as DMN 1.3 XML.
getTable getTable() => Promise<BmxDecisionTableData> The table as it stands.
importDmn importDmn(xml: string) => Promise<string[]> Reads a DMN file's first decision table into the element. Resolves with what could not be carried across.
redo redo() => Promise<boolean> Redoes the last undone change.
runTests runTests() => Promise<BmxDecisionTestResult[]> Runs the table's test cases.
tryValuesOf tryValuesOf(context: Record<string, unknown>) => Promise<BmxDecisionResult> Fills Try it with values and shows the answer.
undo undo() => Promise<boolean> Undoes the last change. Resolves whether there was one.

CSS shadow parts

PartDescription
cell A rule's cell; also cell-problem.
dialog The column editor.
grid The rules grid.
panels The Try it, Check and Tests area.
toolbar The row of name, hit policy and actions.

CSS custom properties

PropertyDescription
--bmx-decision-table-cell-width The narrowest a rule cell is drawn.
--bmx-decision-table-fired The colour that marks the rule that fired.
--bmx-decision-table-input The tint of the input columns' headings.
--bmx-decision-table-max-height The tallest the rules grid grows before it scrolls.
--bmx-decision-table-output The tint of the output columns' headings.
--bmx-decision-table-problem The colour that marks a problem.