Skip to content

decide() / select()

First-match branching with automatic evidence capture. Returns a DecisionResult that the engine uses to route and record which conditions were evaluated.

decide<S>(
scope: TypedScope<S> | ScopeFacade,
rules: DecideRule<S>[],
defaultBranch: string,
): DecisionResult

Parameters

ParamTypeDescription
scopeTypedScope<S>The stage scope — passed directly from the decider handler
rulesDecideRule<S>[]Ordered list of rules. First match wins.
defaultBranchstringBranch ID returned when no rule matches
import { flowChart, decide } from 'footprintjs';
interface LoanState {
creditScore: number;
dti: number;
decision?: string;
}
const chart = flowChart<LoanState>('Load', async (scope) => {
scope.creditScore = 750;
scope.dti = 0.38;
}, 'load')
.addDeciderFunction('ClassifyRisk', (scope) => {
return decide(scope, [
{
when: { creditScore: { gt: 700 }, dti: { lt: 0.43 } },
then: 'approved',
label: 'Strong credit profile',
},
{
when: { creditScore: { gt: 580 } },
then: 'review',
label: 'Marginal credit',
},
], 'rejected');
}, 'classify-risk')
.addFunctionBranch('approved', 'Approve', async (scope) => { scope.decision = 'Approved'; })
.addFunctionBranch('review', 'Review', async (scope) => { scope.decision = 'Manual review'; })
.addFunctionBranch('rejected', 'Reject', async (scope) => { scope.decision = 'Rejected'; })
.setDefault('rejected')
.end()
.build();

Narrative output:

[Condition]: It evaluated Rule 0 "Strong credit profile":
creditScore 750 gt 700 ✓, dti 0.38 lt 0.43 ✓, and chose approved.

Multi-match fan-out with automatic evidence capture. Evaluates all rules and returns every branch that matched. Used with .addSelectorFunction().

select<S>(
scope: TypedScope<S> | ScopeFacade,
rules: DecideRule<S>[],
): SelectionResult
import { flowChart, select } from 'footprintjs';
interface ScreeningState {
glucose: number;
systolicBP: number;
results: string[];
}
const chart = flowChart<ScreeningState>('LoadVitals', async (scope) => {
scope.glucose = 128;
scope.systolicBP = 148;
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' },
]);
}, 'triage')
.addFunctionBranch('diabetes', 'DiabetesCheck', async (scope) => {
scope.results = [...scope.results, 'glucose check'];
})
.addFunctionBranch('hypertension', 'BPCheck', async (scope) => {
scope.results = [...scope.results, 'BP check'];
})
.end()
.build();

Both branches match, so DiabetesCheck and BPCheck run in parallel.


interface DecideRule<S> {
when: WhereFilter<S> | ((scope: S) => boolean);
then: string;
label?: string;
}
FieldTypeDescription
whenfilter object or functionCondition to test
thenstringBranch ID to route to when matched
labelstring?Human name shown in the narrative — e.g. "Good credit"

A WhereFilter<S> is a plain object mapping state keys to FilterOps:

{ creditScore: { gt: 700 }, dti: { lt: 0.43 } }

All keys use AND semantics — every condition must pass for the rule to match. The narrative captures each condition’s key, operator, threshold, actual value, and pass/fail result.

A (scope: S) => boolean for logic a filter can’t express. The narrative captures which scope keys were read during the function call.

{ when: (s) => s.creditScore > 700 && someExternalCheck(s.id), then: 'approved' }

All 8 operators follow Prisma naming:

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

→ Full guide: Decision branching