Decision branching
decide() and select() turn opaque branch IDs into captured evidence. The narrative goes from “chose approved” to “creditScore 750 gt 700 ✓, dti 0.38 lt 0.43 ✓ → approved.”
This guide assumes you know how to use .addDeciderFunction() and .addSelectorFunction() — see Building a flowchart if you haven’t already.
decide() — first-match branching
Section titled “decide() — first-match branching”decide(scope, rules, defaultBranch) evaluates rules in order and returns on the first match. If no rule matches, it returns defaultBranch.
import { flowChart, decide } from 'footprintjs';
interface LoanState { creditScore: number; dti: number; decision?: string;}
const chart = flowChart<LoanState>('LoadApplication', async (scope) => { scope.creditScore = 750; scope.dti = 0.38;}, 'load-app') .addDeciderFunction('ClassifyRisk', (scope) => { return decide(scope, [ { when: { creditScore: { gt: 700 }, dti: { lt: 0.43 } }, then: 'approved', label: 'Good credit + low DTI', }, { when: { creditScore: { gt: 600 } }, then: 'manual-review', label: 'Marginal credit', }, ], 'rejected'); }, 'classify-risk', 'Evaluate loan risk') .addFunctionBranch('approved', 'Approve', async (scope) => { scope.decision = 'Approved'; }) .addFunctionBranch('manual-review', 'Review', async (scope) => { scope.decision = 'Sent to manual review'; }) .addFunctionBranch('rejected', 'Reject', async (scope) => { scope.decision = 'Rejected'; }) .setDefault('rejected') .end() .build();The narrative automatically includes the evidence:
Stage 3: Next, it moved on to ClassifyRisk.[Condition]: It evaluated Rule 0 "Good credit + low DTI": creditScore 750 gt 700 ✓, dti 0.38 lt 0.43 ✓, and chose approved.Stage 4: Next, it moved on to Approve.Two when formats
Section titled “Two when formats”Each rule’s when field accepts either a filter object or a function.
Filter object (recommended) — captures key, operator, threshold, actual value, and pass/fail for every condition. Narrative: “creditScore 750 gt 700 ✓”.
{ when: { creditScore: { gt: 700 }, dti: { lt: 0.43 } }, then: 'approved', label: 'Good credit',}Function — captures which keys were read via a temporary recorder. Narrative: “examined creditScore (750), dti (0.38)”. Use when you need arbitrary logic that a filter can’t express.
{ when: (s) => s.creditScore > 700 && s.dti < 0.43, then: 'approved', label: 'Full qualification',}Filter operators
Section titled “Filter operators”All 8 operators follow Prisma naming conventions:
| Operator | Meaning | Example |
|---|---|---|
eq | Equal | { status: { eq: 'active' } } |
ne | Not equal | { status: { ne: 'banned' } } |
gt | Greater than | { score: { gt: 700 } } |
gte | Greater than or equal | { score: { gte: 700 } } |
lt | Less than | { dti: { lt: 0.43 } } |
lte | Less than or equal | { dti: { lte: 0.43 } } |
in | Value in array | { tier: { in: ['gold', 'platinum'] } } |
notIn | Value not in array | { tier: { notIn: ['free'] } } |
Multiple keys in one when use AND semantics — all conditions must match.
Mixed rules
Section titled “Mixed rules”A single decide() call can mix filter rules and function rules:
return decide(scope, [ // Filter rule — full evidence capture { when: { creditScore: { gt: 700 }, dti: { lt: 0.43 } }, then: 'approved', label: 'Clean profile' }, // Function rule — captures which keys were read { when: (s) => s.creditScore > 600, then: 'manual-review', label: 'Marginal' },], 'rejected');select() — multi-match (parallel fan-out)
Section titled “select() — multi-match (parallel fan-out)”select(scope, rules) evaluates all rules and returns every branch that matched. Use it with .addSelectorFunction() to run matched branches in parallel.
import { flowChart, select } from 'footprintjs';
interface ScreeningState { glucose: number; systolicBP: number; bmi: number; results: string[];}
const chart = flowChart<ScreeningState>('LoadVitals', async (scope) => { scope.glucose = 128; scope.systolicBP = 148; scope.bmi = 25; scope.results = [];}, 'load-vitals') .addSelectorFunction('Triage', (scope) => { return select(scope, [ { when: { glucose: { gt: 100 } }, then: 'diabetes', label: 'Elevated glucose' }, { when: { systolicBP: { gt: 140 } }, then: 'hypertension', label: 'High BP' }, { when: { bmi: { gt: 30 } }, then: 'obesity', label: 'High BMI' }, ]); }, 'triage') .addFunctionBranch('diabetes', 'DiabetesScreen', async (scope) => { scope.results = [...scope.results, 'glucose: ' + scope.glucose]; }) .addFunctionBranch('hypertension', 'BPCheck', async (scope) => { scope.results = [...scope.results, 'BP: ' + scope.systolicBP]; }) .addFunctionBranch('obesity', 'BMIAssess', async (scope) => { scope.results = [...scope.results, 'BMI: ' + scope.bmi]; }) .end() .build();With glucose=128 and systolicBP=148, both diabetes and hypertension branches run in parallel. The obesity branch is skipped. Narrative shows evidence for every evaluated rule.
Plain string fallback
Section titled “Plain string fallback”If you don’t need evidence capture, return a plain string from the decider function. This is fully backward-compatible:
.addDeciderFunction('Route', (scope) => { return scope.amount > 100 ? 'large' : 'small';}, 'route')Try it live
Section titled “Try it live”Run the loan application and health screening examples in the browser with live evidence output:
- decide() / select() sample — filter rules, function rules, multi-match
- Decider (Conditional) — plain decider without decide()
- Selector — selector without select()