Skip to content

FlowChartExecutor

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 } });

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

FieldTypeDescription
inputunknownBusiness input — frozen, accessible via scope.$getArgs()
envExecutionEnv?Infrastructure context (signal, timeoutMs, traceId) — accessible via scope.$getEnv(), auto-inherited by subflows
signalAbortSignal?Cooperative cancellation
timeoutMsnumber?Aborts the run after the deadline via an internal AbortController
maxDepthnumber?Override the max recursive traversal depth (default 500)
await executor.run({
input: { userId: 42 },
env: { traceId: 'req-abc123', timeoutMs: 5000 },
});

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.


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): void
import { MetricRecorder, DebugRecorder } from 'footprintjs';
executor.attachScopeRecorder(new MetricRecorder());
executor.attachScopeRecorder(new DebugRecorder({ verbosity: 'verbose' }));

Attach a flow recorder — fires on control-flow events (decisions, loops, forks) after each stage completes.

executor.attachFlowRecorder(recorder: FlowRecorder): void
import { ManifestFlowRecorder } from 'footprintjs';
const manifest = new ManifestFlowRecorder();
executor.attachFlowRecorder(manifest);
await executor.run();
console.log(manifest.getManifest());

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): void
import { 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);

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): void
executor.attachEmitRecorder({
id: 'token-meter',
onEmit: (e) => {
if (e.name === 'myapp.llm.tokens') trackTokens(e.payload);
},
});

Shorthand: attaches a CombinedNarrativeRecorder (a CombinedRecorder spanning scope, flow, and emit channels), producing a merged timeline.

executor.enableNarrative(options?: CombinedNarrativeRecorderOptions): void
executor.enableNarrative();
await executor.run();
executor.getNarrativeEntries().forEach(entry => console.log(entry.text));

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"

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 }): RuntimeSnapshot
const snapshot = executor.getSnapshot();
const finalState = snapshot.sharedState as LoanState;
console.log(finalState.decision); // 'Approved'

Use getSubtreeSnapshot() to drill into subflows.


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(): boolean
executor.getCheckpoint(): FlowchartCheckpoint | undefined
executor.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.


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): void
executor.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).


→ Full guide: Observing with recorders