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.