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{
"@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 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{
"@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 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{
"@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 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")))- Architecture - how the dispatcher, contracts, and plugins interlock
- The Archivist - in-browser demo wiring adapters, tools, and patterns together
- Example 33: Plugin-Defined DAGs - Cartographer normalization plugin in a runnable DAG
- The Cartographer - browser runner that registers the normalization plugin before execution
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
| Tier | Subpath consumed | Packages | Shape |
|---|---|---|---|
| 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:
| Symbol | Role |
|---|---|
LlmAdapterInterface | The contract every adapter implements (chat(ChatRequestType): Promise<ChatResponseType>) |
BaseAdapter | Abstract base with retry, error classification, request normalization |
OpenAiCompatibleAdapter | Concrete base for OpenAI-shaped HTTP backends |
LlmAdapterCascade, LlmAdapterRegistry, AdapterDescriptor | Multi-adapter routing |
EmbedderCascade, EmbedderRegistry, BaseEmbedder | Embedding model cascade |
ChatRequestType, ChatResponseType, ChatRequest, ChatResponseMessage | Wire types and value factories via .create(...) |
LlmError, Classifications | Error taxonomy — LlmError.classifyHttp(status, body) and LlmError.ofNetworkError(err) are static methods on LlmError |
ToolCallCodec | JSON envelope decoder for models that emit tool calls as text (Gemini Nano, WebLLM) |
AdapterCapabilitiesType, ToolCall, ToolChoiceType, ToolDefinition, TokenUsage | Capability metadata |
Using an adapter
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:
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:
| Symbol | Role |
|---|---|
ToolInterface<TInput, TOutput> | Contract: definition (the JSON-Schema LLM-facing surface) + execute(input, options?) |
ToolError | Error type with classification.reason |
HttpTransport | Built-in retry, timeout, abort propagation, optional token bucket / circuit breaker composition, JSON parsing for HTTP-backed tools |
Using a tool
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
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:
| Symbol | Role |
|---|---|
MonadicNode<TState, TOutput> | Abstract base class. Owns the dispatch loop; subclasses inject domain pieces via abstract methods |
LlmClientInterface | Service contract: chat(ChatRequestType): Promise<ChatResponseType> (subset of LlmAdapterInterface) |
TripleStoreInterface | Service contract: assert, ask, select, count, clearGraph, triples |
Binding, Quad, SlotPattern, Term | RDF 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
└── RespondNodeExample: classifying intent
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/adapteronly; never pulls in the pattern surface. - A tool package depends on
@studnicky/dagonizer/tool+/adapter(forToolDefinition); never pulls in patterns. - A pattern package depends on
@studnicky/dagonizer/patterns(root) + occasionally/adapter(RAG patterns need LLM types) +/tool(ScoutNode referencesTool).
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.
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:
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
@contextand 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.
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 IRIPluginDiscovery (static DAG-walker)
PluginDiscovery from @studnicky/dagonizer/plugin provides graph-backed utilities for discovering which plugin DAGs a given entry DAG transitively needs:
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.
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:
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
dispatcher.registerPlugin(normalizePlugin);
dispatcher.registerDAG(parentDag);Run: npx tsx examples/33-plugin.ts
Related Concepts
- Example 33: Plugin-Defined DAGs - the smallest runnable plugin registration and embedding path
- The Cartographer - browser demo registering plugin-defined normalization DAGs before execution
- IRI Identity - prefix expansion rules that keep independently-authored plugins from colliding
- DAGBuilder - the
.embed()API used by plugin-host DAGs - Reference: Contracts - plugin, bundle, node, store, and adapter contracts