Skip to content

Example 02: DAGBuilder

What It Is

Example 02 shows the Archivist parent DAG authored with DAGBuilder instead of a hand-written object literal. The output is still the same JSON-LD DAG the dispatcher consumes; the builder only makes authoring safer and easier to review.

Use this page when you want TypeScript to help with graph assembly: route exhaustiveness, typed output names, auto-entrypoint selection, scatter placement options, embedded DAG mappings, and one final .build() that returns the canonical document.

How It Works

Each builder call appends a placement to the DAG document. The node instance supplies the output union, and the route object must cover that union. If a node can return 'retry', the route map needs a 'retry' key. If it does not, TypeScript complains before the docs, tests, or browser demo get involved.

build() freezes the assembly into a plain DAG value with a DAG IRI, placement IRIs, labeled entrypoints, and output targets. From that point forward, the builder disappears. Registration, serialization, visualization, plugins, and execution all see normal JSON-LD.

Diagrams, Examples, and Outputs

The diagram below is generated from the built Archivist DAG, not from a separate drawing. That matters: every .node(), .embed(), and .scatter() call in the source has a corresponding shape in the rendered graph.

DAG registration and diagram

The diagram is the same Archivist parent DAG that the dispatcher consumes at runtime. .build() returns this JSON-LD document directly; there is no second runtime DSL or projection layer.

The Archivist parent DAG

