Skip to content

Recorders

footprintjs has two observer layers. Scope recorders (Recorder) fire on data operations during stage execution. Flow recorders (FlowRecorder) fire on control-flow events after each stage completes. All built-in recorders attach via chart.recorder(r) or executor.attachRecorder(r) / executor.attachFlowRecorder(r).


The primary observer. Implements both Recorder and FlowRecorder — captures reads, writes, decisions, and loops into a single human-readable timeline.

import { narrative } from 'footprintjs';
const narr = narrative();
await chart.recorder(narr).run({ input });
narr.lines() // string[] — one sentence per event
narr.structured() // CombinedNarrativeEntry[] — typed entries with depth/stageName

NarrativeInstance methods

MethodReturnsDescription
.lines()string[]Full merged narrative
.structured()CombinedNarrativeEntry[]Structured entries with type, depth, stageName, stageId
const narr = narrative();
await chart.recorder(narr).run({ input: { creditScore: 750, dti: 0.38 } });
narr.lines().forEach(line => console.log(line));
// Stage 1: The process began with AssessCredit.
// Step 1: Write creditScore = 750
// [Condition]: It evaluated Rule 0 "Strong profile": creditScore 750 gt 700 ✓ → approved.

Tracks per-stage read/write counts and wall-clock duration.

import { metrics } from 'footprintjs';
const metr = metrics();
await chart.recorder(metr).run({ input });
metr.all() // aggregate totals
metr.stage(id) // per-stage breakdown
metr.reads() // total read count
metr.writes() // total write count
metr.commits() // total commit count

MetricsInstance methods

MethodReturnsDescription
.all()AggregateMetrics{ totalDuration, totalReads, totalWrites, stageMetrics }
.stage(stageId)StageMetrics | undefinedDuration + counts for one stage
.reads()numberTotal reads across all stages
.writes()numberTotal writes across all stages
.commits()numberTotal commit events
const metr = metrics();
await chart.recorder(metr).run({ input });
const all = metr.all();
console.log(`Total: ${all.totalDuration}ms`);
for (const [id, m] of all.stageMetrics) {
console.log(`${id}: ${m.totalDuration}ms · ${m.readCount}r ${m.writeCount}w`);
}

Captures a structured log of every scope operation — reads, writes, commits, errors — for debugging and inspection.

import { debug } from 'footprintjs';
const dbg = debug();
await chart.recorder(dbg).run({ input });
dbg.logs() // DebugEntry[] — all operations in order

DebugInstance methods

MethodReturnsDescription
.logs()DebugEntry[]All scope operations: { type, stageName, key, value }
const dbg = debug();
await chart.recorder(dbg).run({ input });
const writes = dbg.logs().filter(e => e.type === 'write');
const errors = dbg.logs().filter(e => e.type === 'error');
console.log(`${writes.length} writes, ${errors.length} errors`);

Builds a lightweight catalog of all subflow IDs, names, and descriptions as a side effect of traversal. An LLM receiving a snapshot can read the manifest to understand structure, then pull full specs on demand.

import { manifest } from 'footprintjs';
const mani = manifest();
await chart.recorder(mani).run({ input });
mani.entries() // ManifestEntry[] — nested tree of subflow entries

ManifestInstance methods

MethodReturnsDescription
.entries()ManifestEntry[]Nested tree: { subflowId, name, description, children }

The factory functions above (narrative(), metrics(), etc.) wrap these classes. Use the classes directly only when you need access to raw methods or want to attach via executor.attachRecorder().

import { MetricRecorder } from 'footprintjs';
const rec = new MetricRecorder();
executor.attachRecorder(rec);
await executor.run();
const result = rec.getMetrics();
// result.totalDuration, result.totalReads, result.stageMetrics (Map)
import { DebugRecorder } from 'footprintjs';
const rec = new DebugRecorder({ verbosity: 'verbose' });
executor.attachRecorder(rec);
await executor.run();
rec.getEntries().forEach(e => {
if (e.type === 'write') console.log(`${e.stageName}: ${e.data.key} = ${JSON.stringify(e.data.value)}`);
});

When a pipeline loops, the default narrative grows linearly. Six strategies control this:

import {
WindowedNarrativeFlowRecorder, // first N + last N iterations
SilentNarrativeFlowRecorder, // summary only
AdaptiveNarrativeFlowRecorder, // full for first N, then every Kth
RLENarrativeFlowRecorder, // run-length encoding
MilestoneNarrativeFlowRecorder, // only milestone iterations
SeparateNarrativeFlowRecorder, // main + separate loop detail channel
} from 'footprintjs';
executor.attachFlowRecorder(new WindowedNarrativeFlowRecorder(3, 2));
// → full detail for first 3 and last 2 iterations; summary for the rest

Implement Recorder for audit logs, compliance trails, or telemetry:

import { type Recorder, type ReadEvent, type WriteEvent } 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}`);
}
getLog() { return this.log; }
}
executor.attachRecorder(new AuditRecorder());

Scope recorder hooks: onRead, onWrite, onCommit, onStageStart, onStageEnd, onError

Implement FlowRecorder for control-flow observation:

import { type FlowRecorder } from 'footprintjs';
const tracer: FlowRecorder = {
id: 'tracer',
onDecision: (event) => {
console.log(`${event.stageName}${event.chosen}`);
// event.evidence populated when decide() was used
},
onLoop: (event) => console.log(`loop ${event.iteration}`),
};
executor.attachFlowRecorder(tracer);

Flow recorder hooks: onStageExecuted, onNext, onDecision, onFork, onSelected, onLoop, onBreak, onSubflowEntry, onSubflowExit, onError


→ Full guide: Observing with recorders