Getting Started
Overview
Section titled “Overview”The Flow package composes schema-defined functions into a validated, executable directed acyclic graph. Nodes select registered functions, edges connect numbered output and input handles, and compileFlow() returns diagnostics plus, for a valid graph, a StandardFnSchema that is itself a standard fn-sphere function.
The example below computes (a + b) * c:
import { arithmeticFns } from "@fn-sphere/core";import { compileFlow, type FlowSpec } from "@fn-sphere/flow";import { useMemo, useState } from "react";
const formula = { 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 compilation = compileFlow({ flow: formula, fnList: arithmeticFns });if (!compilation.valid) throw new Error("Invalid formula flow");const { compiled } = compilation;const runFormula = compiled.define.implement(compiled.implement);
const inputClass = "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";
export function FlowExample() { const [a, setA] = useState(1); const [b, setB] = useState(2); const [c, setC] = useState(3); const result = useMemo(() => runFormula(a, b, c), [a, b, c]);
return ( <div className="flex flex-col gap-4"> <div className="flex flex-wrap items-end gap-3"> {[ { label: "a", value: a, setValue: setA }, { label: "b", value: b, setValue: setB }, { label: "c", value: c, setValue: setC }, ].map(({ label, value, setValue }) => ( <label key={label} className="flex flex-col gap-1 text-sm"> <span className="font-medium text-gray-700 dark:text-gray-300"> {label} </span> <input type="number" value={value} onChange={(event) => setValue(Number(event.currentTarget.value))} className={inputClass} /> </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"> ({a} + {b}) × {c} = <strong>{String(result)}</strong> </div> </div> );}
export default FlowExample;Usage Summary
Section titled “Usage Summary”-
Install Flow and Core:
Terminal window pnpm add @fn-sphere/flow @fn-sphere/coreTerminal window npm install @fn-sphere/flow @fn-sphere/coreTerminal window yarn add @fn-sphere/flow @fn-sphere/core -
Define a version 1 flow:
import type { FlowSpec } from "@fn-sphere/flow";const flow = {version: 1,name: "add",nodes: [{ id: "input", type: "input" },{ id: "add", type: "fn", fnName: "add" },{ id: "output", type: "output" },],edges: [{id: "input-0-to-add-0",source: "input",sourceHandle: 0,target: "add",targetHandle: 0,},{id: "input-1-to-add-1",source: "input",sourceHandle: 1,target: "add",targetHandle: 1,},{id: "add-to-output",source: "add",sourceHandle: 0,target: "output",targetHandle: 0,},],} satisfies FlowSpec; -
Analyze the graph before compiling when you want structured diagnostics:
import { analyzeFlow } from "@fn-sphere/flow";import { arithmeticFns } from "@fn-sphere/core";const analysis = analyzeFlow({ flow, fnList: arithmeticFns });if (!analysis.valid) {console.error(analysis.diagnostics);}validbecomesfalseonly when diagnostics contain anerror. Unreachable function nodes are reported withseverity: "warning"; they do not prevent compilation. -
Compile and execute the flow:
import { compileFlow } from "@fn-sphere/flow";const result = compileFlow({ flow, fnList: arithmeticFns });if (!result.valid) throw new Error("Invalid flow");const add = result.compiled.define.implement(result.compiled.implement);add(1, 2); // 3
Nodes and Handles
Section titled “Nodes and Handles”A version 1 flow has exactly one input node and one output node. Function nodes use fnName to select an entry from fnList:
{ id: "sum", type: "fn", fnName: "add" }Handles are zero-based numbers. On an input node they identify flow arguments; on a function node, target handles identify tuple parameters. Function nodes currently expose their single result at source handle 0, and the output node accepts one edge at target handle 0.
Editing with React Flow
Section titled “Editing with React Flow”@fn-sphere/flow is the graph model and execution layer; React Flow is the optional canvas layer. React Flow stores positions and string handle IDs for editing, while the adapter below selects the semantic node fields, converts handles to numbers, and validates the result with flowSpecSchema.
Try moving nodes, deleting an edge, or reconnecting handles. Run graph analyzes the current canvas before compiling it, so invalid edits appear as structured diagnostics instead of runtime crashes.
import { arithmeticFns } from "@fn-sphere/core";import { compileFlow, flowSpecSchema, type FlowNodeSpec, type FlowSpec,} from "@fn-sphere/flow";import { addEdge, Background, Controls, ReactFlow, reconnectEdge, useEdgesState, useNodesState, type Connection, type Edge, type NodeTypes,} from "@xyflow/react";import "@xyflow/react/dist/style.css";import { useCallback, useState } from "react";import { FlowCanvasNodeView, type FlowCanvasNode } from "./react-flow-node";
const nodeTypes = { flow: FlowCanvasNodeView } satisfies NodeTypes;
const createNodes = (): FlowCanvasNode[] => [ { id: "input", type: "flow", position: { x: 0, y: 95 }, data: { flowType: "input", label: "Inputs", inputCount: 0, outputCount: 3, }, }, { id: "sum", type: "flow", position: { x: 210, y: 20 }, data: { flowType: "fn", fnName: "add", label: "Add", inputCount: 2, outputCount: 1, }, }, { id: "product", type: "flow", position: { x: 420, y: 95 }, data: { flowType: "fn", fnName: "multiply", label: "Multiply", inputCount: 2, outputCount: 1, }, }, { id: "output", type: "flow", position: { x: 630, y: 95 }, data: { flowType: "output", label: "Output", inputCount: 1, outputCount: 0, }, },];
const createEdges = (): Edge[] => [ { id: "a-to-sum", source: "input", sourceHandle: "0", target: "sum", targetHandle: "0", type: "smoothstep", }, { id: "b-to-sum", source: "input", sourceHandle: "1", target: "sum", targetHandle: "1", type: "smoothstep", }, { id: "sum-to-product", source: "sum", sourceHandle: "0", target: "product", targetHandle: "0", type: "smoothstep", }, { id: "c-to-product", source: "input", sourceHandle: "2", target: "product", targetHandle: "1", type: "smoothstep", }, { id: "product-to-output", source: "product", sourceHandle: "0", target: "output", targetHandle: "0", type: "smoothstep", },];
const numericHandle = (handle: string | null | undefined, edgeId: string) => { if (typeof handle !== "string" || !/^(0|[1-9]\d*)$/.test(handle)) { throw new Error(`Edge ${edgeId} requires a numeric handle ID.`); } return Number(handle);};
const toFlowSpec = (nodes: FlowCanvasNode[], edges: Edge[]): FlowSpec => { const flowNodes: FlowNodeSpec[] = nodes.map(({ id, data }) => data.flowType === "fn" ? { id, type: "fn", fnName: data.fnName ?? "" } : { id, type: data.flowType }, );
return flowSpecSchema.parse({ version: 1, name: "formula", nodes: flowNodes, edges: edges.map((edge) => ({ id: edge.id, source: edge.source, sourceHandle: numericHandle(edge.sourceHandle, edge.id), target: edge.target, targetHandle: numericHandle(edge.targetHandle, edge.id), })), });};
type DisplayDiagnostic = { severity: "error" | "warning"; text: string;};
export function FlowCanvasExample() { const [nodes, setNodes, onNodesChange] = useNodesState(createNodes()); const [edges, setEdges, onEdgesChange] = useEdgesState(createEdges()); const [inputs, setInputs] = useState([1, 2, 3]); const [result, setResult] = useState<unknown>(); const [diagnostics, setDiagnostics] = useState<DisplayDiagnostic[]>([]);
const onConnect = useCallback( (connection: Connection) => setEdges((current) => addEdge({ ...connection, type: "smoothstep" }, current), ), [setEdges], );
const onReconnect = useCallback( (oldEdge: Edge, newConnection: Connection) => setEdges((current) => reconnectEdge(oldEdge, newConnection, current)), [setEdges], );
const run = () => { try { const flow = toFlowSpec(nodes, edges); const compilation = compileFlow({ flow, fnList: arithmeticFns }); setDiagnostics( compilation.diagnostics.map((diagnostic) => ({ severity: diagnostic.severity, text: `${diagnostic.code}: ${diagnostic.message}`, })), ); if (!compilation.valid) { setResult(undefined); return; }
const { compiled } = compilation; const execute = compiled.define.implement(compiled.implement); setResult(execute(...inputs)); } catch (error) { setResult(undefined); setDiagnostics([ { severity: "error", text: error instanceof Error ? error.message : "Unable to read the graph.", }, ]); } };
const hasDiagnosticErrors = diagnostics.some( (diagnostic) => diagnostic.severity === "error", );
const reset = () => { setNodes(createNodes()); setEdges(createEdges()); setResult(undefined); setDiagnostics([]); };
return ( <div className="flex flex-col gap-4"> <div className="h-96 overflow-hidden rounded-lg border border-gray-300 dark:border-gray-600"> <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onReconnect={onReconnect} fitView > <Background /> <Controls /> </ReactFlow> </div>
<div className="flex flex-wrap items-end gap-3"> {inputs.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 = [...inputs]; next[index] = Number(event.currentTarget.value); setInputs(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> ))} <button type="button" onClick={run} className="rounded-md border border-blue-600 bg-blue-600 px-3 py-1.5 text-sm text-white hover:bg-blue-700" > Run graph </button> <button type="button" onClick={reset} className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700" > Reset graph </button> </div>
{result !== undefined && ( <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"> Result: <strong>{String(result)}</strong> </div> )} {diagnostics.length > 0 && ( <ul className={`m-0 rounded-md border px-8 py-3 text-sm ${ hasDiagnosticErrors ? "border-red-200 bg-red-50 text-red-900 dark:border-red-800 dark:bg-red-950 dark:text-red-100" : "border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-100" }`} > {diagnostics.map((diagnostic) => ( <li key={`${diagnostic.severity}:${diagnostic.text}`}> {diagnostic.text} </li> ))} </ul> )} </div> );}
export default FlowCanvasExample;import { Handle, Position, useUpdateNodeInternals, type Node, type NodeProps,} from "@xyflow/react";import { useEffect } from "react";
type FlowCanvasNodeData = { flowType: "input" | "fn" | "output"; label: string; fnName?: string; inputCount: number; outputCount: number;};
export type FlowCanvasNode = Node<FlowCanvasNodeData, "flow">;
const handleTop = (index: number, count: number) => `${((index + 1) / (count + 1)) * 100}%`;
export function FlowCanvasNodeView({ id, data, selected,}: NodeProps<FlowCanvasNode>) { const updateNodeInternals = useUpdateNodeInternals();
useEffect(() => { updateNodeInternals(id); }, [data.inputCount, data.outputCount, id, updateNodeInternals]);
return ( <div className={`min-w-28 rounded-lg border bg-white px-4 py-3 text-center shadow-sm dark:bg-gray-800 ${ selected ? "border-blue-500 ring-2 ring-blue-200 dark:border-blue-400 dark:ring-blue-900" : "border-gray-300 dark:border-gray-600" }`} > {Array.from({ length: data.inputCount }, (_, index) => ( <Handle key={`input-${index}`} id={String(index)} type="target" position={Position.Left} style={{ top: handleTop(index, data.inputCount) }} /> ))} <div className="text-sm font-semibold text-gray-900 dark:text-gray-100"> {data.label} </div> <div className="text-xs text-gray-500 dark:text-gray-400"> {data.flowType === "fn" ? data.fnName : data.flowType} </div> {Array.from({ length: data.outputCount }, (_, index) => ( <Handle key={`output-${index}`} id={String(index)} type="source" position={Position.Right} style={{ top: handleTop(index, data.outputCount) }} /> ))} </div> );}Schema Inference
Section titled “Schema Inference”Flow does not duplicate Zod schemas in the graph. Starting at the output node, compileFlow walks upstream through the function nodes and edges that contribute to the result. It reads that executable slice’s fixed tuple input and output schemas, checks its edges for compatibility, and derives the compiled function schema from its active input handles.
Active input handles must form a contiguous zero-based tuple. Unreachable branches do not add parameters to the compiled function.
Parsing Serialized Flows
Section titled “Parsing Serialized Flows”Use flowSpecSchema at JSON or storage boundaries:
import { flowSpecSchema } from "@fn-sphere/flow";
const flow = flowSpecSchema.parse(JSON.parse(serialized));Next Steps
Section titled “Next Steps”- Build a graph in the interactive Flow Playground.
- Learn how to diagnose and compose flows.