Skip to content

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:

ts
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',
    },
  ],
});
ts
// 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
DAG JSON-LD registered with the dispatcher
{
  "@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 generated from the same DAG
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:

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:

ts
// 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 DAGSchema via Ajv 2020-12).
  • Missing required fields (@context, @id, @type, name, version, entrypoints, nodes).
  • Invalid @type discriminator 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/dagonizer as DAG_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 @context expansion. 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. name is display/observability text; entrypoints point at placement IRIs.
  • nodes. Array of placement objects, each with its own @id and @type. Placement @id values are route targets.

Example 03 embeds a full JSON-LD DAG as a string and feeds it through the ingest boundary:

ts
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:

@typeRole
DAGTop-level document
SingleNodeOne registered node, routed by named outputs
ScatterNodeFork over a source array: one clone per item, run a body in each clone, emit clone outcome records, route on aggregate outcome
GatherNodeFan in records from one or more producer placements and fold them into parent state
EmbeddedDAGNodeInvoke a nested registered DAG at cardinality 1, with optional stateMapping to copy fields in and out
TerminalNodeExplicit terminus with outcome of 'completed' or 'failed'
PhaseNodeLifecycle-attached pre or post placement

Exporting ​

DAGDocument.serialize(dag) produces pretty-printed JSON (2-space indent):

ts
// 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:

ts
/**
 * 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:

ts
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 ​

ts
// 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:

@typePlacementRequired fields
SingleNodeOne registered node@id, @type, name, node, outputs
ScatterNodeFork over source array, run body per clone, route@id, @type, name, body, source, outputs
GatherNodeFan in producer records, gather, route@id, @type, name, sources, gather, outputs
EmbeddedDAGNodeNested DAG invocation at cardinality 1@id, @type, name, dag, outputs
TerminalNodeExplicit terminus@id, @type, name, outcome
PhaseNodeLifecycle-attached node@id, @type, name, phase, node

Persistence patterns ​

The serializer and loader have no opinion about storage. File on disk:

ts
// 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):

ts
// 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.

Watched over by the Order of Dagon.