Graphs as data
Normative: MODEL.md §9.
A graph is always constructible from a serializable spec — normative from day one, because retrofitting it is expensive. This is the foundation for a drag-and-drop builder: a CRUD app over a JSON document plus a registry browser. Nothing in the engine knows the builder exists.
The stored form is plain data — this JSON is the whole graph:
{
"name": "support",
"channels": { "messages": { "reducer": "append" } },
"nodes": [{ "id": "agent", "type": "llm", "config": { "model": "claude-opus-4-8" } }],
"edges": [
{ "from": "__start__", "to": "agent" },
{ "from": "agent", "to": "__end__" }
]
}
Build a graph from that spec and serialize it straight back — the round-trip is a conformance test:
- TypeScript
- .NET (C#)
import { fromSpec, toSpec, START, END } from "@ilmek/core";
import type { GraphSpec, NodeRegistry } from "@ilmek/core";
import { deepStrictEqual } from "node:assert";
/** Maps a node `type` to a factory. DB-defined graphs reference these by name. */
const registry: NodeRegistry = {
llm: (config) => async (state, ctx) => ({ messages: [`${config.model} replies`] }),
};
/** The stored document — plain data, no executable text. */
const spec: GraphSpec = {
name: "support",
channels: { messages: { reducer: "append" } },
nodes: [{ id: "agent", type: "llm", config: { model: "claude-opus-4-8" } }],
edges: [
{ from: START, to: "agent" },
{ from: "agent", to: END },
],
};
const g = fromSpec(spec, registry).compile();
deepStrictEqual(toSpec(g), spec); // round-trip is a conformance test
using Ilmek;
// Maps a node `type` to a builder. DB-defined graphs reference these by name.
var registry = new Dictionary<string, NodeBuilder>
{
["llm"] = config => (state, ctx) =>
new Dictionary<string, object?> { ["messages"] = new[] { $"{config["model"]} replies" } },
};
// The stored document — plain data, no executable text.
var spec = new GraphSpec
{
Name = "support",
Channels = new Dictionary<string, SpecChannel> { ["messages"] = new("append") },
Nodes = new[] { new SpecNode("agent", "llm", new Dictionary<string, object?> { ["model"] = "claude-opus-4-8" }) },
Edges = new[] { new SpecEdge(Graph.Start, "agent"), new SpecEdge("agent", Graph.End) },
};
var g = Spec.FromSpec(spec, registry).Compile();
var rebuilt = Spec.ToSpec(g); // GraphSpec, value-equal to spec — a conformance test
The node registry
type resolves against a node registry: type => (config) => nodeFn.
Code-defined graphs register anonymous types; DB-defined graphs reference
registered ones. The engine cannot tell the difference — the same executor
runs both.
Two rules keep stored graphs safe
- A stored spec never carries executable text. A conditional
whenis a declarative predicate ({ channel: "intent", eq: "buy" }), not code — there is no eval path. Code-defined graphs may pass a real function; the serializer refuses to emit one. toSpec()refuses to serialize what a document cannot honestly hold — a code router, an anonymous node type, a hand-written guard. If it round-trips, it is safe to store; if it cannot, you find out at serialize time, not at load time.
{
"name": "support-agent",
"channels": { "messages": { "reducer": "append" }, "cart": { "reducer": "last_write" } },
"nodes": [
{ "id": "agent", "type": "llm", "config": { "model": "claude-opus-4-8", "tools": ["search"] } },
{ "id": "checkout", "type": "http", "config": { "url": "…" } }
],
"edges": [
{ "from": "__start__", "to": "agent" },
{ "from": "agent", "to": "checkout", "when": { "channel": "intent", "eq": "buy" } },
{ "from": "checkout", "to": "__end__" }
]
}
Because the round-trip compile(spec) → toSpec() is a conformance test, a builder
that reads and writes this document can trust the engine to preserve exactly what
it stored.