FlowChartExecutor
Constructor
Section titled “Constructor”new FlowChartExecutor(chart: FlowChart, options?: FlowChartExecutorOptions)Takes a compiled FlowChart (the return value of .build()). The optional second argument is a FlowChartExecutorOptions bag (e.g. { scopeFactory }). The executor is stateful — call .run() and then read the results.
import { FlowChartExecutor } from 'footprintjs';
const executor = new FlowChartExecutor(chart);await executor.run({ input: { orderId: 'ORD-001', amount: 49.99 } });.run()
Section titled “.run()”Execute the flowchart. Returns an ExecutorResult — the traversal result (a branch id / mapped value / Error), or a PausedResult ({ paused: true, checkpoint }) if a pausable stage paused. Read the final state via getSnapshot() and the trace via getNarrativeEntries().
executor.run(options?: RunOptions): Promise<ExecutorResult>RunOptions
| Field | Type | Description |
|---|---|---|
input | unknown | Business input — frozen, accessible via scope.$getArgs() |
env | ExecutionEnv? | Infrastructure context (signal, timeoutMs, traceId) — accessible via scope.$getEnv(), auto-inherited by subflows |
signal | AbortSignal? | Cooperative cancellation |
timeoutMs | number? | Aborts the run after the deadline via an internal AbortController |
maxDepth | number? | Override the max recursive traversal depth (default 500) |
await executor.run({ input: { userId: 42 }, env: { traceId: 'req-abc123', timeoutMs: 5000 },});Fluent chaining with .recorder()
Section titled “Fluent chaining with .recorder()”For a one-shot run without a persistent executor, chain recorders directly on the compiled chart:
import { narrative } from 'footprintjs';import { metrics } from 'footprintjs/recorders';
const narr = narrative();const metr = metrics();
await chart .recorder(narr) .recorder(metr) .run({ input: { orderId: 'ORD-001', amount: 49.99 } });
console.log(narr.getEntries().map(e => e.text).join('\n'));console.log(`Total: ${metr.all().totalDuration}ms`);chart.recorder(...) returns a chainable RunContext; its .run() resolves to a RunResult ({ state, output, executionTree, commitLog }). Recorder factories (metrics, debug, manifest, …) live in footprintjs/recorders; narrative() is also re-exported from the main entry.
Attaching recorders
Section titled “Attaching recorders”.attachScopeRecorder()
Section titled “.attachScopeRecorder()”Attach a scope recorder — fires on data operations (reads, writes, commits) during stage execution. Idempotent by id: a recorder with the same id replaces the previous one; different ids coexist.
executor.attachScopeRecorder(recorder: ScopeRecorder): voidimport { MetricRecorder, DebugRecorder } from 'footprintjs';
executor.attachScopeRecorder(new MetricRecorder());executor.attachScopeRecorder(new DebugRecorder({ verbosity: 'verbose' }));.attachFlowRecorder()
Section titled “.attachFlowRecorder()”Attach a flow recorder — fires on control-flow events (decisions, loops, forks) after each stage completes.
executor.attachFlowRecorder(recorder: FlowRecorder): voidimport { ManifestFlowRecorder } from 'footprintjs';
const manifest = new ManifestFlowRecorder();executor.attachFlowRecorder(manifest);await executor.run();console.log(manifest.getManifest());.attachCombinedRecorder()
Section titled “.attachCombinedRecorder()”Attach one object that observes multiple channels (scope data-flow, control-flow, and emit). The library duck-types the recorder’s on* methods and routes it to the right internal channels — one id, one call.
executor.attachCombinedRecorder(recorder: CombinedRecorder): voidimport { isFlowEvent } from 'footprintjs';import type { CombinedRecorder } from 'footprintjs';
const audit: CombinedRecorder = { id: 'audit', onWrite: (e) => log('scope write', e.key), onDecision: (e) => log('routed to', e.chosen), onError: (e) => (isFlowEvent(e) ? log('flow error', e.stageName) : log('scope error', e.operation)),};executor.attachCombinedRecorder(audit);.attachEmitRecorder()
Section titled “.attachEmitRecorder()”Attach a recorder for the emit channel — consumer-emitted structured events from scope.$emit(name, payload). Fires synchronously, in call order.
executor.attachEmitRecorder(recorder: EmitRecorder): voidexecutor.attachEmitRecorder({ id: 'token-meter', onEmit: (e) => { if (e.name === 'myapp.llm.tokens') trackTokens(e.payload); },});.enableNarrative()
Section titled “.enableNarrative()”Shorthand: attaches a CombinedNarrativeRecorder (a CombinedRecorder spanning scope, flow, and emit channels), producing a merged timeline.
executor.enableNarrative(options?: CombinedNarrativeRecorderOptions): voidexecutor.enableNarrative();await executor.run();executor.getNarrativeEntries().forEach(entry => console.log(entry.text));Reading results
Section titled “Reading results”.getNarrativeEntries()
Section titled “.getNarrativeEntries()”The single public narrative API. Returns structured CombinedNarrativeEntry[] — each entry has a type (stage, step, condition, fork, subflow, loop, emit, …), text, depth, and optional stageName / stageId / runtimeStageId metadata. Render however you want; call .map(e => e.text) for a flat string[].
executor.getNarrativeEntries(): CombinedNarrativeEntry[]executor.enableNarrative();await executor.run();
executor.getNarrativeEntries().forEach(e => console.log(e.text));// Stage 1: The process began with AssessCredit.// Step 1: Write creditScore = 750// [Condition]: It evaluated Rule 0 "Strong profile": creditScore 750 gt 700 ✓ → approved.// Stage 2: Next, it moved on to Approve.// Step 1: Write decision = "Approved".getSnapshot()
Section titled “.getSnapshot()”Returns the full RuntimeSnapshot — shared state, execution tree, commit log, and recorder snapshots at every nesting depth. Pass { redact: true } to get a scrubbed sharedState.
executor.getSnapshot(options?: { redact?: boolean }): RuntimeSnapshotconst snapshot = executor.getSnapshot();const finalState = snapshot.sharedState as LoanState;console.log(finalState.decision); // 'Approved'Use getSubtreeSnapshot() to drill into subflows.
Pause / Resume
Section titled “Pause / Resume”When a pausable stage pauses, run() resolves with a PausedResult. Inspect and persist the checkpoint, then continue later — even on a different process.
executor.isPaused(): booleanexecutor.getCheckpoint(): FlowchartCheckpoint | undefinedexecutor.resume(checkpoint: FlowchartCheckpoint, resumeInput?: unknown): Promise<ExecutorResult>await executor.run({ input });
if (executor.isPaused()) { const checkpoint = executor.getCheckpoint()!; // JSON-safe — store in Redis/Postgres/etc. // Later, with the human's answer: await executor.resume(checkpoint, { approved: true });}The checkpoint is JSON-serializable. See the Pause / Resume guide for the full human-in-the-loop pattern.
Redaction
Section titled “Redaction”.setRedactionPolicy()
Section titled “.setRedactionPolicy()”Protect sensitive fields from leaking into narratives and debug logs. Keys matching the policy are replaced with [REDACTED] in all recorder output.
executor.setRedactionPolicy(policy: RedactionPolicy): voidexecutor.setRedactionPolicy({ keys: ['password', 'ssn', 'cardNumber'], // exact key matches patterns: [/token$/i], // regex-matched keys fields: { customer: ['email'] }, // field-level scrub within an object emitPatterns: [/\.auth\./], // matched against scope.$emit() event names});getRedactionReport() returns a compliance-friendly summary of what was redacted (never the actual values).
Try it live
Section titled “Try it live”- Auto-Narrative —
enableNarrative()+getNarrativeEntries() - Metrics —
MetricRecorder - Redaction —
setRedactionPolicy()
→ Full guide: Observing with recorders