- C-05: Add flowgraph-api.md with complete public API surface - C-06: Document <Map> component in workflow-templates.md - C-07: Specify Conditional else-branch behavior - C-08: Add lifecycle/ownership section to reactive-execution.md - C-09: Add consumer-integration.md end-to-end walkthrough - W-02: Add reactive error boundary semantics (3 levels) - W-03: Complete ReactiveContext interface definition - W-04: Add template composition rules (8 rules) - W-05: Document removeChild for both HostConfigs - W-06: Document signal/effect disposal lifecycle - W-07: Add ADR-004 (no schema version field) - W-08: Add type compatibility depth/contract to analysis.md - W-11: Add performance characteristics section - S-01: Getting Started merged into consumer-integration.md - S-02: Add flow diagrams for template rendering pipeline - S-03: Add node status state machine diagram - S-04: Add testing strategy section - S-06: Validate source structure cross-references Review round 2 fixes: - Define TemplateNodeAttrs as alias for OperationNodeAttrs - Document CallEventMapValue and CallResult types in schema.md - Standardize CycleError naming (replace CircularDependencyError) - Add function form to Map.over type definition - Define Map aggregate completion/failure semantics - Fix immutability claim for fromCallEvents - Clarify edgeType storage alongside OperationEdgeAttrs - Clarify WorkflowNode.status === statusMap (same Signal) - Add component-to-tag mapping for WorkflowTag
15 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-05-19 |
FlowGraph Public API
Complete public API surface for the FlowGraph class — constructor, type parameters, methods, and the delegation model with graphology.
Overview
FlowGraph is the central class that wraps a graphology DirectedGraph and enforces DAG invariants. It is generic over node and edge attribute types, supporting three distinct graph modes: operation graph, call graph, and template DAG.
The class delegates graph operations to graphology while providing flowgraph-specific methods for construction, mutation, queries, and analysis. It is NOT a subclass of DirectedGraph — it wraps one, exposing a curated API surface.
Type Parameters
class FlowGraph<
NodeAttrs extends TSchema = OperationNodeAttrs | CallNodeAttrs,
EdgeAttrs extends TSchema = OperationEdgeAttrs | CallEdgeAttrs | TemplateEdgeAttrs,
> {
private _graph: DirectedGraph;
// ...
}
| Parameter | Default | Purpose |
|---|---|---|
NodeAttrs |
OperationNodeAttrs or CallNodeAttrs |
TypeBox schema for node attributes |
EdgeAttrs |
OperationEdgeAttrs, CallEdgeAttrs, or TemplateEdgeAttrs |
TypeBox schema for edge attributes |
Common instantiations:
// Operation graph (static type compatibility)
type OperationGraph = FlowGraph<OperationNodeAttrs, OperationEdgeAttrs>;
// Call graph (dynamic call events)
type CallGraph = FlowGraph<CallNodeAttrs, CallEdgeAttrs>;
// Template DAG (workflow structure)
type TemplateDAG = FlowGraph<TemplateNodeAttrs, TemplateEdgeAttrs>;
Constructor and Factories
new FlowGraph()
constructor(options?: FlowGraphOptions)
Creates an empty graph. Options:
interface FlowGraphOptions {
type?: "directed"; // Always "directed" (default)
multi?: false; // Always false (default) — no parallel edges
allowSelfLoops?: false; // Always false (default) — no self-loops
}
The options object is passed through to new DirectedGraph(). The DAG constraints (multi: false, allowSelfLoops: false) are enforced at the graphology level.
FlowGraph.fromSpecs(specs)
static fromSpecs(specs: OperationSpec[]): OperationGraph
Constructs an operation graph from an array of OperationSpec objects. Creates nodes for each operation and type-compatibility edges via buildTypeEdges(). Throws CycleError if the resulting graph has cycles (shouldn't happen with valid operation specs, but validated defensively).
FlowGraph.fromCallEvents(events)
static fromCallEvents(events: CallEventMapValue[]): CallGraph
Constructs a call graph from an array of call protocol events. Processes events in order, adding nodes and edges. Idempotent — duplicate events have no effect.
FlowGraph.fromJSON(data)
static fromJSON(data: FlowGraphSerialized<NodeAttrs, EdgeAttrs>): FlowGraph<NodeAttrs, EdgeAttrs>
Deserializes from graphology native JSON format. Validates against the appropriate schema (OperationGraphSerialized or CallGraphSerialized). Throws InvalidInputError on validation failure.
Round-trip guarantee: fromSpecs() → export() → fromJSON() is lossless.
Mutation Methods
Node Mutations
| Method | Signature | Behavior |
|---|---|---|
addNode |
(key: string, attrs: NodeAttrs): void |
Adds a node. Throws DuplicateNodeError if key exists. |
removeNode |
(key: string): void |
Removes a node and all attached edges. Throws NodeNotFoundError if key doesn't exist. |
updateNode |
(key: string, attrs: Partial<NodeAttrs>): void |
Merges attributes into an existing node. Throws NodeNotFoundError if key doesn't exist. |
hasNode |
(key: string): boolean |
Checks if a node exists. |
getNodeAttributes |
(key: string): NodeAttrs |
Returns node attributes. Throws NodeNotFoundError if key doesn't exist. |
Edge Mutations
| Method | Signature | Behavior |
|---|---|---|
addEdge |
(source: string, target: string, attrs?: EdgeAttrs): void |
Adds a directed edge. Throws NodeNotFoundError if either endpoint doesn't exist. Throws CycleError if the edge would create a cycle. Throws DuplicateEdgeError if an edge already exists between the same (source, target). |
removeEdge |
(source: string, target: string): void |
Removes an edge. No-op if the edge doesn't exist. |
hasEdge |
(source: string, target: string): boolean |
Checks if an edge exists between source and target. |
getEdgeAttributes |
(source: string, target: string): EdgeAttrs |
Returns edge attributes. Throws if edge doesn't exist. |
Call Graph Mutations
These are convenience methods specific to FlowGraph<CallNodeAttrs, CallEdgeAttrs>:
| Method | Signature | Behavior |
|---|---|---|
addCall |
(attrs: CallNodeAttrs): void |
Adds a call node. If attrs.parentRequestId is set, also creates a triggered edge from parent to child. |
addDependency |
(source: string, target: string): void |
Creates a depends_on edge. Validates both endpoints exist and the edge wouldn't create a cycle. |
updateStatus |
(requestId: string, status: CallStatus, extra?: Partial<CallNodeAttrs>): void |
Updates call status. Throws InvalidTransitionError on invalid transitions. |
updateCall |
(requestId: string, attrs: Partial<CallNodeAttrs>): void |
Partial merge of call attributes. |
removeCall |
(requestId: string): void |
Removes a call node and all attached edges. |
Operation Graph Mutations
| Method | Signature | Behavior |
|---|---|---|
addOperation |
(spec: OperationSpec): void |
Adds an operation node. Key is ${spec.namespace}.${spec.name}. |
addTypedEdge |
(source: string, target: string, attrs: { compatible: boolean; detail?: string }): void |
Adds a type-compatibility edge with edgeType: "typed". |
Query Methods
Graph Traversal
These methods delegate directly to graphology and graphology-dag:
| Method | Returns | Delegated To |
|---|---|---|
topologicalOrder() |
string[] |
graphology-dag.topologicalSort |
hasCycles() |
boolean |
graphology-dag.hasCycle (always false after validated construction) |
findCycles() |
string[][] |
graphology-dag.findCycle (debugging) |
ancestors(nodeId) |
string[] |
graphology-dag.ancestors |
descendants(nodeId) |
string[] |
graphology-dag.descendants |
predecessors(nodeId) |
string[] |
graph.inNeighbors |
successors(nodeId) |
string[] |
graph.outNeighbors |
reachableFrom(nodeIds) |
Set<string> |
Custom BFS/DFS traversal |
Call Graph Queries
| Method | Returns | Description |
|---|---|---|
filterByStatus(status) |
string[] |
Node keys with the given status |
getRoots() |
string[] |
Top-level call nodes (no parentRequestId) |
children(requestId) |
string[] |
Direct children via triggered edges |
duration(requestId) |
number |
completedAt - startedAt in ms |
lineage(requestId) |
string[] |
Ancestor chain from root to this call |
Serialization
| Method | Signature | Description |
|---|---|---|
export() |
FlowGraphSerialized<NodeAttrs, EdgeAttrs> |
Returns graphology native JSON format |
toJSON() |
FlowGraphSerialized<NodeAttrs, EdgeAttrs> |
Alias for export() |
toString() |
string |
JSON.stringify of export() |
Analysis Convenience Methods
The FlowGraph class exposes convenience methods that delegate to standalone analysis functions:
class FlowGraph {
// Delegates to analysis/topologicalOrder
topologicalOrder(): string[] { return _topologicalOrder(this._graph); }
// Delegates to analysis/hasCycles
hasCycles(): boolean { return _hasCycles(this._graph); }
// Delegates to analysis/validate
validate(): AnyValidationError[] { return _validate(this._graph); }
// Delegates to analysis/typeCompat
typeCompat(sourceKey: string, targetKey: string): TypeCompatResult {
const source = this.getNodeAttributes(sourceKey);
const target = this.getNodeAttributes(targetKey);
return _typeCompat(source.outputSchema, target.inputSchema);
}
}
Standalone functions are also available from @alkdev/flowgraph/analysis:
import { topologicalOrder, hasCycles, validateGraph, typeCompat } from "@alkdev/flowgraph/analysis";
Delegation Model
FlowGraph wraps a graphology DirectedGraph instance. It does NOT extend DirectedGraph:
class FlowGraph<NodeAttrs, EdgeAttrs> {
private _graph: DirectedGraph;
// Construction
constructor(options?: FlowGraphOptions) {
this._graph = new DirectedGraph({
type: "directed",
multi: false,
allowSelfLoops: false,
});
}
// Internal access for delegation
get graph(): DirectedGraph { return this._graph; }
}
What FlowGraph delegates
The FlowGraph class exposes only a subset of graphology's API — the methods that are meaningful for an enforced-DAG graph:
| Category | What FlowGraph does | What raw graphology provides |
|---|---|---|
| Node ops | addNode, removeNode, hasNode, getNodeAttributes, updateNode |
Full node CRUD + attributes |
| Edge ops | addEdge, removeEdge, hasEdge, getEdgeAttributes |
Full edge CRUD + attributes |
| Traversal | topologicalOrder, ancestors, descendants, predecessors, successors |
All graphology traversal methods |
| Queries | filterByStatus, getRoots, children, duration, lineage |
N/A (flowgraph-specific) |
| Analysis | typeCompat, validate, hasCycles |
N/A (flowgraph-specific) |
What FlowGraph does NOT expose
Methods that would violate DAG invariants or are unnecessary for flowgraph's use cases:
addUndirectedEdge— not applicable, all edges are directedaddEdgeWithKey— edge keys are deterministic (${source}->${target}), not user-specifiedmerge/mergeEdge— graph merging is not a supported operation (rebuild instead)import— useFlowGraph.fromJSON()which validates schema- Any
multi: trueorallowSelfLoops: trueoptions
Direct graphology access
Consumers who need graphology's full API can access the underlying graph via flowGraph.graph:
const graph = flowGraph.graph;
graph.forEachNode((node, attrs) => {
console.log(node, attrs);
});
This is an escape hatch. Direct graph mutation bypasses flowgraph's validation (cycle detection, duplicate checks). Use with caution.
Immutability Guarantees
| Method | Mutates? | Returns |
|---|---|---|
fromSpecs() |
Creates new graph | OperationGraph |
fromCallEvents() |
Creates new graph | CallGraph |
fromJSON() |
Creates new graph | FlowGraph |
addNode, addEdge, etc. |
Yes — mutates | void |
removeNode, removeEdge |
Yes — mutates | void |
updateNode, updateStatus |
Yes — mutates | void |
export(), toJSON() |
No — reads | Serialized data |
topologicalOrder(), ancestors(), etc. |
No — reads | Query results |
validate(), hasCycles() |
No — reads | Validation results |
typeCompat() |
No — reads | TypeCompatResult |
Key invariant: The operation graph produced by fromSpecs() is immutable after construction — no mutation methods are exposed. If the registry changes, rebuild the graph. The call graph produced by fromCallEvents() supports incremental mutation via addCall, updateStatus, and addDependency. The initial events populate the graph, and subsequent events update it.
Exports Map
| Sub-path | Key exports |
|---|---|
@alkdev/flowgraph |
FlowGraph, all public types |
@alkdev/flowgraph/graph |
FlowGraph, FlowGraphOptions |
@alkdev/flowgraph/analysis |
typeCompat, buildTypeEdges, validateGraph, validateTemplate, topologicalOrder, parallelGroups, criticalPath, reachableFrom |
@alkdev/flowgraph/schema |
OperationNodeAttrs, CallNodeAttrs, OperationEdgeAttrs, CallEdgeAttrs, TemplateEdgeAttrs, CallStatus, NodeStatus, EdgeType |
@alkdev/flowgraph/component |
Operation, Sequential, Parallel, Conditional, Map |
@alkdev/flowgraph/host |
GraphologyHostConfig, ReactiveHostConfig |
@alkdev/flowgraph/reactive |
WorkflowReactiveRoot, WorkflowNode, ReactiveContext |
@alkdev/flowgraph/error |
FlowgraphError, ConstructionError, CycleError, ValidationError, TypeIncompatError, InvalidTransitionError |
Constraints
- FlowGraph wraps, not extends, graphology — direct
DirectedGraphaccess is available via.graphbut bypasses validation. - DAG invariants enforced at construction time —
addEdgethrowsCycleErrorif the edge would create a cycle.hasCycles()should always returnfalseafter validated construction. - No parallel edges —
addEdgethrowsDuplicateEdgeErrorif an edge already exists between the same (source, target) pair. - No self-loops — enforced at the graphology level (
allowSelfLoops: false). - Edge keys are deterministic —
${source}->${target}format. No user-specified edge keys. - Operation graph is immutable after construction — no mutation methods are exposed after
fromSpecs(). If the registry changes, rebuild the graph. - Call graph supports incremental mutation —
addCall,updateStatus,addDependencyare the primary mutation paths.
Open Questions
-
Should
FlowGraphexpose graphology's traversal methods directly or only via convenience methods? Currently the plan is convenience methods that delegate. Direct graphology access via.graphis the escape hatch. But some consumers may find it inconvenient to go through.graph.forEachNode()instead offlowGraph.forEachNode(). -
Should the operation graph's
addTypedEdgebe auto-populated or manual? CurrentlyfromSpecs()callsbuildTypeEdges()which adds all type-compatibility edges.addTypedEdgeis for manual or incremental construction. ShouldaddOperationalso attempt auto-type-compat edge creation? -
Should
FlowGraphsupport multiple graph instances sharing analysis functions? Currently eachFlowGraphinstance owns its ownDirectedGraph. A future optimization could pool analysis functions across instances.
References
- Schema: schema.md — TypeBox schemas for all node/edge attribute types
- Operation graph: operation-graph.md — Static graph construction and queries
- Call graph: call-graph.md — Dynamic graph from call events
- Analysis: analysis.md — Type compatibility, validation, ordering
- Error handling: error-handling.md — Error hierarchy
- Build & distribution: build-distribution.md — Exports map and package structure