Skip to content

Plugins

What It Is

Plugins package reusable Dagonizer parts without creating a second execution model. A plugin can ship nodes, DAG JSON-LD, namespace context, and exported DAG IRIs. The host registers the plugin once, then composes those exported DAGs exactly like locally-authored child DAGs, tool DAGs, or runtime-selected DagReference candidates.

Adapters, tools, and patterns are still plugin tiers, but the main assembly rule is simpler: if a reusable flow can be embedded, ship it as a DAG.

How It Works

A plugin declares an ID, optional context, nodes, DAGs, and exports. registerPlugin scopes and registers those parts into the same registries used by local bundles. Higher-level DAGs reference plugin DAG IRIs through normal EmbeddedDAGNode, scatter DAG body, or dynamic DagReference placements; there is no plugin-specific execution path.

Dagonizer ships three tiers of plugins, each installable independently. Every tier consumes a stable subpath surface on the main @studnicky/dagonizer package; the surface stays narrow so an adapter package does not pull in pattern code, a tool package does not pull in adapter internals, and so on.

Beta

The plugin packages are GitHub-only and not yet published to npm. Install via the repo + workspace path while live-API confirmation lands against each provider. The contracts (./adapter, ./tool, ./patterns) are stable.

Diagrams, Examples, and Outputs

The Cartographer packages source-normalization child DAGs as a plugin. The ingest DAG embeds those plugin-provided DAG IRIs through normal EmbeddedDAGNode placements:

Cartographer ingest DAG embedding plugin DAGs

