Skip to content

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(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.

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',
}

All 8 operators follow Prisma naming conventions:

OperatorMeaningExample
eqEqual{ status: { eq: 'active' } }
neNot equal{ status: { ne: 'banned' } }
gtGreater than{ score: { gt: 700 } }
gteGreater than or equal{ score: { gte: 700 } }
ltLess than{ dti: { lt: 0.43 } }
lteLess than or equal{ dti: { lte: 0.43 } }
inValue in array{ tier: { in: ['gold', 'platinum'] } }
notInValue not in array{ tier: { notIn: ['free'] } }

Multiple keys in one when use AND semantics — all conditions must match.

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.

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')

Run the loan application and health screening examples in the browser with live evidence output: