flowChart() / FlowChartBuilder
flowChart()
Section titled “flowChart()”The primary factory function. Creates the root stage and returns a builder.
flowChart<State>( name: string, fn: (scope: TypedScope<State>) => Promise<void>, id: string, options?: FlowChartOptions,): FlowChartBuilder<State>Parameters
| Param | Type | Description |
|---|---|---|
name | string | Stage display name (shown in narrative) |
fn | async function | Stage handler — reads/writes scope |
id | string | Stable ID for snapshot navigation |
options | FlowChartOptions? | Options bag: description? (human description shown on the root spec + MCP tool output), structureRecorders? (build-time observers), failFast? (only for flowChartSelector) |
import { flowChart } from 'footprintjs';
interface LoanState { creditScore: number; dti: number; decision?: string;}
const chart = flowChart<LoanState>('AssessCredit', async (scope) => { scope.creditScore = 720; scope.dti = 0.38;}, 'assess-credit', { description: 'Load and validate credit application' }) .build();FlowChartBuilder
Section titled “FlowChartBuilder”Returned by flowChart(). All methods return the builder (or a branch builder) for chaining. Call .build() at the end.
.addFunction()
Section titled “.addFunction()”Append a sequential stage.
.addFunction( name: string, fn: (scope: TypedScope<State>) => Promise<void>, id: string, description?: string,): FlowChartBuilder<State>const chart = flowChart<LoanState>('Load', async (scope) => { scope.creditScore = 720;}, 'load') .addFunction('Validate', async (scope) => { scope.decision = scope.creditScore >= 580 ? 'proceed' : 'reject'; }, 'validate', 'Check minimum credit score') .build();.addDeciderFunction()
Section titled “.addDeciderFunction()”Add a branching stage. The handler returns a branch ID (plain string) or a DecisionResult from decide().
.addDeciderFunction( name: string, fn: (scope: TypedScope<State>) => string | DecisionResult, id: string, description?: string,): DeciderBuilder<State>Inside the returned DeciderBuilder, call .addFunctionBranch(), .setDefault(), then .end() to return to the parent builder.
import { decide } from 'footprintjs';
const chart = flowChart<LoanState>('Load', async (scope) => { scope.creditScore = 720; scope.dti = 0.38;}, 'load') .addDeciderFunction('ClassifyRisk', (scope) => { return decide(scope, [ { when: { creditScore: { gt: 700 }, dti: { lt: 0.43 } }, then: 'approved', label: 'Strong profile' }, { when: { creditScore: { gt: 580 } }, then: 'review', label: 'Marginal' }, ], 'rejected'); }, 'classify-risk', 'Route by credit profile') .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();.addSelectorFunction()
Section titled “.addSelectorFunction()”Add a parallel fan-out stage. The handler returns a SelectionResult from select(). All matched branches run concurrently.
.addSelectorFunction( name: string, fn: (scope: TypedScope<State>) => SelectionResult, id: string, description?: string, options?: { failFast?: boolean },): SelectorBuilder<State>By default a selected fan-out is best-effort (Promise.allSettled — a branch error is collected, not rethrown). Pass { failFast: true } to use Promise.all so the first branch error rejects the whole run — use it when every selected branch is required.
.addSubFlowChartNext()
Section titled “.addSubFlowChartNext()”Mount another compiled flowchart as a sequential subflow.
.addSubFlowChartNext( id: string, chart: FlowChart, name?: string, options?: SubflowMountOptions,): FlowChartBuilder<State>SubflowMountOptions carries inputMapper (parent scope → subflow input), outputMapper (subflow output → parent scope), arrayMerge (ArrayMergeMode.Concat default / .Replace), convergeAt (structure-only merge target), and propagateBreak (inner $break propagates to the parent).
const paymentChart = new FlowChartBuilder() .start('ChargeCard', async (scope: any) => { scope.txnId = 'TXN-001'; }, 'charge-card') .build();
const chart = flowChart<OrderState>('ReceiveOrder', async (scope) => { scope.amount = 99.99;}, 'receive-order') .addSubFlowChartNext('sf-payment', paymentChart, 'Payment', { inputMapper: (s) => ({ amount: s.amount }), }) .build();.addStreamingFunction()
Section titled “.addStreamingFunction()”Add a stage that emits tokens incrementally (for LLM responses, SSE, etc.).
.addStreamingFunction( name: string, fn: (scope: TypedScope<State>, breakPipeline: () => void, streamCallback?: StreamCallback) => Promise<void>, id: string, streamId?: string, description?: string,): FlowChartBuilder<State>The stage emits tokens by calling the injected streamCallback. The lifecycle handlers (onStart / onToken / onEnd) are StreamHandlers attached to the executor, not passed to the stage:
.addStreamingFunction('GenerateResponse', async (scope, _breakPipeline, streamCallback) => { for await (const chunk of callLLM(scope.$getArgs())) { streamCallback?.(chunk); }}, 'generate-response', 'llm-stream')import type { StreamHandlers } from 'footprintjs';
const handlers: StreamHandlers = { onStart: (id) => console.log(`[${id}] started`), onToken: (id, token) => process.stdout.write(token), onEnd: (id, full) => console.log(`[${id}] done: "${full}"`),};
const executor = new FlowChartExecutor(chart, { streamHandlers: handlers });.loopTo()
Section titled “.loopTo()”Create a back-edge to an earlier stage. The loop exits when scope.$break() is called inside any stage.
.loopTo(targetStageId: string): FlowChartBuilder<State>Pass the target stage’s id (not its display name) — loopTo validates against declared stage ids and throws at build time otherwise.
const chart = flowChart<{ attempt: number; result?: string }>('Init', async (scope) => { scope.attempt = 0;}, 'init') .addFunction('TryRequest', async (scope) => { scope.attempt += 1; const ok = await fetchWithRetry(); if (ok) { scope.result = 'success'; scope.$break(); } if (scope.attempt >= 3) { scope.result = 'failed'; scope.$break(); } }, 'try-request') .loopTo('try-request') .build();.contract()
Section titled “.contract()”Attach input/output schemas for validation and API generation. See Self-describing APIs.
.contract(options: { input?: ZodSchema | JsonSchema; output?: ZodSchema | JsonSchema; mapper?: (finalScope: Record<string, unknown>) => unknown;}): FlowChartBuilder<State>.build()
Section titled “.build()”Compile the builder into an immutable FlowChart. Must be called last.
.build(): FlowChartTry it live
Section titled “Try it live”- Linear Pipeline — sequential stages
- Decider (Conditional) — branching
- Loops — loopTo + $break()
- Subflow — nested flowchart
→ Full guide: Building a flowchart