Validation and Composition
Analyze Before Compiling
Section titled “Analyze Before Compiling”analyzeFlow returns all detected graph diagnostics without throwing:
const analysis = analyzeFlow({ flow, fnList });
for (const diagnostic of analysis.diagnostics) { if (diagnostic.severity === "error") { console.error(diagnostic.code, diagnostic.message); } else { console.warn(diagnostic.code, diagnostic.message); }}Each diagnostic has an error or warning severity and can identify a nodeId, edgeId, or numeric handle. analysis.valid depends only on errors. compileFlow performs the same analysis and returns the same diagnostics.
Warnings remain available even when the flow is valid:
const warnings = analysis.diagnostics.filter( (diagnostic) => diagnostic.severity === "warning",);When an editor needs both diagnostics and a compiled function, call compileFlow to inspect the graph once. Invalid graphs return valid: false with diagnostics; valid graphs also include compiled:
import { compileFlow } from "@fn-sphere/flow";
const result = compileFlow({ flow, fnList });if (result.valid) { const run = result.compiled.define.implement(result.compiled.implement); // Reuse run when only input values change; recompile when the graph changes.}Current Graph Rules
Section titled “Current Graph Rules”A version 1 executable flow must satisfy these rules:
- Node IDs and edge IDs are unique.
- Every output-reachable
fnNameresolves to a function infnList. - There is exactly one input node and one output node.
- Output-reachable function schemas use fixed tuple inputs; variadic array inputs are unsupported.
- Every output-reachable function input handle has exactly one incoming edge.
- Each active flow input handle has one consumer, and active handles form a contiguous zero-based tuple.
- Function output and output-node handles are
0. - Connected schemas in the executable slice are compatible.
- Output-reachable function nodes form an acyclic graph.
- Unreachable function nodes produce warnings and are excluded from the compiled function.
Reuse a Compiled Flow
Section titled “Reuse a Compiled Flow”A valid result’s compiled is a StandardFnSchema, so it can be added to another flow’s function list. The outer graph references the inner flow by its compiled name:
const formula = compileFlow({ flow: formulaSpec, fnList: arithmeticFns });if (!formula.valid) throw new Error("Invalid formula flow");
const outer = compileFlow({ flow: outerSpec, fnList: [...arithmeticFns, formula.compiled],});The example composes (a + b) * c as one function node, then adds d:
import { arithmeticFns } from "@fn-sphere/core";import { compileFlow, type FlowSpec } from "@fn-sphere/flow";import { useMemo, useState } from "react";
const formulaSpec = { version: 1, name: "formula", nodes: [ { id: "input", type: "input" }, { id: "sum", type: "fn", fnName: "add" }, { id: "product", type: "fn", fnName: "multiply" }, { id: "output", type: "output" }, ], edges: [ { id: "a-to-sum", source: "input", sourceHandle: 0, target: "sum", targetHandle: 0, }, { id: "b-to-sum", source: "input", sourceHandle: 1, target: "sum", targetHandle: 1, }, { id: "sum-to-product", source: "sum", sourceHandle: 0, target: "product", targetHandle: 0, }, { id: "c-to-product", source: "input", sourceHandle: 2, target: "product", targetHandle: 1, }, { id: "product-to-output", source: "product", sourceHandle: 0, target: "output", targetHandle: 0, }, ],} satisfies FlowSpec;
const compileValid = (...args: Parameters<typeof compileFlow>) => { const compilation = compileFlow(...args); if (!compilation.valid) throw new Error("Invalid example flow"); return compilation.compiled;};
const formula = compileValid({ flow: formulaSpec, fnList: arithmeticFns });
const outerSpec = { version: 1, name: "formulaPlus", nodes: [ { id: "input", type: "input" }, { id: "formula", type: "fn", fnName: "formula" }, { id: "add", type: "fn", fnName: "add" }, { id: "output", type: "output" }, ], edges: [ { id: "a-to-formula", source: "input", sourceHandle: 0, target: "formula", targetHandle: 0, }, { id: "b-to-formula", source: "input", sourceHandle: 1, target: "formula", targetHandle: 1, }, { id: "c-to-formula", source: "input", sourceHandle: 2, target: "formula", targetHandle: 2, }, { id: "formula-to-add", source: "formula", sourceHandle: 0, target: "add", targetHandle: 0, }, { id: "d-to-add", source: "input", sourceHandle: 3, target: "add", targetHandle: 1, }, { id: "add-to-output", source: "add", sourceHandle: 0, target: "output", targetHandle: 0, }, ],} satisfies FlowSpec;
const outer = compileValid({ flow: outerSpec, fnList: [...arithmeticFns, formula],});const runOuter = outer.define.implement(outer.implement);
export function FlowCompositionExample() { const [values, setValues] = useState([1, 2, 3, 4]); const result = useMemo(() => runOuter(...values), [values]);
return ( <div className="flex flex-col gap-4"> <div className="flex flex-wrap gap-3"> {values.map((value, index) => ( <label key={index} className="flex flex-col gap-1 text-sm"> <span className="font-medium text-gray-700 dark:text-gray-300"> {String.fromCharCode(97 + index)} </span> <input type="number" value={value} onChange={(event) => { const next = [...values]; next[index] = Number(event.currentTarget.value); setValues(next); }} className="w-24 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm shadow-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" /> </label> ))} </div> <div className="rounded-md border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-950 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-100"> (({values[0]} + {values[1]}) × {values[2]}) + {values[3]} ={" "} <strong>{String(result)}</strong> </div> </div> );}
export default FlowCompositionExample;Serialized Boundary Validation
Section titled “Serialized Boundary Validation”FlowSpec is a compile-time type. Use flowSpecSchema to validate untrusted JSON before analysis or compilation:
const flow = flowSpecSchema.parse(JSON.parse(serialized));const analysis = analyzeFlow({ flow, fnList });