Overview
A core design principle of @statelyai/graph is that graphs are plain JSON-serializable objects . No classes, no methods, no functions—just data.
This means you can:
Serialize to JSON and back without losing information
Store graphs in databases, files, or localStorage
Send graphs over the network
Use graphs with any framework or library
Inspect graphs in debugger without custom formatters
Plain Objects
Graphs are plain JavaScript objects that satisfy the Graph interface:
import type { Graph } from '@statelyai/graph' ;
// This is a valid graph (no factory function needed)
const graph : Graph = {
id: 'my-graph' ,
type: 'directed' ,
initialNodeId: null ,
nodes: [
{
type: 'node' ,
id: 'a' ,
label: 'Node A' ,
data: undefined
},
{
type: 'node' ,
id: 'b' ,
label: 'Node B' ,
data: undefined
}
],
edges: [
{
type: 'edge' ,
id: 'e1' ,
sourceId: 'a' ,
targetId: 'b' ,
label: 'to B' ,
data: undefined
}
],
data: undefined
};
While you can create graphs with plain objects, using createGraph() is recommended because it handles default values and validation.
JSON Serialization
Graphs serialize to JSON without any special handling:
import { createGraph } from '@statelyai/graph' ;
const graph = createGraph ({
id: 'example' ,
nodes: [
{ id: 'a' , label: 'Node A' , data: { value: 1 } },
{ id: 'b' , label: 'Node B' , data: { value: 2 } }
],
edges: [
{ id: 'e1' , sourceId: 'a' , targetId: 'b' , data: { weight: 10 } }
]
});
// Serialize to JSON string
const json = JSON . stringify ( graph , null , 2 );
// Deserialize from JSON string
const restored : Graph = JSON . parse ( json );
// Restored graph is identical
console . log ( restored . id ); // 'example'
console . log ( restored . nodes . length ); // 2
Here’s what a serialized graph looks like:
{
"id" : "example" ,
"type" : "directed" ,
"initialNodeId" : null ,
"nodes" : [
{
"type" : "node" ,
"id" : "a" ,
"label" : "Node A" ,
"data" : { "value" : 1 }
},
{
"type" : "node" ,
"id" : "b" ,
"label" : "Node B" ,
"data" : { "value" : 2 }
}
],
"edges" : [
{
"type" : "edge" ,
"id" : "e1" ,
"sourceId" : "a" ,
"targetId" : "b" ,
"label" : "" ,
"data" : { "weight" : 10 }
}
],
"data" : null
}
No Hidden State
Unlike class-based graph libraries, there’s no hidden state:
import { createGraph , getNode } from '@statelyai/graph' ;
const graph = createGraph ({
nodes: [{ id: 'a' }]
});
// Everything is visible in the object
console . log ( graph );
// {
// id: '',
// type: 'directed',
// nodes: [{ type: 'node', id: 'a', label: '', data: undefined }],
// edges: [],
// ...
// }
// Functions are separate from data
const node = getNode ( graph , 'a' );
The library uses transparent WeakMap-based indexing for performance. Indexes are built on-demand and never serialized. When you deserialize a graph, indexes are automatically rebuilt on first access.
Working with GraphInstance
The GraphInstance class is just a wrapper around a plain graph:
import { GraphInstance } from '@statelyai/graph' ;
const instance = new GraphInstance ({
id: 'test' ,
nodes: [{ id: 'a' }]
});
// The underlying graph is a plain object
const plainGraph = instance . graph ;
console . log ( plainGraph );
// { id: 'test', type: 'directed', nodes: [...], edges: [] }
// toJSON() returns the plain graph
const json = JSON . stringify ( instance );
const parsed = JSON . parse ( json );
// Wrap the restored graph
const restored = GraphInstance . from ( parsed );
Data Constraints
Since graphs must be JSON-serializable, avoid storing:
Functions
Class instances (unless they have toJSON())
Symbols
undefined in arrays
Circular references
Valid Data
const graph = createGraph ({
nodes: [
{
id: 'a' ,
data: {
// All JSON-serializable
name: 'Alice' ,
age: 30 ,
active: true ,
tags: [ 'user' , 'admin' ],
metadata: { role: 'developer' }
}
}
]
});
Invalid Data
// Don't do this - functions aren't serializable
const graph = createGraph ({
nodes: [
{
id: 'a' ,
data: {
onClick : () => console . log ( 'clicked' ), // ❌ Function
date: new Date (), // ⚠️ Serializes as string
regex: /test/ , // ❌ Loses type
map: new Map () // ❌ Serializes as {}
}
}
]
});
Date objects serialize to ISO strings. After deserialization, you’ll need to manually convert them back:const graph = createGraph ({
nodes: [{ id: 'a' , data: { createdAt: new Date () } }]
});
const json = JSON . stringify ( graph );
const restored = JSON . parse ( json );
// Convert back to Date
const createdAt = new Date ( restored . nodes [ 0 ]. data . createdAt );
Validation
The library includes Zod schemas for validation:
import { GraphSchema , NodeSchema , EdgeSchema } from '@statelyai/graph/schemas' ;
const json = `{
"id": "test",
"type": "directed",
"initialNodeId": null,
"nodes": [],
"edges": [],
"data": null
}` ;
const data = JSON . parse ( json );
const validated = GraphSchema . parse ( data );
// Throws if invalid
Schema Definitions
The schemas validate the core structure:
// From src/schemas.ts
export const NodeSchema = z . object ({
type: z . literal ( 'node' ),
id: z . string (),
parentId: z . string (). nullable (),
initialNodeId: z . string (). nullable (),
label: z . string (),
data: z . any ()
});
export const EdgeSchema = z . object ({
type: z . literal ( 'edge' ),
id: z . string (),
sourceId: z . string (),
targetId: z . string (),
label: z . string (),
data: z . any ()
});
export const GraphSchema = z . object ({
id: z . string (),
type: z . enum ([ 'directed' , 'undirected' ]),
initialNodeId: z . string (). nullable (),
nodes: z . array ( NodeSchema ),
edges: z . array ( EdgeSchema ),
data: z . any ()
});
The schemas validate the core required fields but don’t validate optional visual properties like x, y, width, height, shape, color, or style.
Storage Examples
LocalStorage
import { createGraph } from '@statelyai/graph' ;
import type { Graph } from '@statelyai/graph' ;
const graph = createGraph ({
id: 'workflow' ,
nodes: [{ id: 'a' }, { id: 'b' }],
edges: [{ id: 'e1' , sourceId: 'a' , targetId: 'b' }]
});
// Save to localStorage
localStorage . setItem ( 'my-graph' , JSON . stringify ( graph ));
// Load from localStorage
const json = localStorage . getItem ( 'my-graph' );
if ( json ) {
const restored : Graph = JSON . parse ( json );
console . log ( restored . id ); // 'workflow'
}
File System (Node.js)
import { writeFileSync , readFileSync } from 'fs' ;
import { createGraph } from '@statelyai/graph' ;
import type { Graph } from '@statelyai/graph' ;
const graph = createGraph ({
id: 'saved-graph' ,
nodes: [{ id: 'a' }]
});
// Save to file
writeFileSync ( 'graph.json' , JSON . stringify ( graph , null , 2 ));
// Load from file
const json = readFileSync ( 'graph.json' , 'utf-8' );
const restored : Graph = JSON . parse ( json );
Database (MongoDB example)
import { MongoClient } from 'mongodb' ;
import { createGraph } from '@statelyai/graph' ;
import type { Graph } from '@statelyai/graph' ;
const client = new MongoClient ( 'mongodb://localhost:27017' );
const db = client . db ( 'myapp' );
const collection = db . collection ( 'graphs' );
const graph = createGraph ({
id: 'db-graph' ,
nodes: [{ id: 'a' }]
});
// MongoDB stores plain objects directly
await collection . insertOne ( graph );
// Retrieve from database
const restored = await collection . findOne ({ id: 'db-graph' }) as Graph | null ;
if ( restored ) {
console . log ( restored . nodes . length );
}
REST API
import { createGraph } from '@statelyai/graph' ;
import type { Graph } from '@statelyai/graph' ;
const graph = createGraph ({
id: 'api-graph' ,
nodes: [{ id: 'a' }]
});
// Send via POST
await fetch ( '/api/graphs' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( graph )
});
// Receive via GET
const response = await fetch ( '/api/graphs/api-graph' );
const restored : Graph = await response . json ();
Indexing
The library uses WeakMap-based indexes for fast lookups:
Indexes are built on-demand (lazy)
Indexes are cached until the graph is mutated
Indexes are never serialized
After deserialization, indexes rebuild automatically on first access
import { createGraph , getNode } from '@statelyai/graph' ;
const graph = createGraph ({
nodes: [{ id: 'a' }, { id: 'b' }]
});
// First lookup builds index
getNode ( graph , 'a' ); // Builds index
// Subsequent lookups use cached index
getNode ( graph , 'b' ); // Uses cached index (O(1))
// Serialize and restore
const json = JSON . stringify ( graph );
const restored = JSON . parse ( json );
// First lookup rebuilds index
getNode ( restored , 'a' ); // Rebuilds index
Mutation Invalidation
Mutations that change the arrays invalidate indexes:
import { createGraph , addNode , getNode } from '@statelyai/graph' ;
const graph = createGraph ({
nodes: [{ id: 'a' }]
});
getNode ( graph , 'a' ); // Builds index
addNode ( graph , { id: 'b' }); // Invalidates index
getNode ( graph , 'b' ); // Rebuilds index
Invalidation is smart: adding nodes invalidates node indexes but not edge indexes. This keeps performance high even with frequent mutations.
Type Safety After Deserialization
TypeScript can’t verify JSON at runtime. Use type assertions carefully:
import type { Graph } from '@statelyai/graph' ;
import { GraphSchema } from '@statelyai/graph/schemas' ;
const json = localStorage . getItem ( 'graph' );
if ( json ) {
// Option 1: Type assertion (unsafe)
const graph : Graph = JSON . parse ( json );
// Option 2: Validation with Zod (safe)
const data = JSON . parse ( json );
const graph = GraphSchema . parse ( data ); // Throws if invalid
// Option 3: Type guard (safest)
const data = JSON . parse ( json );
if ( isValidGraph ( data )) {
const graph : Graph = data ;
}
}
function isValidGraph ( data : unknown ) : data is Graph {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'type' in data &&
'nodes' in data &&
'edges' in data &&
Array . isArray (( data as any ). nodes ) &&
Array . isArray (( data as any ). edges )
);
}
Next Steps
Graphs Back to core graph concepts
Formats Export to other graph formats
Operations Mutate graphs with add/update/delete
Graph Creation Create new graphs with factory functions