Skip to content

Observing with recorders

This guide assumes you know how to build a flowchart — see Building a flowchart first.

footprintjs has two observer layers: Scope recorders fire on data operations (reads, writes, commits) during stage execution. Flow recorders fire on control-flow events (decisions, loops, forks) after each stage completes.

Both attach to the executor and run in error-isolated dispatch — one failing recorder never stops the pipeline.

executor.enableNarrative() attaches a CombinedNarrativeRecorder that observes both layers and produces a merged human-readable trace:

import { flowChart, FlowChartExecutor } from 'footprintjs';
interface TempState {
temperature: number;
unit: string;
temperatureF?: number;
status?: string;
}
const chart = flowChart<TempState>('Ingest', async (scope) => {
scope.temperature = 38.5;
scope.unit = 'celsius';
}, 'ingest')
.addFunction('Convert', async (scope) => {
if (scope.unit === 'celsius') {
scope.temperatureF = scope.temperature * 1.8 + 32;
}
}, 'convert')
.addFunction('Classify', async (scope) => {
scope.status = scope.temperatureF! > 100.4 ? 'fever' : 'normal';
}, 'classify')
.build();
const executor = new FlowChartExecutor(chart);
executor.enableNarrative();
await executor.run();
executor.getNarrative().forEach((line) => console.log(line));
// Stage 1: The process began with Ingest.
// Step 1: Write temperature = 38.5
// Step 2: Write unit = "celsius"
// Stage 2: Next, it moved on to Convert.
// Step 1: Read unit = "celsius"
// Step 2: Read temperature = 38.5
// Step 3: Write temperatureF = 101.3
// Stage 3: Next, it moved on to Classify.
// Step 1: Read temperatureF = 101.3
// Step 2: Write status = "fever"

MetricRecorder tracks per-stage read/write counts and latency:

import { FlowChartExecutor, MetricRecorder } from 'footprintjs';
const metrics = new MetricRecorder();
const executor = new FlowChartExecutor(chart);
executor.attachRecorder(metrics);
await executor.run();
const result = metrics.getMetrics();
for (const [stage, m] of result.stageMetrics) {
console.log(`${stage}: ${m.totalDuration}ms reads:${m.readCount} writes:${m.writeCount}`);
}
console.log(`Total: ${result.totalDuration}ms`);

DebugRecorder captures a structured log of all ops for debugging:

import { FlowChartExecutor, DebugRecorder } from 'footprintjs';
const debug = new DebugRecorder({ verbosity: 'verbose' });
const executor = new FlowChartExecutor(chart);
executor.attachRecorder(debug);
await executor.run();
const entries = debug.getEntries();
const errors = entries.filter(e => e.type === 'error');
const writes = entries.filter(e => e.type === 'write');
console.log(`${writes.length} writes, ${errors.length} errors`);

Implement Recorder to build audit logs, compliance trails, or custom telemetry. All hooks are optional:

import { flowChart, FlowChartExecutor, type Recorder, type ReadEvent, type WriteEvent, type CommitEvent } from 'footprintjs';
class AuditRecorder implements Recorder {
readonly id = 'audit';
private log: string[] = [];
onWrite(event: WriteEvent): void {
this.log.push(`[WRITE] ${event.stageName}: ${event.key} = ${JSON.stringify(event.value)}`);
}
onRead(event: ReadEvent): void {
this.log.push(`[READ] ${event.stageName}: ${event.key} -> ${JSON.stringify(event.value)}`);
}
onCommit(event: CommitEvent): void {
this.log.push(`[COMMIT] ${event.stageName}: ${event.mutations.length} mutation(s)`);
}
getLog(): string[] { return this.log; }
}
const auditRecorder = new AuditRecorder();
const executor = new FlowChartExecutor(chart);
executor.attachRecorder(auditRecorder);
await executor.run();
auditRecorder.getLog().forEach((line) => console.log(line));
HookFires when
onRead(event)A scope property is read
onWrite(event)A scope property is written (pre-commit)
onCommit(event)A stage’s transaction is flushed
onStageStart(event)A stage begins
onStageEnd(event)A stage completes
onError(event)A stage throws

Implement FlowRecorder to observe control-flow events — decisions, loops, forks. All hooks are optional:

import { FlowChartExecutor, type FlowRecorder } from 'footprintjs';
const metrics: FlowRecorder = {
id: 'metrics',
onStageExecuted: (event) => {
console.log(`stage: ${event.stageName}`);
},
onDecision: (event) => {
console.log(`decision: ${event.stageName} -> ${event.chosen}`);
// event.evidence is set when decide() was used
},
onLoop: (event) => {
console.log(`loop ${event.iteration} -> ${event.target}`);
},
onBreak: (event) => {
console.log(`break at ${event.stageName}`);
},
};
executor.attachFlowRecorder(metrics);
HookFires when
onStageExecuted(event)Any stage finishes
onNext(event)Sequential stage transition
onDecision(event)A decider chooses a branch
onFork(event)A fork/join (addListOfFunction) executes
onSelected(event)A selector picks branches
onLoop(event)A loop iteration completes
onBreak(event)scope.$break() is called
onSubflowEntry(event)Execution enters a subflow
onSubflowExit(event)Execution exits a subflow
onError(event)A stage throws

When a pipeline loops many times, the default narrative can grow large. footprintjs ships 6 loop-aware strategies:

import {
NarrativeFlowRecorder,
WindowedNarrativeFlowRecorder,
SilentNarrativeFlowRecorder,
AdaptiveNarrativeFlowRecorder,
SeparateNarrativeFlowRecorder,
} from 'footprintjs';
// WindowedNarrativeFlowRecorder(first, last) — keeps first 3 + last 2 iterations
executor.attachFlowRecorder(new WindowedNarrativeFlowRecorder(3, 2));
// SilentNarrativeFlowRecorder — summary only, no per-iteration sentences
executor.attachFlowRecorder(new SilentNarrativeFlowRecorder());
// AdaptiveNarrativeFlowRecorder(full, every) — full for first 3, then every 5th
executor.attachFlowRecorder(new AdaptiveNarrativeFlowRecorder(3, 5));
// SeparateNarrativeFlowRecorder — main narrative + separate loop detail channel
const separate = new SeparateNarrativeFlowRecorder();
executor.attachFlowRecorder(separate);
await executor.run();
console.log(separate.getLoopSentences()); // loop detail
console.log(Object.fromEntries(separate.getLoopCounts()));

Attach as many recorders as needed — they all run in the same execution:

executor.attachRecorder(auditRecorder);
executor.attachRecorder(new MetricRecorder());
executor.attachFlowRecorder(new NarrativeFlowRecorder());
executor.attachFlowRecorder(metricsFlowRecorder);
await executor.run();

See recorders and narrative strategies in action in the browser: