JSON-LD Export and Import
What It Is
JSON-LD is Dagonizer's workflow document format, not a reporting export. DAGBuilder.build() returns a JSON-LD-shaped object, DAGDocument.load() validates external JSON-LD into that object, and DAGDocument.serialize() writes it back out for storage or transport.
Every DAG carries @context, @id, and @type, so the same file can be consumed by Dagonizer, checked by JSON Schema, rendered as a graph, or inspected by RDF tooling. The DAG @id is the registry identity; name is the display and observability label.
How It Works
DAGBuilder.build() returns the same JSON-LD-shaped object that DAGDocument.serialize, DAGDocument.load, schema validation, visualization, and the dispatcher consume. Every placement carries @type, every DAG carries @context and @id, and the graph can round-trip without losing execution semantics.
Dagonizer DAGs are JSON-LD 1.1 documents. There is no separate wire format or projection layer. The object DAGBuilder.build() returns is the same object the engine consumes and the same object that round-trips through DAGDocument.serialize and DAGDocument.load. Every DAG carries @context, @id, and @type so RDF stores, schema validators, and generic JSON-LD processors read the shape natively without an adapter.
Diagrams, Examples, and Outputs
Example 03 starts from a JSON-LD string, loads it through DAGDocument.load, and executes the resulting DAG. The diagram is generated from that loaded object:
const dagJson = JSON.stringify({
'@context': DAG_CONTEXT,
'@id': 'urn:noocodec:dag:from-json',
'@type': 'DAG',
'name': 'from-json',
'version': '1',
'entrypoints': { 'main': 'urn:noocodec:dag:from-json/node/echo' },
'nodes': [
{
'@id': 'urn:noocodec:dag:from-json/node/echo',
'@type': 'SingleNode',
'name': 'echo',
'node': 'urn:noocodec:node:echo',
'outputs': { 'success': 'urn:noocodec:dag:from-json/node/end' },
},
{
'@id': 'urn:noocodec:dag:from-json/node/end',
'@type': 'TerminalNode',
'name': 'end',
'outcome': 'completed',
},
],
});// DAGDocument.load() throws DAGError (code VALIDATION_ERROR) if JSON is malformed or schema fails.
export const dag = DAGDocument.load(dagJson);Example 03 loaded JSON-LD DAG
2 placements{
"@context": {
"@version": 1.1,
"name": {
"@id": "https://noocodec.dev/ontology/dag/name"
},
"version": {
"@id": "https://noocodec.dev/ontology/dag/version"
},
"entrypoints": {
"@id": "https://noocodec.dev/ontology/dag/entrypoints",
"@container": "@index"
},
"nodes": {
"@id": "https://noocodec.dev/ontology/dag/nodes",
"@container": "@set"
},
"outputs": {
"@id": "https://noocodec.dev/ontology/dag/outputs"
},
"node": {
"@id": "https://noocodec.dev/ontology/dag/node"
},
"dag": {
"@id": "https://noocodec.dev/ontology/dag/dag"
},
"body": {
"@id": "https://noocodec.dev/ontology/dag/body"
},
"source": {
"@id": "https://noocodec.dev/ontology/dag/source"
},
"sources": {
"@id": "https://noocodec.dev/ontology/dag/sources",
"@container": "@index"
},
"itemKey": {
"@id": "https://noocodec.dev/ontology/dag/itemKey"
},
"execution": {
"@id": "https://noocodec.dev/ontology/dag/execution"
},
"concurrency": {
"@id": "https://noocodec.dev/ontology/dag/concurrency"
},
"throttle": {
"@id": "https://noocodec.dev/ontology/dag/throttle"
},
"reservoir": {
"@id": "https://noocodec.dev/ontology/dag/reservoir"
},
"gather": {
"@id": "https://noocodec.dev/ontology/dag/gather"
},
"dagReference": {
"@id": "https://noocodec.dev/ontology/dag/dagReference",
"@type": "@id"
},
"DagReference": {
"@id": "https://noocodec.dev/ontology/dag/DagReference"
},
"from": {
"@id": "https://noocodec.dev/ontology/dag/from"
},
"path": {
"@id": "https://noocodec.dev/ontology/dag/path"
},
"candidates": {
"@id": "https://noocodec.dev/ontology/dag/candidates",
"@container": "@set"
},
"candidateDag": {
"@id": "https://noocodec.dev/ontology/dag/candidateDag",
"@type": "@id"
},
"selectedDag": {
"@id": "https://noocodec.dev/ontology/dag/selectedDag",
"@type": "@id"
},
"resultField": {
"@id": "https://noocodec.dev/ontology/dag/resultField"
},
"policy": {
"@id": "https://noocodec.dev/ontology/dag/policy"
},
"reducer": {
"@id": "https://noocodec.dev/ontology/dag/reducer"
},
"outcome": {
"@id": "https://noocodec.dev/ontology/dag/outcome"
},
"phase": {
"@id": "https://noocodec.dev/ontology/dag/phase"
},
"stateMapping": {
"@id": "https://noocodec.dev/ontology/dag/stateMapping"
},
"container": {
"@id": "https://noocodec.dev/ontology/dag/container"
},
"DAG": {
"@id": "https://noocodec.dev/ontology/dag/DAG"
},
"Placement": {
"@id": "https://noocodec.dev/ontology/dag/Placement"
},
"SingleNode": {
"@id": "https://noocodec.dev/ontology/dag/SingleNode"
},
"ScatterNode": {
"@id": "https://noocodec.dev/ontology/dag/ScatterNode"
},
"EmbeddedDAGNode": {
"@id": "https://noocodec.dev/ontology/dag/EmbeddedDAGNode"
},
"GatherNode": {
"@id": "https://noocodec.dev/ontology/dag/GatherNode"
},
"TerminalNode": {
"@id": "https://noocodec.dev/ontology/dag/TerminalNode"
},
"PhaseNode": {
"@id": "https://noocodec.dev/ontology/dag/PhaseNode"
}
},
"@id": "urn:noocodec:dag:from-json",
"@type": "DAG",
"name": "from-json",
"version": "1",
"entrypoints": {
"main": "urn:noocodec:dag:from-json/node/echo"
},
"nodes": [
{
"@id": "urn:noocodec:dag:from-json/node/echo",
"@type": "SingleNode",
"name": "echo",
"node": "urn:noocodec:node:echo",
"outputs": {
"success": "urn:noocodec:dag:from-json/node/end"
}
},
{
"@id": "urn:noocodec:dag:from-json/node/end",
"@type": "TerminalNode",
"name": "end",
"outcome": "completed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% from-json (v1)
entry_main(["main"])
entry_main --> urn_noocodec_dag_from-json/node/echo
urn_noocodec_dag_from-json/node/echo["echo"]
urn_noocodec_dag_from-json/node/echo -->|success| urn_noocodec_dag_from-json/node/end
urn_noocodec_dag_from-json/node/end((("end")))Use the runnable pages and references around this one:
- Example 03: Tool Schemas shows the JSON-LD literal, the load boundary, and the run output.
- DAGBuilder shows the code-authoring path that produces the same shape.
- Schema and JSON Loading explains the Ajv validation surface.
- Visualization shows how JSON-LD DAGs render to Mermaid on guide pages and Cytoscape on runnable demo pages.
What It Lets You Do
Use when
Use JSON-LD when a DAG must leave TypeScript source: plugin packages, persisted workflow definitions, docs diagrams, runtime loading, graph tooling, or cross-service transport. The JSON-LD document is the canonical assembly, not an export-only artifact.
Code Samples
Importing
DAGDocument.load(json) parses and validates a JSON-LD string. It is the single ingest boundary for external input. unknown enters here and exits as a fully-typed DAG:
// DAGDocument.load() throws DAGError (code VALIDATION_ERROR) if JSON is malformed or schema fails.
export const dag = DAGDocument.load(dagJson);DAGDocument.load throws ValidationError for:
- Malformed JSON (delegates to
JSON.parse). - Schema-noncompliant input (validates against
DAGSchemavia Ajv 2020-12). - Missing required fields (
@context,@id,@type,name,version,entrypoints,nodes). - Invalid
@typediscriminator on any placement.
Details for Nerds
The canonical shape
A DAG document carries these top-level fields:
@context. The canonical Dagonizer JSON-LD context inlined as an object literal. The full context is exported from@studnicky/dagonizerasDAG_CONTEXT(source:packages/dagonizer/src/entities/dag/DAG.ts).DAGBuilder.build()embeds it verbatim.@id. IRI identifier for the DAG document. This is the registry key after@contextexpansion. Convention:urn:noocodec:dag:<slug>or a project-owned HTTPS IRI.@type. RDF class."DAG"for the document; one of"SingleNode","ScatterNode","EmbeddedDAGNode","TerminalNode", or"PhaseNode"for placements.name,version,entrypoints.nameis display/observability text;entrypointspoint at placement IRIs.nodes. Array of placement objects, each with its own@idand@type. Placement@idvalues are route targets.
Example 03 embeds a full JSON-LD DAG as a string and feeds it through the ingest boundary:
const dagJson = JSON.stringify({
'@context': DAG_CONTEXT,
'@id': 'urn:noocodec:dag:from-json',
'@type': 'DAG',
'name': 'from-json',
'version': '1',
'entrypoints': { 'main': 'urn:noocodec:dag:from-json/node/echo' },
'nodes': [
{
'@id': 'urn:noocodec:dag:from-json/node/echo',
'@type': 'SingleNode',
'name': 'echo',
'node': 'urn:noocodec:node:echo',
'outputs': { 'success': 'urn:noocodec:dag:from-json/node/end' },
},
{
'@id': 'urn:noocodec:dag:from-json/node/end',
'@type': 'TerminalNode',
'name': 'end',
'outcome': 'completed',
},
],
});Placement @ids typically nest under the DAG's URN: urn:noocodec:dag:demo/node/transform.
@type vocabulary
Six placement classes plus the document class:
@type | Role |
|---|---|
DAG | Top-level document |
SingleNode | One registered node, routed by named outputs |
ScatterNode | Fork over a source array: one clone per item, run a body in each clone, emit clone outcome records, route on aggregate outcome |
GatherNode | Fan in records from one or more producer placements and fold them into parent state |
EmbeddedDAGNode | Invoke a nested registered DAG at cardinality 1, with optional stateMapping to copy fields in and out |
TerminalNode | Explicit terminus with outcome of 'completed' or 'failed' |
PhaseNode | Lifecycle-attached pre or post placement |
Exporting
DAGDocument.serialize(dag) produces pretty-printed JSON (2-space indent):
// Write the pretty JSON to disk; read it back and re-validate at load time.
await fs.writeFile('/tmp/dag-json-ld.json', DAGDocument.serialize(original));
const loadedFromFile = DAGDocument.load(
await fs.readFile('/tmp/dag-json-ld.json', 'utf8'),
);
process.stdout.write(`loaded from file: ${loadedFromFile.name}\n`); // 'demo'DAGDocument.serialize(dag) produces pretty JSON for storage or inspection:
/**
* json-ld: DAGDocument round-trip and persistence patterns.
*
* Demonstrates the full JSON-LD lifecycle:
* 1. Build a DAG via DAGBuilder
* 2. Serialize to JSON (DAGDocument.serialize)
* 3. Load back from a JSON string (DAGDocument.load)
* 4. Persistence: file on disk, simulated database row
*
* The serialized document is a JSON-LD 1.1 document with @context, @id,
* and @type. DAGDocument.load is the only DAG document ingest boundary;
* it validates against DAGSchema via Ajv 2020-12.
*
* Run: npx tsx examples/json-ld.ts
*/
import * as fs from 'node:fs/promises';
import {
Batch,
DAGBuilder,
Dagonizer,
DAGIdentity,
MonadicNode,
NodeOutput,
NodeStateBase,
RoutedBatch,
} from '@studnicky/dagonizer';
import { DAGDocument } from '@studnicky/dagonizer/dag';
import type { SchemaObjectType } from '@studnicky/dagonizer';
// ---------------------------------------------------------------------------
// Node: a minimal transform node for the demo DAG
// ---------------------------------------------------------------------------
class TransformNode extends MonadicNode<NodeStateBase, 'success'> {
readonly name = 'transform';
readonly '@id' = 'urn:noocodec:node:transform';
readonly outputs = ['success'] as const;
override get outputSchema(): Record<'success', SchemaObjectType> {
return { 'success': { 'type': 'object' } };
}
override async execute(batch: Batch<NodeStateBase>) {
for (const item of batch) {
item.state.setMetadata('transformed', true);
}
return RoutedBatch.create(NodeOutput.create('success').output, batch);
}
}
// ---------------------------------------------------------------------------
// Round-trip: build → serialize → load
// ---------------------------------------------------------------------------
// #region round-trip
// Build a DAG via DAGBuilder — the canonical JSON-LD object.
const dagIri = 'urn:noocodec:dag:demo' as const;
const placement = (placementIdentifier: string): string => DAGIdentity.placementId(dagIri, placementIdentifier);
const original = new DAGBuilder(dagIri, '1')
.node(placement('transform'), new TransformNode(), { success: placement('end') })
.terminal(placement('end'))
.build();
// Serialize to JSON.
const json = DAGDocument.serialize(original);
// Load back from the JSON string — validates against DAGSchema and returns
// a fully-typed DAG. Structure is identical to the original.
const reloaded = DAGDocument.load(json);
process.stdout.write(`@type: ${reloaded['@type']}\n`); // 'DAG'
process.stdout.write(`nodes[0] @type: ${reloaded.nodes[0]?.['@type']}\n`); // 'SingleNode'
// #endregion round-trip
// ---------------------------------------------------------------------------
// Persistence: file on disk
// ---------------------------------------------------------------------------
// #region persistence-file
// Write the pretty JSON to disk; read it back and re-validate at load time.
await fs.writeFile('/tmp/dag-json-ld.json', DAGDocument.serialize(original));
const loadedFromFile = DAGDocument.load(
await fs.readFile('/tmp/dag-json-ld.json', 'utf8'),
);
process.stdout.write(`loaded from file: ${loadedFromFile.name}\n`); // 'demo'
// #endregion persistence-file
// ---------------------------------------------------------------------------
// Persistence: simulated database row (text / JSON column)
// ---------------------------------------------------------------------------
// #region persistence-db
// A database column stores the serialized DAG body. On read, pass the JSON
// string through DAGDocument.load to validate at the ingest boundary.
const store = new Map<string, string>();
store.set(original['@id'], DAGDocument.serialize(original));
const row = store.get('urn:noocodec:dag:demo') ?? '';
const loadedFromDb = DAGDocument.load(row);
process.stdout.write(`loaded from db: ${loadedFromDb.name}\n`); // 'demo'
// #endregion persistence-db
// ---------------------------------------------------------------------------
// Execution: the loaded DAG is consumed directly by the dispatcher
// ---------------------------------------------------------------------------
// #region execute-loaded
// The loaded DAG is the same canonical object DAGBuilder.build() returns.
// Pass it directly to dispatcher.registerDAG() — no projection or adapter needed.
const dispatcher = new Dagonizer<NodeStateBase>();
dispatcher.registerNode(new TransformNode());
dispatcher.registerDAG(loadedFromFile);
const state = new NodeStateBase();
await dispatcher.execute(dagIri, state);
process.stdout.write(`transformed: ${String(state.getMetadata('transformed'))}\n`); // true
// #endregion execute-loaded
process.stdout.write('\nLesson: DAGDocument.serialize() + DAGDocument.load() is a lossless round-trip.\n');
process.stdout.write(' The serialized document is a JSON-LD 1.1 doc: @context, @id, @type.\n');
process.stdout.write(' DAGDocument.load is the only valid DAG document ingest boundary.\n');The serializer is a thin wrapper over JSON.stringify. There is no transformation step. The object IS the wire shape.
Reachable rendering
JsonLdRenderer.render(dag) renders one DAG document. JsonLdRenderer.renderReachable(entryDag, registry) renders the entry DAG plus every literal embedded DAG reachable through the registry, deduplicated by @id.
Use the reachable renderer when you want the canonical JSON-LD document for a plugin-backed forest instead of a single DAG:
import { DagGraphProjector } from '@studnicky/dagonizer/graph';
import { JsonLdRenderer } from '@studnicky/dagonizer/viz';
const registry = new Map(dispatcher.listDAGs().map((dag) => [DagGraphProjector.dagIri(dag), dag]));
const doc = JsonLdRenderer.renderReachable(parentDag, registry);The registry is keyed by expanded DAG IRI. That keeps local DAGs, plugin-exported DAGs, tool DAGs, and dynamic candidates on the same lookup path.
Round-trip
// Build a DAG via DAGBuilder — the canonical JSON-LD object.
const dagIri = 'urn:noocodec:dag:demo' as const;
const placement = (placementIdentifier: string): string => DAGIdentity.placementId(dagIri, placementIdentifier);
const original = new DAGBuilder(dagIri, '1')
.node(placement('transform'), new TransformNode(), { success: placement('end') })
.terminal(placement('end'))
.build();
// Serialize to JSON.
const json = DAGDocument.serialize(original);
// Load back from the JSON string — validates against DAGSchema and returns
// a fully-typed DAG. Structure is identical to the original.
const reloaded = DAGDocument.load(json);
process.stdout.write(`@type: ${reloaded['@type']}\n`); // 'DAG'
process.stdout.write(`nodes[0] @type: ${reloaded.nodes[0]?.['@type']}\n`); // 'SingleNode'The round-trip preserves identity. DAGDocument.load(DAGDocument.serialize(dag)) produces a value structurally equal to dag.
Placement discriminators
Each placement type carries a distinct @type that drives the runtime dispatch:
@type | Placement | Required fields |
|---|---|---|
SingleNode | One registered node | @id, @type, name, node, outputs |
ScatterNode | Fork over source array, run body per clone, route | @id, @type, name, body, source, outputs |
GatherNode | Fan in producer records, gather, route | @id, @type, name, sources, gather, outputs |
EmbeddedDAGNode | Nested DAG invocation at cardinality 1 | @id, @type, name, dag, outputs |
TerminalNode | Explicit terminus | @id, @type, name, outcome |
PhaseNode | Lifecycle-attached node | @id, @type, name, phase, node |
Persistence patterns
The serializer and loader have no opinion about storage. File on disk:
// Write the pretty JSON to disk; read it back and re-validate at load time.
await fs.writeFile('/tmp/dag-json-ld.json', DAGDocument.serialize(original));
const loadedFromFile = DAGDocument.load(
await fs.readFile('/tmp/dag-json-ld.json', 'utf8'),
);
process.stdout.write(`loaded from file: ${loadedFromFile.name}\n`); // 'demo'Database column (text or JSON body):
// A database column stores the serialized DAG body. On read, pass the JSON
// string through DAGDocument.load to validate at the ingest boundary.
const store = new Map<string, string>();
store.set(original['@id'], DAGDocument.serialize(original));
const row = store.get('urn:noocodec:dag:demo') ?? '';
const loadedFromDb = DAGDocument.load(row);
process.stdout.write(`loaded from db: ${loadedFromDb.name}\n`); // 'demo'For HTTP transport, pass the JSON string to DAGDocument.load(json) at the ingest boundary.
RDF interop
Because every field carries a canonical IRI through @context, a Dagonizer DAG is a valid RDF graph. Generic JSON-LD processors can extract triples without knowing anything about Dagonizer:
<urn:noocodec:dag:demo>
rdf:type dag:DAG ;
dag:name "demo" ;
dag:version "1" ;
dag:entrypoints [ dag:main "urn:noocodec:dag:demo/node/transform" ] ;
dag:nodes <urn:noocodec:dag:demo/node/transform> .
<urn:noocodec:dag:demo/node/transform>
rdf:type dag:SingleNode ;
dag:name "transform" ;
dag:node "transform" ;
dag:outputs [ dag:success "urn:noocodec:dag:demo/node/end" ] .This is the same data the engine consumes. No separate ontology model, no projection. Applications that want to query DAGs as RDF (SHACL validation, SPARQL queries over a fleet of stored DAGs) get it for free by treating the JSON document as JSON-LD.
Related Concepts
- DAGBuilder - fluent authoring API; build() returns canonical JSON-LD
- Schema and JSON loading - Ajv validation surface at the ingest boundary
- Architecture - JSON-LD canonical wire format in the framework architecture
- Entities - every @type and its required field set
- Example 03: Tool Schemas - runnable load-and-round-trip example
- Checkpoint and Resume - serialize in-flight DAG state alongside the topology
- Reference, Entities
- Reference, Validation