@fbp/types
v0.3.0
Published
Flow-Based Programming schemas and converters in TypeScript
Readme
@fbp/types
Flow-Based Programming schemas spec
install
npm install @fbp/typesGraphSchemata Specification
This document describes GraphSchemata, a Houdini-inspired, merkle-friendly graph specification for composing systems out of nested nodes, explicit ports, and deterministic structure.
The core goals are:
- Explicitness over magic
- Composable subgraphs (“folders”)
- Stable, content-addressable structure
- Clear separation between dataflow and configuration
- No implicit behavior (no hidden lanes, no implicit merges)
Core Concepts
Everything is a Node
The entire document is a single Node. Nodes may contain other nodes and edges, forming nested graphs (subnets).
A node becomes a subnet simply by containing:
nodes[]edges[]
There is no separate “graph” type.
Identity Model
- Node identity = name within parent scope
- Names may be hierarchical paths by convention (
some/subnet/node) - Rename or move = identity change
- Tooling is responsible for rewriting references
This model is intentionally compatible with merkle / content-addressable versioning.
Node Structure
Each node has a public signature and optional internal structure.
Public Signature (Contract)
Defined directly on the node:
inputs[]— dataflow inputsoutputs[]— dataflow outputsprops[]— configuration parameters
This signature is the public API of the node.
For leaf nodes, the signature may be derived from a registry. For subnets, the signature is explicitly declared and editable.
Ports (No Lanes)
Ports are:
- Named
- Typed
- Singular
There is no lane or index dimension.
Consequences
Branching uses multiple named output ports
- e.g.
true,false,error,caseA
- e.g.
Fan-in is never implicit
- Multiple values must be combined via explicit nodes (
Merge,Collect, etc.)
- Multiple values must be combined via explicit nodes (
Inputs always match their declared type
This mirrors Houdini’s “typed sockets” philosophy.
Props vs Dataflow
Props (props) are not dataflow.
- They configure node behavior
- They are not connected by edges
- They may reference other parameters or graph paths via
Ref
Example parameter value:
{ "ref": "../config/apiKey" }Breaking refs on rename is expected and accepted.
Note: You may optionally choose to expose parameters visually via
@propsboundary ports, but props remain a configuration contract (not implicit dataflow).
Edges
Edges connect output ports → input ports.
A.outputs.out → B.inputs.inEdge Rules
Edges are in-scope only
EdgeEndpoint.nodemust reference a sibling node (within the samenodes[]array of the containing subnet)
No cross-scope or implicit parent/child wiring
No implicit fan-in
No implicit transformations
If logic is needed, insert a node.
Channels
Edges may optionally specify a channel.
{
"src": { "node": "A", "port": "out" },
"dst": { "node": "B", "port": "in" },
"channel": "error"
}Channel Semantics
channelis a namespace, not a typeDefault channel is
"main"Omit
channelwhen using"main"Non-main channels are for:
- error routing
- control/dependency edges
- metadata propagation
- future extensions
Channels do not imply different data types or execution unless the engine defines them.
Subnets (Nested Graphs)
A subnet is just a node with children.
Public Interface
A subnet’s public interface is defined by its own:
inputsoutputsprops
This is the canonical contract. Nothing is inferred from internal wiring.
Changing this interface is a breaking change.
Boundary Adapter Nodes (UI-Only)
To make subnets usable visually, tooling generates boundary adapter nodes inside subnets.
These nodes are structural adapters: they are always in sync with the subnet signature, do not add logic, and may be regenerated deterministically. They exist so internal edges have stable anchors.
Boundary Node Kinds
graphInput— exposes subnet inputs inside the subnetgraphOutput— collects subnet outputs inside the subnetgraphProp— exposes subnet props inside the subnet
Canonical Boundary Convention
Inside every subnet scope, tooling should ensure the presence of the following reserved boundary nodes:
@in(kind:graphInput)@out(kind:graphOutput)@props(kind:graphProp)
Each boundary node exposes one port per declared signature entry:
@inexposes outputs with port names matchinginputs[].name@outexposes inputs with port names matchingoutputs[].name@propsexposes outputs with port names matchingprops[].name
Example
Given subnet signature:
{
"inputs": [{ "name": "users", "type": "User[]" }],
"outputs": [{ "name": "adults", "type": "User[]" }],
"props": [{ "name": "minAge", "type": "number", "value": 18 }]
}Inside the subnet, edges can wire to boundary ports like:
{
"edges": [
{
"src": { "node": "@in", "port": "users" },
"dst": { "node": "filter", "port": "items" }
},
{
"src": { "node": "@props", "port": "minAge" },
"dst": { "node": "filter", "port": "minAge" }
},
{
"src": { "node": "filter", "port": "out" },
"dst": { "node": "@out", "port": "adults" }
}
]
}Generation Rules
Boundary nodes are generated deterministically from the subnet signature:
| Signature Element | Boundary Node | Port Direction | Boundary Port Name |
| ----------------- | ------------- | -------------- | ------------------ |
| inputs[i] | @in | output | inputs[i].name |
| outputs[i] | @out | input | outputs[i].name |
| props[i] | @props | output | props[i].name |
Boundary nodes may be regenerated at any time and must not affect identity.
Reserved Names / Collision Rules
To avoid collisions with user-authored nodes:
- Node names starting with
@are reserved for system/boundary nodes. - Users should not create nodes whose name begins with
@.
This reservation applies to node names only — port names such as users, adults, minAge remain freely chosen (subject to uniqueness constraints within their respective lists).
Groups (UI Only)
groups[] are optional UI helpers for organizing nodes visually.
They have no semantic meaning and should be ignored by execution and hashing if desired.
Metadata
meta exists for layout, annotations, and editor state:
- coordinates
- comments
- UI hints
It may be partially or fully excluded from hashing depending on your canonicalization rules.
Validation Rules (Normative)
Node names must be unique within a scope
Node names starting with
@are reserved for system/boundary nodesPort names must be unique within:
inputsoutputsprops
Edges must reference existing ports
Fan-in requires explicit merge nodes
Branching requires explicit output ports
Boundary nodes must match declared signatures 1:1 (ports mirror the subnet signature)
Canonicalization & Merkle Hashing
Recommended practices:
- Sort
inputs,outputs,propsby name - Sort
nodesby name - Sort
edgesdeterministically - Exclude UI-only metadata if desired
- Regenerate boundary nodes deterministically (
@in,@out,@props)
This ensures equivalent graphs hash identically.
Design Philosophy (Summary)
- No lanes
- No inference
- No hidden behavior
- Folders are graphs
- Ports are contracts
- Edges are explicit
- UI is projection, not truth
If you understand Houdini networks, this system should feel immediately natural.