15 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:ingest-source",
  "@type": "DAG",
  "name": "dag:ingest-source",
  "version": "1.0",
  "entrypoints": {
    "main": "urn:noocodec:dag:ingest-source/node/select-source"
  },
  "nodes": [
    {
      "@id": "urn:noocodec:dag:ingest-source/node/select-source",
      "@type": "SingleNode",
      "name": "dag:ingest-source/node/select-source",
      "node": "urn:noocodec:node:select-source",
      "outputs": {
        "compressed": "urn:noocodec:dag:ingest-source/node/decompress",
        "plain": "urn:noocodec:dag:ingest-source/node/route-format",
        "invalid": "urn:noocodec:dag:ingest-source/node/rejected"
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/decompress",
      "@type": "SingleNode",
      "name": "dag:ingest-source/node/decompress",
      "node": "urn:noocodec:node:decompress",
      "outputs": {
        "route-format": "urn:noocodec:dag:ingest-source/node/route-format",
        "invalid": "urn:noocodec:dag:ingest-source/node/rejected"
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/route-format",
      "@type": "SingleNode",
      "name": "dag:ingest-source/node/route-format",
      "node": "urn:noocodec:node:route-format",
      "outputs": {
        "csv": "urn:noocodec:dag:ingest-source/node/parse-csv",
        "json": "urn:noocodec:dag:ingest-source/node/parse-json",
        "ndjson": "urn:noocodec:dag:ingest-source/node/parse-ndjson",
        "yaml": "urn:noocodec:dag:ingest-source/node/parse-yaml",
        "invalid": "urn:noocodec:dag:ingest-source/node/rejected"
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/parse-csv",
      "@type": "SingleNode",
      "name": "dag:ingest-source/node/parse-csv",
      "node": "urn:noocodec:node:parse-csv",
      "outputs": {
        "normalized": "urn:noocodec:dag:ingest-source/node/normalize-csv",
        "invalid": "urn:noocodec:dag:ingest-source/node/rejected"
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/parse-json",
      "@type": "SingleNode",
      "name": "dag:ingest-source/node/parse-json",
      "node": "urn:noocodec:node:parse-json",
      "outputs": {
        "normalized": "urn:noocodec:dag:ingest-source/node/normalize-json",
        "invalid": "urn:noocodec:dag:ingest-source/node/rejected"
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/parse-ndjson",
      "@type": "SingleNode",
      "name": "dag:ingest-source/node/parse-ndjson",
      "node": "urn:noocodec:node:parse-ndjson",
      "outputs": {
        "normalized": "urn:noocodec:dag:ingest-source/node/normalize-ndjson",
        "invalid": "urn:noocodec:dag:ingest-source/node/rejected"
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/parse-yaml",
      "@type": "SingleNode",
      "name": "dag:ingest-source/node/parse-yaml",
      "node": "urn:noocodec:node:parse-yaml",
      "outputs": {
        "normalized": "urn:noocodec:dag:ingest-source/node/normalize-yaml",
        "invalid": "urn:noocodec:dag:ingest-source/node/rejected"
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/normalize-csv",
      "@type": "EmbeddedDAGNode",
      "name": "dag:ingest-source/node/normalize-csv",
      "outputs": {
        "success": "urn:noocodec:dag:ingest-source/node/coerce-types",
        "error": "urn:noocodec:dag:ingest-source/node/rejected"
      },
      "dag": "urn:noocodec:dag:normalize-csv",
      "stateMapping": {
        "input": {
          "parsedRecords": "parsedRecords",
          "currentSource": "currentSource"
        },
        "output": {
          "mappedRecords": "mappedRecords"
        }
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/normalize-json",
      "@type": "EmbeddedDAGNode",
      "name": "dag:ingest-source/node/normalize-json",
      "outputs": {
        "success": "urn:noocodec:dag:ingest-source/node/coerce-types",
        "error": "urn:noocodec:dag:ingest-source/node/rejected"
      },
      "dag": "urn:noocodec:dag:normalize-json",
      "stateMapping": {
        "input": {
          "parsedRecords": "parsedRecords",
          "currentSource": "currentSource"
        },
        "output": {
          "mappedRecords": "mappedRecords"
        }
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/normalize-ndjson",
      "@type": "EmbeddedDAGNode",
      "name": "dag:ingest-source/node/normalize-ndjson",
      "outputs": {
        "success": "urn:noocodec:dag:ingest-source/node/coerce-types",
        "error": "urn:noocodec:dag:ingest-source/node/rejected"
      },
      "dag": "urn:noocodec:dag:normalize-ndjson",
      "stateMapping": {
        "input": {
          "parsedRecords": "parsedRecords",
          "currentSource": "currentSource"
        },
        "output": {
          "mappedRecords": "mappedRecords"
        }
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/normalize-yaml",
      "@type": "EmbeddedDAGNode",
      "name": "dag:ingest-source/node/normalize-yaml",
      "outputs": {
        "success": "urn:noocodec:dag:ingest-source/node/coerce-types",
        "error": "urn:noocodec:dag:ingest-source/node/rejected"
      },
      "dag": "urn:noocodec:dag:normalize-yaml",
      "stateMapping": {
        "input": {
          "parsedRecords": "parsedRecords",
          "currentSource": "currentSource"
        },
        "output": {
          "mappedRecords": "mappedRecords"
        }
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/coerce-types",
      "@type": "SingleNode",
      "name": "dag:ingest-source/node/coerce-types",
      "node": "urn:noocodec:node:coerce-types",
      "outputs": {
        "validate-event": "urn:noocodec:dag:ingest-source/node/validate-event"
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/validate-event",
      "@type": "SingleNode",
      "name": "dag:ingest-source/node/validate-event",
      "node": "urn:noocodec:node:validate-event",
      "outputs": {
        "validated": "urn:noocodec:dag:ingest-source/node/ingested"
      }
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/ingested",
      "@type": "TerminalNode",
      "name": "dag:ingest-source/node/ingested",
      "outcome": "completed"
    },
    {
      "@id": "urn:noocodec:dag:ingest-source/node/rejected",
      "@type": "TerminalNode",
      "name": "dag:ingest-source/node/rejected",
      "outcome": "failed"
    }
  ]
}
Mermaid generated from the same DAG
Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
  %% dag:ingest-source (v1.0)
  entry_main(["main"])
  entry_main --> urn_noocodec_dag_ingest-source/node/select-source
  urn_noocodec_dag_ingest-source/node/select-source["dag:ingest-source/node/select-source"]
  urn_noocodec_dag_ingest-source/node/select-source -->|compressed| urn_noocodec_dag_ingest-source/node/decompress
  urn_noocodec_dag_ingest-source/node/select-source -->|plain| urn_noocodec_dag_ingest-source/node/route-format
  urn_noocodec_dag_ingest-source/node/select-source -->|invalid| urn_noocodec_dag_ingest-source/node/rejected
  urn_noocodec_dag_ingest-source/node/decompress["dag:ingest-source/node/decompress"]
  urn_noocodec_dag_ingest-source/node/decompress -->|route-format| urn_noocodec_dag_ingest-source/node/route-format
  urn_noocodec_dag_ingest-source/node/decompress -->|invalid| urn_noocodec_dag_ingest-source/node/rejected
  urn_noocodec_dag_ingest-source/node/route-format["dag:ingest-source/node/route-format"]
  urn_noocodec_dag_ingest-source/node/route-format -->|csv| urn_noocodec_dag_ingest-source/node/parse-csv
  urn_noocodec_dag_ingest-source/node/route-format -->|json| urn_noocodec_dag_ingest-source/node/parse-json
  urn_noocodec_dag_ingest-source/node/route-format -->|ndjson| urn_noocodec_dag_ingest-source/node/parse-ndjson
  urn_noocodec_dag_ingest-source/node/route-format -->|yaml| urn_noocodec_dag_ingest-source/node/parse-yaml
  urn_noocodec_dag_ingest-source/node/route-format -->|invalid| urn_noocodec_dag_ingest-source/node/rejected
  urn_noocodec_dag_ingest-source/node/parse-csv["dag:ingest-source/node/parse-csv"]
  urn_noocodec_dag_ingest-source/node/parse-csv -->|normalized| urn_noocodec_dag_ingest-source/node/normalize-csv
  urn_noocodec_dag_ingest-source/node/parse-csv -->|invalid| urn_noocodec_dag_ingest-source/node/rejected
  urn_noocodec_dag_ingest-source/node/parse-json["dag:ingest-source/node/parse-json"]
  urn_noocodec_dag_ingest-source/node/parse-json -->|normalized| urn_noocodec_dag_ingest-source/node/normalize-json
  urn_noocodec_dag_ingest-source/node/parse-json -->|invalid| urn_noocodec_dag_ingest-source/node/rejected
  urn_noocodec_dag_ingest-source/node/parse-ndjson["dag:ingest-source/node/parse-ndjson"]
  urn_noocodec_dag_ingest-source/node/parse-ndjson -->|normalized| urn_noocodec_dag_ingest-source/node/normalize-ndjson
  urn_noocodec_dag_ingest-source/node/parse-ndjson -->|invalid| urn_noocodec_dag_ingest-source/node/rejected
  urn_noocodec_dag_ingest-source/node/parse-yaml["dag:ingest-source/node/parse-yaml"]
  urn_noocodec_dag_ingest-source/node/parse-yaml -->|normalized| urn_noocodec_dag_ingest-source/node/normalize-yaml
  urn_noocodec_dag_ingest-source/node/parse-yaml -->|invalid| urn_noocodec_dag_ingest-source/node/rejected
  urn_noocodec_dag_ingest-source/node/normalize-csv[["dag:ingest-source/node/normalize-csv"]]
  urn_noocodec_dag_ingest-source/node/normalize-csv -->|success| urn_noocodec_dag_ingest-source/node/coerce-types
  urn_noocodec_dag_ingest-source/node/normalize-csv -->|error| urn_noocodec_dag_ingest-source/node/rejected
  urn_noocodec_dag_ingest-source/node/normalize-json[["dag:ingest-source/node/normalize-json"]]
  urn_noocodec_dag_ingest-source/node/normalize-json -->|success| urn_noocodec_dag_ingest-source/node/coerce-types
  urn_noocodec_dag_ingest-source/node/normalize-json -->|error| urn_noocodec_dag_ingest-source/node/rejected
  urn_noocodec_dag_ingest-source/node/normalize-ndjson[["dag:ingest-source/node/normalize-ndjson"]]
  urn_noocodec_dag_ingest-source/node/normalize-ndjson -->|success| urn_noocodec_dag_ingest-source/node/coerce-types
  urn_noocodec_dag_ingest-source/node/normalize-ndjson -->|error| urn_noocodec_dag_ingest-source/node/rejected
  urn_noocodec_dag_ingest-source/node/normalize-yaml[["dag:ingest-source/node/normalize-yaml"]]
  urn_noocodec_dag_ingest-source/node/normalize-yaml -->|success| urn_noocodec_dag_ingest-source/node/coerce-types
  urn_noocodec_dag_ingest-source/node/normalize-yaml -->|error| urn_noocodec_dag_ingest-source/node/rejected
  urn_noocodec_dag_ingest-source/node/coerce-types["dag:ingest-source/node/coerce-types"]
  urn_noocodec_dag_ingest-source/node/coerce-types -->|validate-event| urn_noocodec_dag_ingest-source/node/validate-event
  urn_noocodec_dag_ingest-source/node/validate-event["dag:ingest-source/node/validate-event"]
  urn_noocodec_dag_ingest-source/node/validate-event -->|validated| urn_noocodec_dag_ingest-source/node/ingested
  urn_noocodec_dag_ingest-source/node/ingested((("dag:ingest-source/node/ingested")))
  urn_noocodec_dag_ingest-source/node/rejected>"dag:ingest-source/node/rejected"]

plugin-provided normalize-csv 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:normalize-csv",
  "@type": "DAG",
  "name": "dag:normalize-csv",
  "version": "1.0",
  "entrypoints": {
    "main": "urn:noocodec:dag:normalize-csv/node/normalize-csv-map"
  },
  "nodes": [
    {
      "@id": "urn:noocodec:dag:normalize-csv/node/normalize-csv-map",
      "@type": "SingleNode",
      "name": "dag:normalize-csv/node/normalize-csv-map",
      "node": "urn:noocodec:node:normalize-csv-map",
      "outputs": {
        "normalized": "urn:noocodec:dag:normalize-csv/node/normalized"
      }
    },
    {
      "@id": "urn:noocodec:dag:normalize-csv/node/normalized",
      "@type": "TerminalNode",
      "name": "dag:normalize-csv/node/normalized",
      "outcome": "completed"
    }
  ]
}
Mermaid generated from the same DAG
Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
  %% dag:normalize-csv (v1.0)
  entry_main(["main"])
  entry_main --> urn_noocodec_dag_normalize-csv/node/normalize-csv-map
  urn_noocodec_dag_normalize-csv/node/normalize-csv-map["dag:normalize-csv/node/normalize-csv-map"]
  urn_noocodec_dag_normalize-csv/node/normalize-csv-map -->|normalized| urn_noocodec_dag_normalize-csv/node/normalized
  urn_noocodec_dag_normalize-csv/node/normalized((("dag:normalize-csv/node/normalized")))

plugin-provided normalize-json 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:normalize-json",
  "@type": "DAG",
  "name": "dag:normalize-json",
  "version": "1.0",
  "entrypoints": {
    "main": "urn:noocodec:dag:normalize-json/node/normalize-json-map"
  },
  "nodes": [
    {
      "@id": "urn:noocodec:dag:normalize-json/node/normalize-json-map",
      "@type": "SingleNode",
      "name": "dag:normalize-json/node/normalize-json-map",
      "node": "urn:noocodec:node:normalize-json-map",
      "outputs": {
        "normalized": "urn:noocodec:dag:normalize-json/node/normalized"
      }
    },
    {
      "@id": "urn:noocodec:dag:normalize-json/node/normalized",
      "@type": "TerminalNode",
      "name": "dag:normalize-json/node/normalized",
      "outcome": "completed"
    }
  ]
}
Mermaid generated from the same DAG
Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
  %% dag:normalize-json (v1.0)
  entry_main(["main"])
  entry_main --> urn_noocodec_dag_normalize-json/node/normalize-json-map
  urn_noocodec_dag_normalize-json/node/normalize-json-map["dag:normalize-json/node/normalize-json-map"]
  urn_noocodec_dag_normalize-json/node/normalize-json-map -->|normalized| urn_noocodec_dag_normalize-json/node/normalized
  urn_noocodec_dag_normalize-json/node/normalized((("dag:normalize-json/node/normalized")))

What It Lets You Do

Use plugins when reusable nodes, DAGs, adapters, tools, or abstract patterns should be packaged independently and installed into a higher-level DAG. Plugin-defined DAGs, hand-authored embedded DAGs, tools-as-DAGs, and dynamic DAG references use the same registry and the same JSON-LD assembly interface.

Code Samples

The snippets below show the public plugin tiers, the high-level plugin definition helper, and the registry/discovery utilities.

Plugin tiers

TierSubpath consumedPackagesShape
Adapters@studnicky/dagonizer/adapter@studnicky/dagonizer-adapter-* (8)Concrete drop-in classes
Tools@studnicky/dagonizer/tool (+ /adapter)@studnicky/dagonizer-tool-* (3)Concrete classes implementing ToolInterface<TInput, TOutput>
Patterns@studnicky/dagonizer/patterns (+ /adapter, /tool)@studnicky/dagonizer-patterns-* (3)Abstract base classes applications extend

@studnicky/dagonizer/adapter

The adapter subpath exposes everything an LLM-provider adapter needs:

SymbolRole
LlmAdapterInterfaceThe contract every adapter implements (chat(ChatRequestType): Promise<ChatResponseType>)
BaseAdapterAbstract base with retry, error classification, request normalization
OpenAiCompatibleAdapterConcrete base for OpenAI-shaped HTTP backends
LlmAdapterCascade, LlmAdapterRegistry, AdapterDescriptorMulti-adapter routing
EmbedderCascade, EmbedderRegistry, BaseEmbedderEmbedding model cascade
ChatRequestType, ChatResponseType, ChatRequest, ChatResponseMessageWire types and value factories via .create(...)
LlmError, ClassificationsError taxonomy — LlmError.classifyHttp(status, body) and LlmError.ofNetworkError(err) are static methods on LlmError
ToolCallCodecJSON envelope decoder for models that emit tool calls as text (Gemini Nano, WebLLM)
AdapterCapabilitiesType, ToolCall, ToolChoiceType, ToolDefinition, TokenUsageCapability metadata

Using an adapter

ts
const dispatcher = new Dagonizer<ChatAdapterState>();
dispatcher.registerNode(new ChatNode());
dispatcher.registerNode(new HandleTextNode());
dispatcher.registerNode(new HandleToolsNode());
dispatcher.registerDAG(dag);

const state = new ChatAdapterState();
state.prompt  = 'What is a DAG?';
state.adapter = adapter;

await dispatcher.execute('urn:noocodec:dag:llm-adapter-demo', state);

Writing an adapter

Extend BaseAdapter and implement the one abstract method, performChat. The adapter below is complete and runnable — it echoes the last user message instead of calling a provider, so it needs no network:

ts
export class EchoAdapter extends BaseAdapter {
  constructor() {
    super('echo', 'Echo Provider', {
      toolUse: 'none',
      structuredOutput: false,
      jsonMode: false,
    });
  }

  protected override async performChat(request: ChatRequestType): Promise<ChatResponseType> {
    // A real adapter calls its provider here. This one echoes the last user
    // message so the example is deterministic and needs no network.
    const lastUser = [...request.messages].reverse().find((m) => m.role === 'user');
    const reply = lastUser === undefined ? '(no user message)' : `echo: ${lastUser.content}`;
    return {
      message: ChatResponseMessage.create(reply, []),
      finishReason: 'stop',
      usage: ZERO_TOKEN_USAGE,
    };
  }
}

A production adapter fills performChat with a real HTTP call; retry, error classification, and probe() come from BaseAdapter for free. The custom-adapter example drives this adapter through a chat() call; run it with npx tsx examples/custom-adapter.ts.

@studnicky/dagonizer/tool

The tool subpath exposes a small surface for external-service wrappers:

SymbolRole
ToolInterface<TInput, TOutput>Contract: definition (the JSON-Schema LLM-facing surface) + execute(input, options?)
ToolErrorError type with classification.reason
HttpTransportBuilt-in retry, timeout, abort propagation, optional token bucket / circuit breaker composition, JSON parsing for HTTP-backed tools

Using a tool

ts
export class CalculatorTool implements ToolInterface<CalcInput, CalcOutput> {
  readonly definition = {
    'name':        'calculator',
    'description': 'Add two numbers. Returns { result: number }.',
    'inputSchema': {
      '$schema':    'https://json-schema.org/draft/2020-12/schema',
      'type':       'object' as const,
      'required':   ['a', 'b'] as const,
      'properties': {
        'a': { 'type': 'number' as const },
        'b': { 'type': 'number' as const },
      },
    },
    'outputSchema': {
      'type': 'object' as const,
    },
    'strict': true,
  };
  async execute(input: CalcInput) {
    return Promise.resolve({ 'result': input.a + input.b });
  }
}

Writing a tool

ts
export interface CalcInput extends Record<string, unknown> {
  readonly a: number;
  readonly b: number;
}

export interface CalcOutput {
  readonly result: number;
}

export class CalculatorTool implements ToolInterface<CalcInput, CalcOutput> {
  readonly definition = {
    'name':        'calculator',
    'description': 'Add two numbers. Returns { result: number }.',
    'inputSchema': {
      '$schema':    'https://json-schema.org/draft/2020-12/schema',
      'type':       'object' as const,
      'required':   ['a', 'b'] as const,
      'properties': {
        'a': { 'type': 'number' as const },
        'b': { 'type': 'number' as const },
      },
    },
    'outputSchema': {
      'type': 'object' as const,
    },
    'strict': true,
  };
  async execute(input: CalcInput) {
    return Promise.resolve({ 'result': input.a + input.b });
  }
}

HttpTransport handles retry on 429/5xx/network, abort propagation, JSON parsing, timeout, and optional substrate TokenBucket / CircuitBreaker guards; every HTTP-backed tool gets the same logical-request ordering. See Execution tuning for when to apply those guards.

@studnicky/dagonizer/patterns

The patterns subpath exposes the abstract MonadicNode root plus the service contracts pattern packages depend on:

SymbolRole
MonadicNode<TState, TOutput>Abstract base class. Owns the dispatch loop; subclasses inject domain pieces via abstract methods
LlmClientInterfaceService contract: chat(ChatRequestType): Promise<ChatResponseType> (subset of LlmAdapterInterface)
TripleStoreInterfaceService contract: assert, ask, select, count, clearGraph, triples
Binding, Quad, SlotPattern, TermRDF value types used by TripleStoreInterface

Pattern taxonomy

MonadicNode<TState, TOutput>                       (root: main package)

├── DecisionNode<TState, TChoice>                  [patterns-rag]
├── ComposeNode<TState>                            [patterns-rag]
├── ScoutNode<TState, TIn, TOut, TItem>            [patterns-rag]
├── GraphNode<TState>                              [patterns-graph]
│   ├── RecallContextNode
│   ├── RecordFindingsNode
│   └── MemoryDigestNode
└── FlowNode<TState>                               [patterns-flow]
    ├── SelectNode → PickByScoreNode, SortByNode
    ├── ReduceNode → DedupeByKeyNode, GroupByFieldNode, MergeReducerNode
    ├── PredicateGateNode
    ├── ExtractFieldNode
    └── RespondNode

Example: classifying intent

ts
export class IntentClassifier extends DecisionNode<IntentState, Intent, Intent> {
  readonly name = 'classify-intent';
  readonly '@id' = 'urn:noocodec:node:classify-intent';
  readonly outputs = ['search', 'describe', 'recommend', 'off-topic'] as const;

  constructor(llm: LlmClientInterface) {
    super(llm);
  }
  override get outputSchema(): Record<'search' | 'describe' | 'recommend' | 'off-topic', SchemaObjectType> {
    return {
      'search':     { 'type': 'object' },
      'describe':   { 'type': 'object' },
      'recommend':  { 'type': 'object' },
      'off-topic':  { 'type': 'object' },
    };
  }

  protected composePrompt(state: IntentState): string {
    return `Classify: "${state.query}" → search | describe | recommend | off-topic. Reply with one word.`;
  }

  protected decodeChoice(content: string): Intent {
    const token = content.trim().toLowerCase();
    if (token === 'search' || token === 'describe' || token === 'recommend') return token;
    return 'off-topic';
  }

  protected routeFor(intent: Intent): Intent {
    return intent;
  }

  protected applyChoice(state: IntentState, intent: Intent): void {
    state.intent = intent;
  }
}

The pattern handles LLM dispatch, retry, abort propagation, and contract field forwarding. The lines above are everything the application writes. The pattern-node example runs this IntentClassifier inside a DAG against an in-process LLM; run it with npx tsx examples/pattern-node.ts.

Why three subpaths

Each contract subpath is independently consumable:

  • An adapter package depends on @studnicky/dagonizer/adapter only; never pulls in the pattern surface.
  • A tool package depends on @studnicky/dagonizer/tool + /adapter (for ToolDefinition); never pulls in patterns.
  • A pattern package depends on @studnicky/dagonizer/patterns (root) + occasionally /adapter (RAG patterns need LLM types) + /tool (ScoutNode references Tool).

Applications install only what they use. The dependency graph stays acyclic.

Details for Nerds

Plugin loader

PluginInterface

A plugin package implements PluginInterface — a stable id plus register(dispatcher) — to install its nodes and DAGs onto any dispatcher. The receiver is typed as PluginReceiverType, a narrow view that only exposes registerBundle. The plugin cannot reach any other dispatcher surface.

ts
import type { PluginInterface, PluginReceiverType, DispatcherBundleType, NodeStateInterface } from '@studnicky/dagonizer';

export class NormalizePlugin implements PluginInterface {
  readonly id = '@acme/dagonizer-normalize';

  private bundle(): DispatcherBundleType<NodeStateInterface> {
    return {
      specifier: this.id,
      nodes: [new NormalizeNode(), new SummarizeNode()],
      dags:  [pluginDag],
    };
  }

  register(dispatcher: PluginReceiverType): void {
    dispatcher.registerBundle(this.bundle());
  }
}

defineDagonizerPlugin

defineDagonizerPlugin() is the high-level authoring helper for plugin packages. It returns a valid PluginInterface plus typed exports that point at DAG IRIs inside the plugin bundle.

Use it when the plugin packages reusable DAGs:

ts
import { defineDagonizerPlugin } from '@studnicky/dagonizer/plugin';

export const retrievalPlugin = defineDagonizerPlugin({
  id: '@acme/dagonizer-retrieval',
  context: {
    retrieval: 'https://noocodec.dev/plugins/retrieval#',
  },
  nodes: [
    new EmbedQueryNode(),
    new VectorSearchNode(),
  ],
  dags: [
    retrievalSearchDag,
  ],
  exports: {
    search: 'retrieval:search',
  },
});

The id is the plugin package/specifier owner for its context prefixes. The export values are DAG IRIs or CURIEs. The helper validates that every exported DAG reference exists in dags before it returns.

Authoring rule:

  • If an application can embed it, it should be a DAG.
  • If a plugin ships it, it exports the DAG IRI or CURIE.
  • There is no separate plugin runtime object graph.

Collision rule:

  • Registered DAGs are keyed by their resolved IRI.
  • The same IRI with a different implementation is a hard registration error.
  • The same IRI with the same object is idempotent.
  • To compose plugins that would otherwise overlap on internal labels, give each plugin its own namespace prefix in @context and export DAG references through that prefix.

Dagonizer.registerPlugin(plugin)

The caller installs a plugin with a single call. Order matters: register plugins before the parent DAG that references their sub-DAG IRIs.

ts
import { Dagonizer } from '@studnicky/dagonizer';

const dispatcher = new Dagonizer<MyState>();
dispatcher.registerPlugin(new NormalizePlugin());   // installs nodes + sub-DAG
dispatcher.registerDAG(parentDag);                  // parent references the plugin sub-DAG IRI

PluginDiscovery (static DAG-walker)

PluginDiscovery from @studnicky/dagonizer/plugin provides graph-backed utilities for discovering which plugin DAGs a given entry DAG transitively needs:

ts
import { DagGraphProjector } from '@studnicky/dagonizer/graph';
import { PluginDiscovery } from '@studnicky/dagonizer/plugin';

// Immediate literal and dynamic candidate IRIs in one DAG's placement graph
const iris = PluginDiscovery.referencedDagIris(myDag);

// Breadth-first walk of the full reachable forest
const registry = new Map(dispatcher.listDAGs().map(dag => [DagGraphProjector.dagIri(dag), dag]));
const allDagIris = PluginDiscovery.walk(myDag, registry);

Dynamic DagReference candidates are projected into the graph and participate in discovery. Literal DAG references and dynamic candidate DAGs use the same graph query path, so plugin DAGs and local DAGs stay on one registry surface.

When applications want to render or inspect a whole reachable forest, the registry should be keyed by expanded DAG IRI, not by plugin object identity. That keeps plugin DAGs and local DAGs on the same interface and matches the graph projection used by validation and JSON-LD rendering.

PluginLoader — type-safe dynamic import

When loading a plugin from an npm package or a dynamic path, the return type of import() is unknown. PluginLoader from @studnicky/dagonizer validates the default export against the PluginInterface structural contract — no casts required at the call site.

ts
import { PluginLoader } from '@studnicky/dagonizer';

// Dynamic import: validates default export, throws DAGError('PLUGIN_INVALID') if invalid
const plugin = await PluginLoader.load('my-dagonizer-plugin');
dispatcher.registerPlugin(plugin);

PluginLoader.load(specifier) is the only plugin-module ingest path. It imports the module, validates the default export structurally, and returns a PluginInterface. On failure it throws a DAGError with code: 'PLUGIN_INVALID'.

PluginDiscovery.loadAll — batch walk + register

To walk a DAG forest, load each referenced plugin module, and register the plugins on a dispatcher in a single call:

ts
import { DagGraphProjector } from '@studnicky/dagonizer/graph';
import { PluginDiscovery, PluginSpecifier } from '@studnicky/dagonizer/plugin';

const registry = new Map(dispatcher.listDAGs().map(dag => [DagGraphProjector.dagIri(dag), dag]));
await PluginDiscovery.loadAll(
  entryDag,
  registry,
  dispatcher,
  PluginSpecifier.byIriPrefix(dispatcher),
);

loadAll uses PluginLoader.load internally; validation and registerPlugin are called for each import specifier resolved from the DAG IRIs returned by PluginDiscovery.walk.

Full example

ts
dispatcher.registerPlugin(normalizePlugin);
dispatcher.registerDAG(parentDag);

Run: npx tsx examples/33-plugin.ts

Watched over by the Order of Dagon.