55 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:the-archivist",
  "@type": "DAG",
  "name": "the-archivist",
  "version": "6.0",
  "entrypoints": {
    "main": "urn:noocodec:dag:the-archivist/node/park-for-input"
  },
  "nodes": [
    {
      "@id": "urn:noocodec:dag:the-archivist/node/setup",
      "@type": "PhaseNode",
      "name": "setup",
      "node": "urn:noocodec:node:pre-run-setup",
      "phase": "pre"
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/park-for-input",
      "@type": "SingleNode",
      "name": "park-for-input",
      "node": "urn:noocodec:node:park-for-input",
      "outputs": {
        "parked": "urn:noocodec:dag:the-archivist/node/park-for-input",
        "resumed": "urn:noocodec:dag:the-archivist/node/recall-context"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recall-context",
      "@type": "SingleNode",
      "name": "recall-context",
      "node": "urn:noocodec:node:recall-context",
      "outputs": {
        "recalled": "urn:noocodec:dag:the-archivist/node/classify-intent"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/classify-intent",
      "@type": "SingleNode",
      "name": "classify-intent",
      "node": "urn:noocodec:node:classify-intent",
      "outputs": {
        "lookup-author": "urn:noocodec:dag:the-archivist/node/author-search",
        "find-reviews": "urn:noocodec:dag:the-archivist/node/reviews-extract",
        "describe-book": "urn:noocodec:dag:the-archivist/node/describe-extract",
        "recommend-similar": "urn:noocodec:dag:the-archivist/node/recommend-similar",
        "recall-memories": "urn:noocodec:dag:the-archivist/node/memory-recall",
        "on-topic": "urn:noocodec:dag:the-archivist/node/on-topic-search",
        "recommend-top-rated": "urn:noocodec:dag:the-archivist/node/recommend-extract",
        "off-topic": "urn:noocodec:dag:the-archivist/node/decline-off-topic",
        "retry": "urn:noocodec:dag:the-archivist/node/classify-intent",
        "salvage": "urn:noocodec:dag:the-archivist/node/classify-intent-salvage"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/classify-intent-salvage",
      "@type": "SingleNode",
      "name": "classify-intent-salvage",
      "node": "urn:noocodec:node:classify-intent-salvage",
      "outputs": {
        "done": "urn:noocodec:dag:the-archivist/node/on-topic-search"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/on-topic-search",
      "@type": "EmbeddedDAGNode",
      "name": "on-topic-search",
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/compose-loop",
        "error": "urn:noocodec:dag:the-archivist/node/compose-empty"
      },
      "dag": "urn:noocodec:dag:book-search-scatter",
      "stateMapping": {
        "output": {
          "terms": "terms",
          "toolPlan": "toolPlan",
          "candidates": "candidates",
          "shortlist": "shortlist",
          "priorContext": "priorContext",
          "failureCause": "failureCause"
        }
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/author-search",
      "@type": "EmbeddedDAGNode",
      "name": "author-search",
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/group-by-year",
        "error": "urn:noocodec:dag:the-archivist/node/compose-empty"
      },
      "dag": "urn:noocodec:dag:book-search-scatter",
      "stateMapping": {
        "output": {
          "terms": "terms",
          "toolPlan": "toolPlan",
          "candidates": "candidates",
          "shortlist": "shortlist",
          "priorContext": "priorContext",
          "failureCause": "failureCause"
        }
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/group-by-year",
      "@type": "SingleNode",
      "name": "group-by-year",
      "node": "urn:noocodec:node:group-by-year",
      "outputs": {
        "ordered": "urn:noocodec:dag:the-archivist/node/compose-loop"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-extract",
      "@type": "SingleNode",
      "name": "reviews-extract",
      "node": "urn:noocodec:node:extract-query",
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools",
        "retry": "urn:noocodec:dag:the-archivist/node/reviews-extract",
        "salvage": "urn:noocodec:dag:the-archivist/node/reviews-extract-salvage"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-extract-salvage",
      "@type": "SingleNode",
      "name": "reviews-extract-salvage",
      "node": "urn:noocodec:node:extract-query-salvage",
      "outputs": {
        "done": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools",
      "@type": "SingleNode",
      "name": "reviews-decide-tools",
      "node": "urn:noocodec:node:decide-tools",
      "outputs": {
        "tools": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets",
        "no-tools": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets",
        "retry": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools",
        "salvage": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools-salvage"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-decide-tools-salvage",
      "@type": "SingleNode",
      "name": "reviews-decide-tools-salvage",
      "node": "urn:noocodec:node:decide-tools-salvage",
      "outputs": {
        "done": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-build-worksets",
      "@type": "SingleNode",
      "name": "reviews-build-worksets",
      "node": "urn:noocodec:node:build-book-worksets",
      "outputs": {
        "ready": "urn:noocodec:dag:the-archivist/node/reviews-scatter"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-scatter",
      "@type": "ScatterNode",
      "name": "reviews-scatter",
      "source": "bookWorksets",
      "body": {
        "dag": {
          "@type": "DagReference",
          "from": "item",
          "path": "dagIri",
          "candidates": [
            "urn:noocodec:tool:web_search_books",
            "urn:noocodec:tool:google_books_search",
            "urn:noocodec:tool:subject_search",
            "urn:noocodec:tool:wikipedia_summary"
          ]
        }
      },
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/reviews-gather",
        "error": "urn:noocodec:dag:the-archivist/node/reviews-gather",
        "empty": "urn:noocodec:dag:the-archivist/node/reviews-rank"
      },
      "itemKey": "currentItem",
      "reducer": "any-success",
      "execution": {
        "mode": "item",
        "concurrency": 4
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-gather",
      "@type": "GatherNode",
      "name": "reviews-gather",
      "sources": {
        "urn:noocodec:dag:the-archivist/node/reviews-scatter": {}
      },
      "gather": {
        "strategy": "tool-candidate-merge"
      },
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/reviews-rank",
        "error": "urn:noocodec:dag:the-archivist/node/reviews-rank",
        "empty": "urn:noocodec:dag:the-archivist/node/reviews-rank"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-rank",
      "@type": "SingleNode",
      "name": "reviews-rank",
      "node": "urn:noocodec:node:rank-by-rating",
      "outputs": {
        "ranked": "urn:noocodec:dag:the-archivist/node/reviews-merge"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-merge",
      "@type": "SingleNode",
      "name": "reviews-merge",
      "node": "urn:noocodec:node:merge-candidates",
      "outputs": {
        "ranked": "urn:noocodec:dag:the-archivist/node/reviews-record",
        "empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-record",
      "@type": "SingleNode",
      "name": "reviews-record",
      "node": "urn:noocodec:node:record-findings",
      "outputs": {
        "recorded": "urn:noocodec:dag:the-archivist/node/reviews-gate"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-gate",
      "@type": "SingleNode",
      "name": "reviews-gate",
      "node": "urn:noocodec:node:has-citations-gate",
      "outputs": {
        "pass": "urn:noocodec:dag:the-archivist/node/reviews-recall",
        "fail": "urn:noocodec:dag:the-archivist/node/compose-empty"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/reviews-recall",
      "@type": "SingleNode",
      "name": "reviews-recall",
      "node": "urn:noocodec:node:recall-past-visits",
      "outputs": {
        "recalled": "urn:noocodec:dag:the-archivist/node/compose-loop"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-extract",
      "@type": "SingleNode",
      "name": "recommend-extract",
      "node": "urn:noocodec:node:extract-query",
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools",
        "retry": "urn:noocodec:dag:the-archivist/node/recommend-extract",
        "salvage": "urn:noocodec:dag:the-archivist/node/recommend-extract-salvage"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-extract-salvage",
      "@type": "SingleNode",
      "name": "recommend-extract-salvage",
      "node": "urn:noocodec:node:extract-query-salvage",
      "outputs": {
        "done": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools",
      "@type": "SingleNode",
      "name": "recommend-decide-tools",
      "node": "urn:noocodec:node:decide-tools",
      "outputs": {
        "tools": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets",
        "no-tools": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets",
        "retry": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools",
        "salvage": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools-salvage"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-decide-tools-salvage",
      "@type": "SingleNode",
      "name": "recommend-decide-tools-salvage",
      "node": "urn:noocodec:node:decide-tools-salvage",
      "outputs": {
        "done": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-build-worksets",
      "@type": "SingleNode",
      "name": "recommend-build-worksets",
      "node": "urn:noocodec:node:build-book-worksets",
      "outputs": {
        "ready": "urn:noocodec:dag:the-archivist/node/recommend-scatter"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-scatter",
      "@type": "ScatterNode",
      "name": "recommend-scatter",
      "source": "bookWorksets",
      "body": {
        "dag": {
          "@type": "DagReference",
          "from": "item",
          "path": "dagIri",
          "candidates": [
            "urn:noocodec:tool:web_search_books",
            "urn:noocodec:tool:google_books_search",
            "urn:noocodec:tool:subject_search",
            "urn:noocodec:tool:wikipedia_summary"
          ]
        }
      },
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/recommend-gather",
        "error": "urn:noocodec:dag:the-archivist/node/recommend-gather",
        "empty": "urn:noocodec:dag:the-archivist/node/recommend-rank"
      },
      "itemKey": "currentItem",
      "reducer": "any-success",
      "execution": {
        "mode": "item",
        "concurrency": 4
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-gather",
      "@type": "GatherNode",
      "name": "recommend-gather",
      "sources": {
        "urn:noocodec:dag:the-archivist/node/recommend-scatter": {}
      },
      "gather": {
        "strategy": "tool-candidate-merge"
      },
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/recommend-rank",
        "error": "urn:noocodec:dag:the-archivist/node/recommend-rank",
        "empty": "urn:noocodec:dag:the-archivist/node/recommend-rank"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-rank",
      "@type": "SingleNode",
      "name": "recommend-rank",
      "node": "urn:noocodec:node:rank-by-rating",
      "outputs": {
        "ranked": "urn:noocodec:dag:the-archivist/node/recommend-merge"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-merge",
      "@type": "SingleNode",
      "name": "recommend-merge",
      "node": "urn:noocodec:node:merge-candidates",
      "outputs": {
        "ranked": "urn:noocodec:dag:the-archivist/node/recommend-record",
        "empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-record",
      "@type": "SingleNode",
      "name": "recommend-record",
      "node": "urn:noocodec:node:record-findings",
      "outputs": {
        "recorded": "urn:noocodec:dag:the-archivist/node/recommend-gate"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-gate",
      "@type": "SingleNode",
      "name": "recommend-gate",
      "node": "urn:noocodec:node:has-citations-gate",
      "outputs": {
        "pass": "urn:noocodec:dag:the-archivist/node/recommend-recall",
        "fail": "urn:noocodec:dag:the-archivist/node/compose-empty"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-recall",
      "@type": "SingleNode",
      "name": "recommend-recall",
      "node": "urn:noocodec:node:recall-past-visits",
      "outputs": {
        "recalled": "urn:noocodec:dag:the-archivist/node/compose-loop"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-extract",
      "@type": "SingleNode",
      "name": "describe-extract",
      "node": "urn:noocodec:node:extract-query",
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/describe-decide-tools",
        "retry": "urn:noocodec:dag:the-archivist/node/describe-extract",
        "salvage": "urn:noocodec:dag:the-archivist/node/describe-extract-salvage"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-extract-salvage",
      "@type": "SingleNode",
      "name": "describe-extract-salvage",
      "node": "urn:noocodec:node:extract-query-salvage",
      "outputs": {
        "done": "urn:noocodec:dag:the-archivist/node/describe-decide-tools"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-decide-tools",
      "@type": "SingleNode",
      "name": "describe-decide-tools",
      "node": "urn:noocodec:node:decide-tools",
      "outputs": {
        "tools": "urn:noocodec:dag:the-archivist/node/describe-build-worksets",
        "no-tools": "urn:noocodec:dag:the-archivist/node/describe-build-worksets",
        "retry": "urn:noocodec:dag:the-archivist/node/describe-decide-tools",
        "salvage": "urn:noocodec:dag:the-archivist/node/describe-decide-tools-salvage"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-decide-tools-salvage",
      "@type": "SingleNode",
      "name": "describe-decide-tools-salvage",
      "node": "urn:noocodec:node:decide-tools-salvage",
      "outputs": {
        "done": "urn:noocodec:dag:the-archivist/node/describe-build-worksets"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-build-worksets",
      "@type": "SingleNode",
      "name": "describe-build-worksets",
      "node": "urn:noocodec:node:build-book-worksets",
      "outputs": {
        "ready": "urn:noocodec:dag:the-archivist/node/describe-scatter"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-scatter",
      "@type": "ScatterNode",
      "name": "describe-scatter",
      "source": "bookWorksets",
      "body": {
        "dag": {
          "@type": "DagReference",
          "from": "item",
          "path": "dagIri",
          "candidates": [
            "urn:noocodec:tool:web_search_books",
            "urn:noocodec:tool:google_books_search",
            "urn:noocodec:tool:subject_search",
            "urn:noocodec:tool:wikipedia_summary"
          ]
        }
      },
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/describe-gather",
        "error": "urn:noocodec:dag:the-archivist/node/compose-empty",
        "empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
      },
      "itemKey": "currentItem",
      "reducer": "any-success",
      "execution": {
        "mode": "item",
        "concurrency": 4
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-gather",
      "@type": "GatherNode",
      "name": "describe-gather",
      "sources": {
        "urn:noocodec:dag:the-archivist/node/describe-scatter": {}
      },
      "gather": {
        "strategy": "tool-candidate-merge"
      },
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/describe-pick",
        "error": "urn:noocodec:dag:the-archivist/node/compose-empty",
        "empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-pick",
      "@type": "SingleNode",
      "name": "describe-pick",
      "node": "urn:noocodec:node:pick-best-match",
      "outputs": {
        "picked": "urn:noocodec:dag:the-archivist/node/describe-merge"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-merge",
      "@type": "SingleNode",
      "name": "describe-merge",
      "node": "urn:noocodec:node:merge-candidates",
      "outputs": {
        "ranked": "urn:noocodec:dag:the-archivist/node/describe-record",
        "empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-record",
      "@type": "SingleNode",
      "name": "describe-record",
      "node": "urn:noocodec:node:record-findings",
      "outputs": {
        "recorded": "urn:noocodec:dag:the-archivist/node/describe-gate"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-gate",
      "@type": "SingleNode",
      "name": "describe-gate",
      "node": "urn:noocodec:node:has-citations-gate",
      "outputs": {
        "pass": "urn:noocodec:dag:the-archivist/node/describe-recall",
        "fail": "urn:noocodec:dag:the-archivist/node/compose-empty"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/describe-recall",
      "@type": "SingleNode",
      "name": "describe-recall",
      "node": "urn:noocodec:node:recall-past-visits",
      "outputs": {
        "recalled": "urn:noocodec:dag:the-archivist/node/compose-loop"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/recommend-similar",
      "@type": "SingleNode",
      "name": "recommend-similar",
      "node": "urn:noocodec:node:recommend-similar",
      "outputs": {
        "seeded": "urn:noocodec:dag:the-archivist/node/similar-search",
        "empty": "urn:noocodec:dag:the-archivist/node/compose-empty"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/similar-search",
      "@type": "EmbeddedDAGNode",
      "name": "similar-search",
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/compose-loop",
        "error": "urn:noocodec:dag:the-archivist/node/compose-empty"
      },
      "dag": "urn:noocodec:dag:book-search-scatter",
      "stateMapping": {
        "output": {
          "terms": "terms",
          "toolPlan": "toolPlan",
          "candidates": "candidates",
          "shortlist": "shortlist",
          "priorContext": "priorContext",
          "failureCause": "failureCause"
        }
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/compose-loop",
      "@type": "EmbeddedDAGNode",
      "name": "compose-loop",
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
        "error": "urn:noocodec:dag:the-archivist/node/compose-empty"
      },
      "dag": "urn:noocodec:dag:compose-retry-loop",
      "stateMapping": {
        "output": {
          "draft": "draft",
          "approvalState": "approvalState"
        }
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
      "@type": "SingleNode",
      "name": "respond-to-visitor",
      "node": "urn:noocodec:node:respond-to-visitor",
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/end"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/memory-recall",
      "@type": "SingleNode",
      "name": "memory-recall",
      "node": "urn:noocodec:node:recall-memories",
      "outputs": {
        "recalled": "urn:noocodec:dag:the-archivist/node/compose-memory-recall"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/compose-memory-recall",
      "@type": "SingleNode",
      "name": "compose-memory-recall",
      "node": "urn:noocodec:node:compose-memory-response",
      "outputs": {
        "drafted": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
        "retry": "urn:noocodec:dag:the-archivist/node/compose-memory-recall",
        "salvage": "urn:noocodec:dag:the-archivist/node/compose-memory-salvage"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/compose-memory-salvage",
      "@type": "SingleNode",
      "name": "compose-memory-salvage",
      "node": "urn:noocodec:node:compose-memory-salvage",
      "outputs": {
        "done": "urn:noocodec:dag:the-archivist/node/respond-to-visitor"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/decline-off-topic",
      "@type": "SingleNode",
      "name": "decline-off-topic",
      "node": "urn:noocodec:node:decline-off-topic",
      "outputs": {
        "success": "urn:noocodec:dag:the-archivist/node/end"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/compose-empty",
      "@type": "SingleNode",
      "name": "compose-empty",
      "node": "urn:noocodec:node:compose-empty",
      "outputs": {
        "drafted": "urn:noocodec:dag:the-archivist/node/respond-to-visitor",
        "retry": "urn:noocodec:dag:the-archivist/node/compose-empty",
        "salvage": "urn:noocodec:dag:the-archivist/node/compose-empty-salvage"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/node/compose-empty-salvage",
      "@type": "SingleNode",
      "name": "compose-empty-salvage",
      "node": "urn:noocodec:node:compose-empty-salvage",
      "outputs": {
        "done": "urn:noocodec:dag:the-archivist/node/respond-to-visitor"
      }
    },
    {
      "@id": "urn:noocodec:dag:the-archivist/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
  %% the-archivist (v6.0)
  entry_main(["main"])
  entry_main --> urn_noocodec_dag_the-archivist/node/park-for-input
  urn_noocodec_dag_the-archivist/node/setup(["setup (pre)"])
  urn_noocodec_dag_the-archivist/node/park-for-input["park-for-input"]
  urn_noocodec_dag_the-archivist/node/park-for-input -->|parked| urn_noocodec_dag_the-archivist/node/park-for-input
  urn_noocodec_dag_the-archivist/node/park-for-input -->|resumed| urn_noocodec_dag_the-archivist/node/recall-context
  urn_noocodec_dag_the-archivist/node/recall-context["recall-context"]
  urn_noocodec_dag_the-archivist/node/recall-context -->|recalled| urn_noocodec_dag_the-archivist/node/classify-intent
  urn_noocodec_dag_the-archivist/node/classify-intent["classify-intent"]
  urn_noocodec_dag_the-archivist/node/classify-intent -->|lookup-author| urn_noocodec_dag_the-archivist/node/author-search
  urn_noocodec_dag_the-archivist/node/classify-intent -->|find-reviews| urn_noocodec_dag_the-archivist/node/reviews-extract
  urn_noocodec_dag_the-archivist/node/classify-intent -->|describe-book| urn_noocodec_dag_the-archivist/node/describe-extract
  urn_noocodec_dag_the-archivist/node/classify-intent -->|recommend-similar| urn_noocodec_dag_the-archivist/node/recommend-similar
  urn_noocodec_dag_the-archivist/node/classify-intent -->|recall-memories| urn_noocodec_dag_the-archivist/node/memory-recall
  urn_noocodec_dag_the-archivist/node/classify-intent -->|on-topic| urn_noocodec_dag_the-archivist/node/on-topic-search
  urn_noocodec_dag_the-archivist/node/classify-intent -->|recommend-top-rated| urn_noocodec_dag_the-archivist/node/recommend-extract
  urn_noocodec_dag_the-archivist/node/classify-intent -->|off-topic| urn_noocodec_dag_the-archivist/node/decline-off-topic
  urn_noocodec_dag_the-archivist/node/classify-intent -->|retry| urn_noocodec_dag_the-archivist/node/classify-intent
  urn_noocodec_dag_the-archivist/node/classify-intent -->|salvage| urn_noocodec_dag_the-archivist/node/classify-intent-salvage
  urn_noocodec_dag_the-archivist/node/classify-intent-salvage["classify-intent-salvage"]
  urn_noocodec_dag_the-archivist/node/classify-intent-salvage -->|done| urn_noocodec_dag_the-archivist/node/on-topic-search
  urn_noocodec_dag_the-archivist/node/on-topic-search[["on-topic-search"]]
  urn_noocodec_dag_the-archivist/node/on-topic-search -->|success| urn_noocodec_dag_the-archivist/node/compose-loop
  urn_noocodec_dag_the-archivist/node/on-topic-search -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/author-search[["author-search"]]
  urn_noocodec_dag_the-archivist/node/author-search -->|success| urn_noocodec_dag_the-archivist/node/group-by-year
  urn_noocodec_dag_the-archivist/node/author-search -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/group-by-year["group-by-year"]
  urn_noocodec_dag_the-archivist/node/group-by-year -->|ordered| urn_noocodec_dag_the-archivist/node/compose-loop
  urn_noocodec_dag_the-archivist/node/reviews-extract["reviews-extract"]
  urn_noocodec_dag_the-archivist/node/reviews-extract -->|success| urn_noocodec_dag_the-archivist/node/reviews-decide-tools
  urn_noocodec_dag_the-archivist/node/reviews-extract -->|retry| urn_noocodec_dag_the-archivist/node/reviews-extract
  urn_noocodec_dag_the-archivist/node/reviews-extract -->|salvage| urn_noocodec_dag_the-archivist/node/reviews-extract-salvage
  urn_noocodec_dag_the-archivist/node/reviews-extract-salvage["reviews-extract-salvage"]
  urn_noocodec_dag_the-archivist/node/reviews-extract-salvage -->|done| urn_noocodec_dag_the-archivist/node/reviews-decide-tools
  urn_noocodec_dag_the-archivist/node/reviews-decide-tools["reviews-decide-tools"]
  urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|tools| urn_noocodec_dag_the-archivist/node/reviews-build-worksets
  urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|no-tools| urn_noocodec_dag_the-archivist/node/reviews-build-worksets
  urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|retry| urn_noocodec_dag_the-archivist/node/reviews-decide-tools
  urn_noocodec_dag_the-archivist/node/reviews-decide-tools -->|salvage| urn_noocodec_dag_the-archivist/node/reviews-decide-tools-salvage
  urn_noocodec_dag_the-archivist/node/reviews-decide-tools-salvage["reviews-decide-tools-salvage"]
  urn_noocodec_dag_the-archivist/node/reviews-decide-tools-salvage -->|done| urn_noocodec_dag_the-archivist/node/reviews-build-worksets
  urn_noocodec_dag_the-archivist/node/reviews-build-worksets["reviews-build-worksets"]
  urn_noocodec_dag_the-archivist/node/reviews-build-worksets -->|ready| urn_noocodec_dag_the-archivist/node/reviews-scatter
  urn_noocodec_dag_the-archivist/node/reviews-scatter[/"reviews-scatter"/]
  urn_noocodec_dag_the-archivist/node/reviews-scatter -->|success| urn_noocodec_dag_the-archivist/node/reviews-gather
  urn_noocodec_dag_the-archivist/node/reviews-scatter -->|error| urn_noocodec_dag_the-archivist/node/reviews-gather
  urn_noocodec_dag_the-archivist/node/reviews-scatter -->|empty| urn_noocodec_dag_the-archivist/node/reviews-rank
  urn_noocodec_dag_the-archivist/node/reviews-gather{"reviews-gather"}
  urn_noocodec_dag_the-archivist/node/reviews-gather -->|success| urn_noocodec_dag_the-archivist/node/reviews-rank
  urn_noocodec_dag_the-archivist/node/reviews-gather -->|error| urn_noocodec_dag_the-archivist/node/reviews-rank
  urn_noocodec_dag_the-archivist/node/reviews-gather -->|empty| urn_noocodec_dag_the-archivist/node/reviews-rank
  urn_noocodec_dag_the-archivist/node/reviews-rank["reviews-rank"]
  urn_noocodec_dag_the-archivist/node/reviews-rank -->|ranked| urn_noocodec_dag_the-archivist/node/reviews-merge
  urn_noocodec_dag_the-archivist/node/reviews-merge["reviews-merge"]
  urn_noocodec_dag_the-archivist/node/reviews-merge -->|ranked| urn_noocodec_dag_the-archivist/node/reviews-record
  urn_noocodec_dag_the-archivist/node/reviews-merge -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/reviews-record["reviews-record"]
  urn_noocodec_dag_the-archivist/node/reviews-record -->|recorded| urn_noocodec_dag_the-archivist/node/reviews-gate
  urn_noocodec_dag_the-archivist/node/reviews-gate["reviews-gate"]
  urn_noocodec_dag_the-archivist/node/reviews-gate -->|pass| urn_noocodec_dag_the-archivist/node/reviews-recall
  urn_noocodec_dag_the-archivist/node/reviews-gate -->|fail| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/reviews-recall["reviews-recall"]
  urn_noocodec_dag_the-archivist/node/reviews-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-loop
  urn_noocodec_dag_the-archivist/node/recommend-extract["recommend-extract"]
  urn_noocodec_dag_the-archivist/node/recommend-extract -->|success| urn_noocodec_dag_the-archivist/node/recommend-decide-tools
  urn_noocodec_dag_the-archivist/node/recommend-extract -->|retry| urn_noocodec_dag_the-archivist/node/recommend-extract
  urn_noocodec_dag_the-archivist/node/recommend-extract -->|salvage| urn_noocodec_dag_the-archivist/node/recommend-extract-salvage
  urn_noocodec_dag_the-archivist/node/recommend-extract-salvage["recommend-extract-salvage"]
  urn_noocodec_dag_the-archivist/node/recommend-extract-salvage -->|done| urn_noocodec_dag_the-archivist/node/recommend-decide-tools
  urn_noocodec_dag_the-archivist/node/recommend-decide-tools["recommend-decide-tools"]
  urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|tools| urn_noocodec_dag_the-archivist/node/recommend-build-worksets
  urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|no-tools| urn_noocodec_dag_the-archivist/node/recommend-build-worksets
  urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|retry| urn_noocodec_dag_the-archivist/node/recommend-decide-tools
  urn_noocodec_dag_the-archivist/node/recommend-decide-tools -->|salvage| urn_noocodec_dag_the-archivist/node/recommend-decide-tools-salvage
  urn_noocodec_dag_the-archivist/node/recommend-decide-tools-salvage["recommend-decide-tools-salvage"]
  urn_noocodec_dag_the-archivist/node/recommend-decide-tools-salvage -->|done| urn_noocodec_dag_the-archivist/node/recommend-build-worksets
  urn_noocodec_dag_the-archivist/node/recommend-build-worksets["recommend-build-worksets"]
  urn_noocodec_dag_the-archivist/node/recommend-build-worksets -->|ready| urn_noocodec_dag_the-archivist/node/recommend-scatter
  urn_noocodec_dag_the-archivist/node/recommend-scatter[/"recommend-scatter"/]
  urn_noocodec_dag_the-archivist/node/recommend-scatter -->|success| urn_noocodec_dag_the-archivist/node/recommend-gather
  urn_noocodec_dag_the-archivist/node/recommend-scatter -->|error| urn_noocodec_dag_the-archivist/node/recommend-gather
  urn_noocodec_dag_the-archivist/node/recommend-scatter -->|empty| urn_noocodec_dag_the-archivist/node/recommend-rank
  urn_noocodec_dag_the-archivist/node/recommend-gather{"recommend-gather"}
  urn_noocodec_dag_the-archivist/node/recommend-gather -->|success| urn_noocodec_dag_the-archivist/node/recommend-rank
  urn_noocodec_dag_the-archivist/node/recommend-gather -->|error| urn_noocodec_dag_the-archivist/node/recommend-rank
  urn_noocodec_dag_the-archivist/node/recommend-gather -->|empty| urn_noocodec_dag_the-archivist/node/recommend-rank
  urn_noocodec_dag_the-archivist/node/recommend-rank["recommend-rank"]
  urn_noocodec_dag_the-archivist/node/recommend-rank -->|ranked| urn_noocodec_dag_the-archivist/node/recommend-merge
  urn_noocodec_dag_the-archivist/node/recommend-merge["recommend-merge"]
  urn_noocodec_dag_the-archivist/node/recommend-merge -->|ranked| urn_noocodec_dag_the-archivist/node/recommend-record
  urn_noocodec_dag_the-archivist/node/recommend-merge -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/recommend-record["recommend-record"]
  urn_noocodec_dag_the-archivist/node/recommend-record -->|recorded| urn_noocodec_dag_the-archivist/node/recommend-gate
  urn_noocodec_dag_the-archivist/node/recommend-gate["recommend-gate"]
  urn_noocodec_dag_the-archivist/node/recommend-gate -->|pass| urn_noocodec_dag_the-archivist/node/recommend-recall
  urn_noocodec_dag_the-archivist/node/recommend-gate -->|fail| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/recommend-recall["recommend-recall"]
  urn_noocodec_dag_the-archivist/node/recommend-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-loop
  urn_noocodec_dag_the-archivist/node/describe-extract["describe-extract"]
  urn_noocodec_dag_the-archivist/node/describe-extract -->|success| urn_noocodec_dag_the-archivist/node/describe-decide-tools
  urn_noocodec_dag_the-archivist/node/describe-extract -->|retry| urn_noocodec_dag_the-archivist/node/describe-extract
  urn_noocodec_dag_the-archivist/node/describe-extract -->|salvage| urn_noocodec_dag_the-archivist/node/describe-extract-salvage
  urn_noocodec_dag_the-archivist/node/describe-extract-salvage["describe-extract-salvage"]
  urn_noocodec_dag_the-archivist/node/describe-extract-salvage -->|done| urn_noocodec_dag_the-archivist/node/describe-decide-tools
  urn_noocodec_dag_the-archivist/node/describe-decide-tools["describe-decide-tools"]
  urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|tools| urn_noocodec_dag_the-archivist/node/describe-build-worksets
  urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|no-tools| urn_noocodec_dag_the-archivist/node/describe-build-worksets
  urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|retry| urn_noocodec_dag_the-archivist/node/describe-decide-tools
  urn_noocodec_dag_the-archivist/node/describe-decide-tools -->|salvage| urn_noocodec_dag_the-archivist/node/describe-decide-tools-salvage
  urn_noocodec_dag_the-archivist/node/describe-decide-tools-salvage["describe-decide-tools-salvage"]
  urn_noocodec_dag_the-archivist/node/describe-decide-tools-salvage -->|done| urn_noocodec_dag_the-archivist/node/describe-build-worksets
  urn_noocodec_dag_the-archivist/node/describe-build-worksets["describe-build-worksets"]
  urn_noocodec_dag_the-archivist/node/describe-build-worksets -->|ready| urn_noocodec_dag_the-archivist/node/describe-scatter
  urn_noocodec_dag_the-archivist/node/describe-scatter[/"describe-scatter"/]
  urn_noocodec_dag_the-archivist/node/describe-scatter -->|success| urn_noocodec_dag_the-archivist/node/describe-gather
  urn_noocodec_dag_the-archivist/node/describe-scatter -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/describe-scatter -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/describe-gather{"describe-gather"}
  urn_noocodec_dag_the-archivist/node/describe-gather -->|success| urn_noocodec_dag_the-archivist/node/describe-pick
  urn_noocodec_dag_the-archivist/node/describe-gather -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/describe-gather -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/describe-pick["describe-pick"]
  urn_noocodec_dag_the-archivist/node/describe-pick -->|picked| urn_noocodec_dag_the-archivist/node/describe-merge
  urn_noocodec_dag_the-archivist/node/describe-merge["describe-merge"]
  urn_noocodec_dag_the-archivist/node/describe-merge -->|ranked| urn_noocodec_dag_the-archivist/node/describe-record
  urn_noocodec_dag_the-archivist/node/describe-merge -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/describe-record["describe-record"]
  urn_noocodec_dag_the-archivist/node/describe-record -->|recorded| urn_noocodec_dag_the-archivist/node/describe-gate
  urn_noocodec_dag_the-archivist/node/describe-gate["describe-gate"]
  urn_noocodec_dag_the-archivist/node/describe-gate -->|pass| urn_noocodec_dag_the-archivist/node/describe-recall
  urn_noocodec_dag_the-archivist/node/describe-gate -->|fail| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/describe-recall["describe-recall"]
  urn_noocodec_dag_the-archivist/node/describe-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-loop
  urn_noocodec_dag_the-archivist/node/recommend-similar["recommend-similar"]
  urn_noocodec_dag_the-archivist/node/recommend-similar -->|seeded| urn_noocodec_dag_the-archivist/node/similar-search
  urn_noocodec_dag_the-archivist/node/recommend-similar -->|empty| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/similar-search[["similar-search"]]
  urn_noocodec_dag_the-archivist/node/similar-search -->|success| urn_noocodec_dag_the-archivist/node/compose-loop
  urn_noocodec_dag_the-archivist/node/similar-search -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/compose-loop[["compose-loop"]]
  urn_noocodec_dag_the-archivist/node/compose-loop -->|success| urn_noocodec_dag_the-archivist/node/respond-to-visitor
  urn_noocodec_dag_the-archivist/node/compose-loop -->|error| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/respond-to-visitor["respond-to-visitor"]
  urn_noocodec_dag_the-archivist/node/respond-to-visitor -->|success| urn_noocodec_dag_the-archivist/node/end
  urn_noocodec_dag_the-archivist/node/memory-recall["memory-recall"]
  urn_noocodec_dag_the-archivist/node/memory-recall -->|recalled| urn_noocodec_dag_the-archivist/node/compose-memory-recall
  urn_noocodec_dag_the-archivist/node/compose-memory-recall["compose-memory-recall"]
  urn_noocodec_dag_the-archivist/node/compose-memory-recall -->|drafted| urn_noocodec_dag_the-archivist/node/respond-to-visitor
  urn_noocodec_dag_the-archivist/node/compose-memory-recall -->|retry| urn_noocodec_dag_the-archivist/node/compose-memory-recall
  urn_noocodec_dag_the-archivist/node/compose-memory-recall -->|salvage| urn_noocodec_dag_the-archivist/node/compose-memory-salvage
  urn_noocodec_dag_the-archivist/node/compose-memory-salvage["compose-memory-salvage"]
  urn_noocodec_dag_the-archivist/node/compose-memory-salvage -->|done| urn_noocodec_dag_the-archivist/node/respond-to-visitor
  urn_noocodec_dag_the-archivist/node/decline-off-topic["decline-off-topic"]
  urn_noocodec_dag_the-archivist/node/decline-off-topic -->|success| urn_noocodec_dag_the-archivist/node/end
  urn_noocodec_dag_the-archivist/node/compose-empty["compose-empty"]
  urn_noocodec_dag_the-archivist/node/compose-empty -->|drafted| urn_noocodec_dag_the-archivist/node/respond-to-visitor
  urn_noocodec_dag_the-archivist/node/compose-empty -->|retry| urn_noocodec_dag_the-archivist/node/compose-empty
  urn_noocodec_dag_the-archivist/node/compose-empty -->|salvage| urn_noocodec_dag_the-archivist/node/compose-empty-salvage
  urn_noocodec_dag_the-archivist/node/compose-empty-salvage["compose-empty-salvage"]
  urn_noocodec_dag_the-archivist/node/compose-empty-salvage -->|done| urn_noocodec_dag_the-archivist/node/respond-to-visitor
  urn_noocodec_dag_the-archivist/node/end((("end")))

Run

bash
npx tsx examples/the-archivist/runArchivist.ts

What It Lets You Do

DAGBuilder lets teams keep graph authoring close to the node implementations while still shipping a portable JSON-LD artifact. It is the right tool when the DAG lives in TypeScript source and reviewers need compile-time help with route coverage.

Code Samples

This code is the builder-authored Archivist DAG. Read it as a route map first and TypeScript second: every chained call becomes one JSON-LD placement, and the final .build() returns the document rendered above.

Code

The complete archivistDAG, the parent DAG as a single DAGBuilder chain. The full source file includes inline branches for reviews and describe (which use distinct post-scout ranking steps):

ts
/**
 * The Archivist: canonical DAG, built with DAGBuilder. Version 6.0.
 *
 * Molecular composition: the parent DAG is composed of two reusable
 * sub-DAGs that ship as independent components and are imported as
 * `.embed(name, dagIri, routes)` placements. The sub-DAGs are registered
 * separately and referenced by canonical IRI; the parent DAG never knows
 * their internals.
 *
 *   recall-context
 *     └─ recalled ──► classify-intent
 *
 *   classify-intent
 *     ├─ off-topic         ──► decline-off-topic ──► END
 *     │
 *     ├─ on-topic          ──► [book-search-scatter] (extract+decide+4scouts+rank+merge+record+gate+recall)
 *     │                             ├─ success ──► [compose-retry-loop] (compose+validate+retry)
 *     │                             └─ error   ──► compose-empty ──┐
 *     │                                                             │
 *     ├─ lookup-author     ──► [book-search-scatter]                │
 *     │                             ├─ success ──► group-by-year ──► [compose-retry-loop]
 *     │                             └─ error   ──► compose-empty ──┐
 *
 *     │                                                             ▼
 *     ├─ find-reviews      ──► reviews-extract ──►  [compose-retry-loop] (success) ──► respond-to-visitor ──► END
 *     │                             (inline: decide+4scouts+rankByRating+merge+record+gate+recall)       ▲
 *     │                                                             ▲
 *     ├─ describe-book     ──► describe-extract ──► [compose-retry-loop]
 *     │                             (inline: decide+4scouts+pickBestMatch+merge+record+gate+recall)
 *     │
 *     ├─ recommend         ──► recommend-extract ──►  [compose-retry-loop] (success) ──► respond-to-visitor ──► END
 *     │                             (inline: decide+4scouts+rankByRating+merge+record+gate+recall)        ▲
 *     │                                                             ▲
 *     ├─ recall-memories   ──► memory-recall ──► compose-memory-recall ──────────────────────────────┐
 *     │                                                                                               ▼
 *     └─ recommend-similar ──► recommend-similar-gate                                  respond-to-visitor ──► END
 *                               ├─ seeded ──► [book-search-scatter]                                  ▲
 *                               │                ├─ success ──► [compose-retry-loop] (success) ──────┘
 *                               │                └─ error   ──► compose-empty ──────────────────────►┘
 *                               └─ empty  ──► compose-empty ───────────────────────────────────────►┘
 *
 * Convergence policy (v6.0): all response-producing branches converge into ONE
 * shared `respond-to-visitor` terminal at this (parent) level. The
 * compose-retry-loop embedded-DAG exits with `success` after producing state.draft
 * and does NOT contain respondToVisitor internally. This ensures exactly one
 * terminal node fires per run with the full converged state.draft.
 *
 * Embedded-DAGs (molecular components):
 *   book-search-scatter: extract-query + decide-tools + 4-source parallel scouts
 *                         (OpenLibrary, Google Books, Subject, Wikipedia) + rankCandidates
 *                         + mergeCandidates + recordFindings + hasCitationsGate +
 *                         recallPastVisits. Three placements in this DAG:
 *                         on-topic-search, author-search, similar-search.
 *
 *   compose-retry-loop: composeResponse + validateResponse (with bounded retry loop)
 *                         + respondToVisitor. Four placements in this DAG:
 *                         compose-loop (shared by all four convergent branches).
 *
 * Inlined branches (reviews, describe, recommend-top-rated):
 *   Reviews and recommend-top-rated both use `rankByRating` (deterministic,
 *   rating-weighted) instead of `rankCandidates` (LLM-driven); recommend-top-rated
 *   is the structural sibling of find-reviews, reusing the same node objects at
 *   `recommend-*` placements, for the vague "good book / good story" recommend
 *   intent that has no topic to rank by relevance. Describe uses `pickBestMatch`
 *   to narrow to the top-3 title-similar candidates before merge. All three are
 *   structurally identical to book-search-scatter except for the post-scout
 *   ranking step; keeping them inline makes the intentional distinction explicit
 *   rather than hiding it behind a embedded-DAG parameter.
 *
 * Empty-result handling (v5.2):
 *   Empty results route through `compose-empty` → `respond-to-visitor`.
 *   `compose-empty` calls the LLM with `state.failureCause` (accumulated by
 *   scouts) to produce an in-character message that acknowledges what was
 *   searched and offers a concrete next step.
 *
 * Builder output shape:
 *   DAGBuilder.node(name, dagNode, routes) emits a
 *   { type: 'single', name, node: dagNode.name, outputs: routes }
 *   object. build() returns a plain DAG passed straight to
 *   DAGDocument.load().
 *
 * DAG containment (WorkerThreadContainer) — why the archivist stays in-process:
 *   The container/worker feature is most natural for CPU-bound, self-contained
 *   per-item work: pure data transforms that only read from state and write a
 *   result back (see the-cartographer, which routes its canonical-event enrichment
 *   sub-DAG through a WorkerThreadContainer — haversine, GDPR redaction, pricing
 *   and ETA are pure arithmetic on serialisable data).
 *
 *   The archivist's scatter items (the four scout providers) are LLM / network
 *   bound: each clone calls a live language model and external book APIs. Workers
 *   cannot share the LLM provider instance (it is not serialisable), and each
 *   scatter item's payload — prompt context, live API credentials, LLM adapter
 *   state — crosses the worker message channel as structured-clone, which drops
 *   functions, closures, class instances, and streams.
 *
 *   Worker containment suits CPU / data DAGs. LLM-cascade DAGs — where nodes
 *   carry network clients, streaming responses, and rich provider objects — run
 *   in-process and rely on async concurrency (Promise.all, scatter concurrency)
 *   rather than OS-level thread isolation for parallelism.
 */


import type { ArchivistState } from './ArchivistState.ts';
import './nodes/scouts.ts'; // registers 'tool-candidate-merge' gather strategy

import { DAGBuilder, DAGIdentity, PlaceholderNode } from '@studnicky/dagonizer';
import type { DAGType } from '@studnicky/dagonizer';

// #region dispatcher-bundle
//
// IRI identity: DAGBuilder embeds the canonical DAG_CONTEXT in every built
// DAG's `@context` field. The archivist uses explicit placement IRIs via
// DAGIdentity.placementId(dagIri, placementIdentifier) so route targets,
// gather sources, and entrypoint wiring all stay on canonical IRIs while the
// placement `name` field remains display-only.
//
// @id values on each node placement follow the urn:noocodec:dag:<dagName>/node/<placementName>
// convention produced by DAGIdentity.placementId(), e.g.:
//   urn:noocodec:dag:the-archivist/node/recall-context
//
// A plugin shipping nodes under its own namespace would declare a prefix in
// the bundle's `context` field — e.g. { context: { archivist: 'https://archivist.example.com/' } }
// — and use prefixed names like 'archivist:recallContext' to prevent collisions
// with other plugins that might register a node named 'recallContext'.
// See docs/guide/iri-identity.md for the full expansion rule set.
const BOOK_SEARCH_TOOL_DAGS = [
  'urn:noocodec:tool:web_search_books',
  'urn:noocodec:tool:google_books_search',
  'urn:noocodec:tool:subject_search',
  'urn:noocodec:tool:wikipedia_summary',
] as const;

const ARCHIVIST_DAG_IRI = 'urn:noocodec:dag:the-archivist';
const BOOK_SEARCH_SCATTER_DAG_IRI = 'urn:noocodec:dag:book-search-scatter';
const COMPOSE_RETRY_LOOP_DAG_IRI = 'urn:noocodec:dag:compose-retry-loop';
const placement = (placementIdentifier: string): string => DAGIdentity.placementId(ARCHIVIST_DAG_IRI, placementIdentifier);
const display = <T extends string>(name: T): { name: T } => ({ name });

const nodes = {
  'preRunSetup': new PlaceholderNode<ArchivistState, 'ready'>('urn:noocodec:node:pre-run-setup', ['ready']),
  'parkForInput': new PlaceholderNode<ArchivistState, 'parked' | 'resumed'>('urn:noocodec:node:park-for-input', ['parked', 'resumed']),
  'recallContext': new PlaceholderNode<ArchivistState, 'recalled'>('urn:noocodec:node:recall-context', ['recalled']),
  'classifyIntent': new PlaceholderNode<ArchivistState, 'lookup-author' | 'find-reviews' | 'describe-book' | 'recommend-similar' | 'recall-memories' | 'on-topic' | 'recommend-top-rated' | 'off-topic' | 'retry' | 'salvage'>('urn:noocodec:node:classify-intent', ['lookup-author', 'find-reviews', 'describe-book', 'recommend-similar', 'recall-memories', 'on-topic', 'recommend-top-rated', 'off-topic', 'retry', 'salvage']),
  'classifyIntentSalvage': new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:classify-intent-salvage', ['done']),
  'extractQuery': new PlaceholderNode<ArchivistState, 'success' | 'retry' | 'salvage'>('urn:noocodec:node:extract-query', ['success', 'retry', 'salvage']),
  'extractQuerySalvage': new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:extract-query-salvage', ['done']),
  'decideTools': new PlaceholderNode<ArchivistState, 'tools' | 'no-tools' | 'retry' | 'salvage'>('urn:noocodec:node:decide-tools', ['tools', 'no-tools', 'retry', 'salvage']),
  'decideToolsSalvage': new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:decide-tools-salvage', ['done']),
  'buildBookWorksets': new PlaceholderNode<ArchivistState, 'ready'>('urn:noocodec:node:build-book-worksets', ['ready']),
  'rankByRating': new PlaceholderNode<ArchivistState, 'ranked'>('urn:noocodec:node:rank-by-rating', ['ranked']),
  'pickBestMatch': new PlaceholderNode<ArchivistState, 'picked'>('urn:noocodec:node:pick-best-match', ['picked']),
  'mergeCandidates': new PlaceholderNode<ArchivistState, 'ranked' | 'empty'>('urn:noocodec:node:merge-candidates', ['ranked', 'empty']),
  'recordFindings': new PlaceholderNode<ArchivistState, 'recorded'>('urn:noocodec:node:record-findings', ['recorded']),
  'hasCitationsGate': new PlaceholderNode<ArchivistState, 'pass' | 'fail'>('urn:noocodec:node:has-citations-gate', ['pass', 'fail']),
  'groupByYear': new PlaceholderNode<ArchivistState, 'ordered'>('urn:noocodec:node:group-by-year', ['ordered']),
  'recallPastVisits': new PlaceholderNode<ArchivistState, 'recalled'>('urn:noocodec:node:recall-past-visits', ['recalled']),
  'recommendSimilar': new PlaceholderNode<ArchivistState, 'seeded' | 'empty'>('urn:noocodec:node:recommend-similar', ['seeded', 'empty']),
  'recallMemories': new PlaceholderNode<ArchivistState, 'recalled'>('urn:noocodec:node:recall-memories', ['recalled']),
  'composeMemoryResponse': new PlaceholderNode<ArchivistState, 'drafted' | 'retry' | 'salvage'>('urn:noocodec:node:compose-memory-response', ['drafted', 'retry', 'salvage']),
  'composeMemoryResponseSalvage': new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:compose-memory-salvage', ['done']),
  'respondToVisitor': new PlaceholderNode<ArchivistState, 'success'>('urn:noocodec:node:respond-to-visitor', ['success']),
  'declineOffTopic': new PlaceholderNode<ArchivistState, 'success'>('urn:noocodec:node:decline-off-topic', ['success']),
  'composeEmptyResponse': new PlaceholderNode<ArchivistState, 'drafted' | 'retry' | 'salvage'>('urn:noocodec:node:compose-empty', ['drafted', 'retry', 'salvage']),
  'composeEmptyResponseSalvage': new PlaceholderNode<ArchivistState, 'done'>('urn:noocodec:node:compose-empty-salvage', ['done']),
} as const;

export const archivistDAG: DAGType = new DAGBuilder(ARCHIVIST_DAG_IRI, '6.0', display('the-archivist'))

      // ── pre-phase: setup ─────────────────────────────────────────────────────
      // Stamps state.runId and clears any stale draft before the main loop starts.
      // PhaseNode placement: runs before the entrypoint node; errors abort the run.
      // No routing: phase placements are out-of-band and never set the entrypoint.
      .phase(placement('setup'), 'pre', nodes.preRunSetup, display('setup'))

      // ── 0. park-for-input (HITL gate) ────────────────────────────────────────
      // First added → auto-entrypoint. Parks the flow when state.query is empty,
      // waiting for the human to supply input via the browser HITL banner. On
      // resume, `state.query` is set by the caller before `dispatcher.resume()`;
      // the node then routes `'resumed'` and proceeds to `recall-context`.
      // The `'parked'` output routes to the engine park machinery (null wiring).
      .node(placement('park-for-input'), nodes.parkForInput, {
        'parked':  placement('park-for-input'),
        'resumed': placement('recall-context'),
      }, display('park-for-input'))

      // ── 1. recall-context ────────────────────────────────────────────────────
      // Runs before classifyIntent so the classifier can benefit from
      // prior-session continuity hints.
      .node(placement('recall-context'), nodes.recallContext, {
        'recalled': placement('classify-intent'),
      }, display('recall-context'))

      // #region intent-routes
      // ── 1. classify-intent ───────────────────────────────────────────────────
      // Wide output union routes to seven branches. EmbeddedDAG placements and inline
      // branches share the same shared terminal: compose-loop and compose-empty.
      // recall-memories routes directly to memory-recall → compose-memory-recall
      // → memory-respond (no search needed; the memory store is the source).
      .node(placement('classify-intent'), nodes.classifyIntent, {
        'lookup-author':        placement('author-search'),
        'find-reviews':         placement('reviews-extract'),
        'describe-book':        placement('describe-extract'),
        'recommend-similar':    placement('recommend-similar'),
        'recall-memories':      placement('memory-recall'),
        'on-topic':             placement('on-topic-search'),
        'recommend-top-rated':  placement('recommend-extract'),
        'off-topic':            placement('decline-off-topic'),
        // Own timeout / classifier failure → retry budget decides. 'retry' loops
        // back; 'salvage' defaults to the broadest on-topic search via a node.
        'retry':                placement('classify-intent'),
        'salvage':              placement('classify-intent-salvage'),
      }, display('classify-intent'))
      .node(placement('classify-intent-salvage'), nodes.classifyIntentSalvage, {
        'done': placement('on-topic-search'),
      }, display('classify-intent-salvage'))
      // #endregion intent-routes

      // #region embedded-dag-placements
      // ── on-topic branch ──────────────────────────────────────────────────────
      // EmbeddedDAGNode: book-search-scatter handles extract-query, decide-tools,
      // all four scouts, rank-candidates, merge, record, gate, and recall.
      // One packaged cluster; first of three placements of the same sub-DAG.
      // gather.map copies the fields the sub-DAG writes back to the parent state
      // so compose-loop and group-by-year can read them.
      .embed(placement('on-topic-search'), BOOK_SEARCH_SCATTER_DAG_IRI, {
        'success': placement('compose-loop'),
        'error':   placement('compose-empty'),
      }, {
        'name': 'on-topic-search',
        'outputs': {
          'terms':         'terms',
          'toolPlan':      'toolPlan',
          'candidates':    'candidates',
          'shortlist':     'shortlist',
          'priorContext':  'priorContext',
          'failureCause':  'failureCause',
        },
      })

      // ── lookup-author branch ─────────────────────────────────────────────────
      // EmbeddedDAGNode: same book-search-scatter cluster, second placement.
      // After success, group-by-year sorts results chronologically before the
      // compose loop; author surveys read better in publication-timeline order.
      .embed(placement('author-search'), BOOK_SEARCH_SCATTER_DAG_IRI, {
        'success': placement('group-by-year'),
        'error':   placement('compose-empty'),
      }, {
        'name': 'author-search',
        'outputs': {
          'terms':         'terms',
          'toolPlan':      'toolPlan',
          'candidates':    'candidates',
          'shortlist':     'shortlist',
          'priorContext':  'priorContext',
          'failureCause':  'failureCause',
        },
      })
      // group-by-year is author-branch-specific: sorts shortlist chronologically.
      .node(placement('group-by-year'), nodes.groupByYear, {
        'ordered': placement('compose-loop'),
      }, display('group-by-year'))

      // ── find-reviews branch ───────────────────────────────────────────────────
      // Inlined. Uses rankByRating (deterministic, rating-weighted) in place of
      // rankCandidates (LLM-driven). The Google Books scout carries notes.rating /
      // notes.ratingsCount; rankByRating weights those for reviews-style output.
      .node(placement('reviews-extract'), nodes.extractQuery, {
        'success': placement('reviews-decide-tools'),
        'retry':   placement('reviews-extract'),
        'salvage': placement('reviews-extract-salvage'),
      }, display('reviews-extract'))
      .node(placement('reviews-extract-salvage'), nodes.extractQuerySalvage, {
        'done': placement('reviews-decide-tools'),
      }, display('reviews-extract-salvage'))
      .node(placement('reviews-decide-tools'), nodes.decideTools, {
        'tools':    placement('reviews-build-worksets'),
        'no-tools': placement('reviews-build-worksets'),
        'retry':    placement('reviews-decide-tools'),
        'salvage':  placement('reviews-decide-tools-salvage'),
      }, display('reviews-decide-tools'))
      .node(placement('reviews-decide-tools-salvage'), nodes.decideToolsSalvage, {
        'done': placement('reviews-build-worksets'),
      }, display('reviews-decide-tools-salvage'))
      // Build scatter worksets: converts toolPlan into bookWorksets items so the
      // scatter can dispatch to each declared tool DAG IRI.
      .node(placement('reviews-build-worksets'), nodes.buildBookWorksets, {
        'ready': placement('reviews-scatter'),
      }, display('reviews-build-worksets'))
      // Tool-registry scatter: each bookWorksets item carries its own tool DAG
  // IRI. The following GatherNode reads each clone's output via
      // accessor (no cast) and folds CandidateType[] into parent candidates.
      .scatter(placement('reviews-scatter'), 'bookWorksets', { 'dag': { 'from': 'item', 'path': 'dagIri', 'candidates': BOOK_SEARCH_TOOL_DAGS } }, {
        'success': placement('reviews-gather'),
        'error': placement('reviews-gather'),
        'empty':   placement('reviews-rank'),
      }, {
        'name': 'reviews-scatter',
        'execution': { 'mode': 'item', 'concurrency': 4 },
        'reducer': 'any-success',
      })
  .gather(placement('reviews-gather'), { [placement('reviews-scatter')]: {} }, { 'strategy': 'tool-candidate-merge' }, {
    'success': placement('reviews-rank'),
    'error': placement('reviews-rank'),
    'empty': placement('reviews-rank'),
  }, display('reviews-gather'))
      .node(placement('reviews-rank'),    nodes.rankByRating,     { 'ranked': placement('reviews-merge') }, display('reviews-rank'))
      .node(placement('reviews-merge'),   nodes.mergeCandidates,  { 'ranked': placement('reviews-record'), 'empty': placement('compose-empty') }, display('reviews-merge'))
      .node(placement('reviews-record'),  nodes.recordFindings,   { 'recorded': placement('reviews-gate') }, display('reviews-record'))
      .node(placement('reviews-gate'),    nodes.hasCitationsGate, { 'pass': placement('reviews-recall'), 'fail': placement('compose-empty') }, display('reviews-gate'))
      .node(placement('reviews-recall'),  nodes.recallPastVisits, { 'recalled': placement('compose-loop') }, display('reviews-recall'))

      // ── recommend-top-rated branch ───────────────────────────────────────────
      // Inlined, structural sibling of find-reviews. Reuses rankByRating
      // (deterministic, rating-weighted) instead of rankCandidates (LLM-driven)
      // because a vague "good book / good story" request carries no topic for
      // relevance ranking — rating is the only signal that makes sense.
      .node(placement('recommend-extract'), nodes.extractQuery, {
        'success': placement('recommend-decide-tools'),
        'retry':   placement('recommend-extract'),
        'salvage': placement('recommend-extract-salvage'),
      }, display('recommend-extract'))
      .node(placement('recommend-extract-salvage'), nodes.extractQuerySalvage, {
        'done': placement('recommend-decide-tools'),
      }, display('recommend-extract-salvage'))
      .node(placement('recommend-decide-tools'), nodes.decideTools, {
        'tools':    placement('recommend-build-worksets'),
        'no-tools': placement('recommend-build-worksets'),
        'retry':    placement('recommend-decide-tools'),
        'salvage':  placement('recommend-decide-tools-salvage'),
      }, display('recommend-decide-tools'))
      .node(placement('recommend-decide-tools-salvage'), nodes.decideToolsSalvage, {
        'done': placement('recommend-build-worksets'),
      }, display('recommend-decide-tools-salvage'))
      // Build scatter worksets: converts toolPlan into bookWorksets items so the
      // scatter can dispatch to each declared tool DAG IRI.
      .node(placement('recommend-build-worksets'), nodes.buildBookWorksets, {
        'ready': placement('recommend-scatter'),
      }, display('recommend-build-worksets'))
      // Tool-registry scatter: each bookWorksets item carries its own tool DAG
  // IRI. The following GatherNode reads each clone's output via
      // accessor (no cast) and folds CandidateType[] into parent candidates.
      .scatter(placement('recommend-scatter'), 'bookWorksets', { 'dag': { 'from': 'item', 'path': 'dagIri', 'candidates': BOOK_SEARCH_TOOL_DAGS } }, {
        'success': placement('recommend-gather'),
        'error': placement('recommend-gather'),
        'empty':   placement('recommend-rank'),
      }, {
        'name': 'recommend-scatter',
        'execution': { 'mode': 'item', 'concurrency': 4 },
        'reducer': 'any-success',
      })
  .gather(placement('recommend-gather'), { [placement('recommend-scatter')]: {} }, { 'strategy': 'tool-candidate-merge' }, {
    'success': placement('recommend-rank'),
    'error': placement('recommend-rank'),
    'empty': placement('recommend-rank'),
  }, display('recommend-gather'))
      .node(placement('recommend-rank'),   nodes.rankByRating,     { 'ranked': placement('recommend-merge') }, display('recommend-rank'))
      .node(placement('recommend-merge'),  nodes.mergeCandidates,  { 'ranked': placement('recommend-record'), 'empty': placement('compose-empty') }, display('recommend-merge'))
      .node(placement('recommend-record'), nodes.recordFindings,   { 'recorded': placement('recommend-gate') }, display('recommend-record'))
      .node(placement('recommend-gate'),   nodes.hasCitationsGate, { 'pass': placement('recommend-recall'), 'fail': placement('compose-empty') }, display('recommend-gate'))
      .node(placement('recommend-recall'), nodes.recallPastVisits, { 'recalled': placement('compose-loop') }, display('recommend-recall'))

      // ── describe-book branch ─────────────────────────────────────────────────
      // Inlined. Uses pickBestMatch to narrow multi-hit results to the top-3
      // title-similar candidates before merge. Ensures the composer receives the
      // specific book the visitor named, not arbitrary top-5 hits.
      .node(placement('describe-extract'),      nodes.extractQuery,     { 'success': placement('describe-decide-tools'), 'retry': placement('describe-extract'), 'salvage': placement('describe-extract-salvage') }, display('describe-extract'))
      .node(placement('describe-extract-salvage'), nodes.extractQuerySalvage, { 'done': placement('describe-decide-tools') }, display('describe-extract-salvage'))
      .node(placement('describe-decide-tools'), nodes.decideTools,      { 'tools': placement('describe-build-worksets'), 'no-tools': placement('describe-build-worksets'), 'retry': placement('describe-decide-tools'), 'salvage': placement('describe-decide-tools-salvage') }, display('describe-decide-tools'))
      .node(placement('describe-decide-tools-salvage'), nodes.decideToolsSalvage, { 'done': placement('describe-build-worksets') }, display('describe-decide-tools-salvage'))
      // Build scatter worksets before dispatch.
      .node(placement('describe-build-worksets'), nodes.buildBookWorksets, {
        'ready': placement('describe-scatter'),
      }, display('describe-build-worksets'))
      // Tool-registry scatter: DagReference resolves body DAG from each item's dagName.
      // any-success reducer: 'success' → describe-pick, 'error' → compose-empty.
      // 'error' fires when all tool scouts return empty.
      .scatter(placement('describe-scatter'), 'bookWorksets', { 'dag': { 'from': 'item', 'path': 'dagIri', 'candidates': BOOK_SEARCH_TOOL_DAGS } }, {
        'success': placement('describe-gather'),
        'error':   placement('compose-empty'),
        'empty':   placement('compose-empty'),
      }, {
        'name': 'describe-scatter',
        'execution': { 'mode': 'item', 'concurrency': 4 },
        'reducer': 'any-success',
      })
  .gather(placement('describe-gather'), { [placement('describe-scatter')]: {} }, { 'strategy': 'tool-candidate-merge' }, {
    'success': placement('describe-pick'),
    'error': placement('compose-empty'),
    'empty': placement('compose-empty'),
  }, display('describe-gather'))
      .node(placement('describe-pick'),   nodes.pickBestMatch,    { 'picked': placement('describe-merge') }, display('describe-pick'))
      .node(placement('describe-merge'),  nodes.mergeCandidates,  { 'ranked': placement('describe-record'), 'empty': placement('compose-empty') }, display('describe-merge'))
      .node(placement('describe-record'), nodes.recordFindings,   { 'recorded': placement('describe-gate') }, display('describe-record'))
      .node(placement('describe-gate'),   nodes.hasCitationsGate, { 'pass': placement('describe-recall'), 'fail': placement('compose-empty') }, display('describe-gate'))
      .node(placement('describe-recall'), nodes.recallPastVisits, { 'recalled': placement('compose-loop') }, display('describe-recall'))

      // ── recommend-similar branch ─────────────────────────────────────────────
      // recommendSimilar seeds state.terms from prior-run shortlist memory.
      // 'seeded' routes to the book-search-scatter sub-DAG; third placement of
      // the same packaged cluster. 'empty' routes to the compose-empty terminal.
      .node(placement('recommend-similar'), nodes.recommendSimilar, {
        'seeded': placement('similar-search'),
        'empty':  placement('compose-empty'),
      }, display('recommend-similar'))

      // EmbeddedDAGNode: same book-search-scatter, third and final placement.
      .embed(placement('similar-search'), BOOK_SEARCH_SCATTER_DAG_IRI, {
        'success': placement('compose-loop'),
        'error':   placement('compose-empty'),
      }, {
        'name': 'similar-search',
        'outputs': {
          'terms':         'terms',
          'toolPlan':      'toolPlan',
          'candidates':    'candidates',
          'shortlist':     'shortlist',
          'priorContext':  'priorContext',
          'failureCause':  'failureCause',
        },
      })

      // ── compose-loop: shared compose/validate sub-DAG ──────────────────────────
      // All branches that successfully find candidates converge here.
      // composeResponse → validateResponse (retry loop, bounded by the retry budget on state (retriesFor('compose'))).
      // One sub-DAG definition serves all four convergent branches.
      // stateMapping.outputs copies the compose loop's writes back to the parent.
      //
      // Convergence policy: 'success' routes to the shared respond-to-visitor terminal
      // at the parent level; the sub-DAG produces state.draft and exits cleanly;
      // exactly ONE respond-to-visitor fires per run regardless of branch count.
      // 'error' (retry budget exhausted) falls through to compose-empty so the
      // visitor always receives an in-character response rather than a silent drop.
      .embed(placement('compose-loop'), COMPOSE_RETRY_LOOP_DAG_IRI, {
        'success': placement('respond-to-visitor'),
        'error':   placement('compose-empty'),
      }, {
        'name': 'compose-loop',
        'outputs': {
          'draft':         'draft',
          'approvalState': 'approvalState',
        },
      })
      // #endregion embedded-dag-placements

      // ── respond-to-visitor: single shared happy-path terminal ───────────────
      // Every branch that successfully composes a response converges here.
      // compose-loop (success) and both memory + empty-result paths all route
      // through this one placement. Convergence policy: exactly ONE respond-to-visitor
      // fires per run with the full converged state.draft in context. Success routes
      // to the canonical `end` TerminalNode rather than a bare null end-of-flow.
      .node(placement('respond-to-visitor'), nodes.respondToVisitor, { 'success': placement('end') }, display('respond-to-visitor'))

      // ── recall-memories branch ───────────────────────────────────────────────
      // No search needed; the memory store is queried directly.
      // recallMemories → composeMemoryResponse → respond-to-visitor (shared terminal).
      .node(placement('memory-recall'),          nodes.recallMemories,       { 'recalled': placement('compose-memory-recall') }, display('memory-recall'))
      .node(placement('compose-memory-recall'),  nodes.composeMemoryResponse, {
        'drafted': placement('respond-to-visitor'),
        'retry':   placement('compose-memory-recall'),
        'salvage': placement('compose-memory-salvage'),
      }, display('compose-memory-recall'))
      .node(placement('compose-memory-salvage'), nodes.composeMemoryResponseSalvage, { 'done': placement('respond-to-visitor') }, display('compose-memory-salvage'))

      // #region terminal-placements
      // ── Terminal nodes ───────────────────────────────────────────────────────
      .node(placement('decline-off-topic'), nodes.declineOffTopic, { 'success': placement('end') }, display('decline-off-topic'))
      .node(placement('compose-empty'),     nodes.composeEmptyResponse,  {
        'drafted': placement('respond-to-visitor'),
        'retry':   placement('compose-empty'),
        'salvage': placement('compose-empty-salvage'),
      }, display('compose-empty'))
      .node(placement('compose-empty-salvage'), nodes.composeEmptyResponseSalvage, { 'done': placement('respond-to-visitor') }, display('compose-empty-salvage'))

      // Canonical end-of-flow: every completed path (a composed answer or an
      // off-topic decline) routes to this one `TerminalNode(completed)` instead of
      // a bare `null` route. The flow ends explicitly, not by absence of a route.
      .terminal(placement('end'), { outcome: 'completed', name: 'end' })
      // #endregion terminal-placements

      .build();
// #endregion dispatcher-bundle

Details for Nerds

The builder is deliberately not a second configuration language. There is no hidden builder runtime, no decorator metadata, and no post-build projection layer. The object returned by .build() is the document registerDAG validates.

That makes builder-authored DAGs easy to package as plugins: the plugin exports a normal DAG IRI/reference, and a parent flow embeds that reference exactly as it would embed a hand-authored DAG.

What it demonstrates

  • Fluent chainable authoring. Every .node() and .scatter() returns this for fluent composition. The chain calls build() once at the end to produce the plain DAG object.
  • Compile-time route exhaustiveness. The routes argument is typed as Record<TOutput, null | string>. TypeScript catches missing outputs (forgot 'error') and stray outputs (typo in output name) at compile time.
  • Auto-entrypoint. The first .node() call sets the DAG entrypoint automatically. Override with .entrypoints(...) when multiple labels should enter the same graph, as Cartographer does for source intake.
  • Embedded-DAG placements via .embed(). on-topic-search, author-search, similar-search, and compose-loop are EmbeddedDAGNode placements. Each references a registered sub-DAG by IRI and declares its stateMapping.outputs.
  • Scatter placements via .scatter(). reviews-scatter and describe-scatter scatter over state.bookWorksets with a dynamic DagReference body. Each clone reads item.dagIri, validates it against explicit candidates, and executes the matching tool DAG; clone outputs then route into a first-class gather placement.
  • Same output as a literal DAG. .build() returns the identical wire shape DAGDocument.load(json) validates. The builder is a convenience layer, not a separate runtime.

See this in action in the Archivist live demo.

Read these next when you want to move between builder authoring, literal JSON-LD, embedded DAGs, and the reference shapes.

Watched over by the Order of Dagon.