withFlowAnalyzer
Two complementary tools for understanding what a flow does:
analyzeFlow()— synchronous static walk; answers "what paths are possible?"withTrace()— installs runtime hooks; answers "what path was actually taken?"
Setup
typescript
import { FlowBuilder } from "flowneer";
import { withFlowAnalyzer } from "flowneer/plugins/dev";
const AppFlow = FlowBuilder.extend([withFlowAnalyzer]);analyzeFlow() — static path map
Walks the compiled steps[] array and returns a PathMap describing all known nodes, anchors, and structural paths. Nothing is executed.
typescript
const flow = new AppFlow<State>()
.anchor("refine", 5)
.then(generateDraft, { label: "gen:draft" })
.then(async (s) => (s.score < 0.9 ? "#refine" : undefined), {
label: "check:score",
})
.then(publish, { label: "publish" });
const map = flow.analyzeFlow();
console.log(map.anchors); // ["refine"]
console.log(map.hasDynamicGotos); // true — fn steps may return goto strings
console.log(map.nodes.map((n) => n.label));
// ["refine", "gen:draft", "check:score", "publish"]PathMap
typescript
interface PathMap {
nodes: PathNode[];
/** All anchor names declared in this flow (and nested sub-flows). */
anchors: string[];
/**
* True whenever fn steps are present — they may return "#anchorName" at
* runtime. Static analysis cannot resolve these edges without execution.
*/
hasDynamicGotos: boolean;
}PathNode
typescript
interface PathNode {
id: string; // "fn_0", "branch_2", "anchor:refine", etc.
type: "fn" | "branch" | "loop" | "batch" | "parallel" | "anchor";
label?: string;
branches?: Record<string, PathNode[]>; // branch arms
body?: PathNode[]; // loop / batch inner steps
parallel?: PathNode[][]; // parallel fan-out lanes
}Branch analysis
typescript
const flow = new AppFlow<State>().branch(
async (s) => (s.ok ? "pass" : "fail"),
{
pass: async (s) => {
s.result = "ok";
},
fail: async (s) => {
s.result = "failed";
},
},
);
const map = flow.analyzeFlow();
const branchNode = map.nodes.find((n) => n.type === "branch");
console.log(Object.keys(branchNode.branches));
// ["pass", "fail"]withTrace() — runtime execution trace
Installs beforeStep/afterStep hooks that record every visited step with its type, label, and wall-clock duration.
typescript
const flow = new AppFlow<State>()
.then(fetchUser, { label: "fetch:user" })
.then(enrichProfile, { label: "enrich:profile" })
.then(saveResult, { label: "save" });
const trace = flow.withTrace();
await flow.run(shared);
const report = trace.getTrace();
console.log(report.pathSummary);
// ["fetch:user", "enrich:profile", "save"]
console.log(report.totalDurationMs);
// 347
trace.dispose(); // remove hooks when doneTraceHandle
typescript
interface TraceHandle {
/** Returns a snapshot of the trace collected so far. Safe to call mid-run. */
getTrace(): TraceReport;
/** Removes the installed hooks. */
dispose(): void;
}TraceReport
typescript
interface TraceReport {
events: TraceEvent[];
totalDurationMs: number;
/** Ordered list of visited step labels. Unlabelled steps are omitted. */
pathSummary: string[];
}
interface TraceEvent {
stepIndex: number;
type: string;
label?: string;
durationMs: number;
}Compose with withDryRun
Trace the execution path without running any real logic:
typescript
const flow = new AppFlow<State>()
.withDryRun()
.then(callExpensiveApi, { label: "api:call" })
.then(processResult, { label: "process" });
const trace = flow.withTrace();
await flow.run(shared); // no real I/O
console.log(trace.getTrace().pathSummary);
// ["api:call", "process"]
trace.dispose();Dispose
withTrace() returns a dispose() function that removes the hooks. Always call dispose() when the trace is no longer needed to avoid accumulating hooks across multiple runs.
typescript
const trace = flow.withTrace();
try {
await flow.run(shared);
console.log(trace.getTrace());
} finally {
trace.dispose();
}Use Cases
- Pre-deployment checks — run
analyzeFlow()at startup to verify anchor names are present and flow structure matches expectations. - Debugging —
.withDryRun().withTrace()shows the execution path without side effects in staging or test environments. - CI test assertions — assert that a flow visited exactly the steps it should in a given scenario.
- Performance profiling — use
TraceEvent.durationMsto find slow steps without a full telemetry stack.