Skip to content

Authoring a DAG

A DAG document is the program. You author it with DAGBuilder, serialize it with DAGDocument.serialize(), and commit the resulting *.dag.jsonld file. The runner loads it at startup and dispatches it via the dagonizer engine. No pipeline array, no config compilation — the document is the execution graph.

Ripperoni drives all scrape orchestration through @studnicky/dagonizer. Each run is a DAG dispatched by RipperDagonizer, built via DAGBuilder.

Placement types

A DAG document is a set of named placements wired by output routes.

Placement type@typeDescription
SingleNode"SingleNode"Runs a registered node instance. node field names the registered node.
ScatterNode"ScatterNode"Fans over a list (source field) running the body sub-DAG once per item.
EmbeddedDAGNode"EmbeddedDAGNode"Drops a full registered DAG in-place as a single logical step.
TerminalNode"TerminalNode"Ends processing for a path. outcome is "completed" or "failed".

DAGBuilder API

ts
import { DAGBuilder, DAGDocument } from '@studnicky/dagonizer';

const dag = new DAGBuilder('ns:name', '1.0')
  .entrypoint('first-node')                              // set the entry placement name
  .node('html:fetch', HtmlFetchNode, {                   // SingleNode placement
    success: 'next-step',
    error:   'my-dag:failed',
  })
  .embeddedDAG('my-parse', 'myplugin:parse', {          // EmbeddedDAGNode placement
    success: 'json:write',
    error:   'my-dag:failed',
  }, {
    inputs:  { page: 'page' },
    outputs: { output: 'output' },
  })
  .scatter('fan-out', {                                  // ScatterNode placement
    source:    'urls',
    body:      { dag: 'myplugin:page' },
    container: 'worker',
    itemKey:   'currentUrl',
    gather:    { strategy: 'partition', partitions: { success: 'succeeded', error: 'failed' } },
    reducer:   'aggregate',
    outputs:   { 'all-success': 'done', partial: 'done', 'all-error': 'done', empty: 'done' },
  })
  .terminal('my-dag:completed', { outcome: 'completed' })
  .terminal('my-dag:failed',    { outcome: 'failed' })
  .build();

// Serialize to a committed *.dag.jsonld file:
const jsonld = DAGDocument.serialize(dag);

DAGBuilder methods chain. Call .build() once when the graph is complete. Pass the result to DAGDocument.serialize() to produce the JSON-LD string; write it to a *.dag.jsonld file and commit it. The runner loads the file at startup via PluginLoader.

Built-in nodes

These nodes are always registered by PluginLoader.registerBuiltinNodes — reference them by name in any DAG document:

Node namePortsDescription
html:fetchsuccess | cached | errorFetches state.page.url via HtmlScraper. cached port fires on a cache hit.
wiki:fetchsuccess | errorFetches state.page.title via MediaWikiScraper.
html:write-rawsuccessWrites raw HTML to <outDir>/<taskName>/raw/<slug>.html.
wiki:write-rawsuccessWrites raw wikitext to <outDir>/<taskName>/raw/<slug>.txt.
json:writesuccess | skippedWrites state.output as JSON. skipped when state.output is null.
markdown:writesuccess | skippedConverts state.page.html to GFM Markdown via MarkdownConverter and writes <slug>.md. skipped when state.page.html is absent. See Markdown output.
jsonl:appendsuccess | skippedAppends state.output to a JSONL file. skipped when state.output is null.
validate:schemavalid | invalidValidates state.output against a JSON schema.
crawl:init-frontierready | emptyInitializes the crawl frontier from the crawler.startUrls in state.
crawl:fetch-and-extractsuccess | empty | error | permanentFetches a frontier page and extracts links.
crawl:dedupe-and-enqueuefrontier-ready | frontier-empty | budget-exhaustedDeduplicates extracted links, promotes the next frontier, checks maxPages.
crawl:exhaustedsuccessTerminal for the crawl loop — crawl:discover's exit point.

Built-in node names follow the convention namespace:verb. BUILTIN_PREFIXES are html:, wiki:, json:, jsonl:, validate:, and crawl:.

Built-in crawl:discover DAG

crawl:discover is a built-in cyclic DAG (loaded from src/crawlers/crawl-discover.dag.jsonld) registered by PluginLoader.registerBuiltinNodes. It implements level-by-level BFS with a native back-edge loop.

Embed it in any orchestration via EmbeddedDAGNode:

json
{
  "@type": "EmbeddedDAGNode",
  "name":  "discover",
  "dag":   "crawl:discover",
  "stateMapping": {
    "output": { "urls": "crawl.discovered" }
  },
  "outputs": { "success": "next-step", "error": "crawl-failed" }
}

The stateMapping.output block seeds state.urls from crawl.discovered after the crawl completes. Configure the crawl behaviour via the crawler block in state.json. See Crawler.

Reservoir scatter

For very large URL lists, reservoir scatter bounds memory use by processing a fixed number of items concurrently rather than fanning out the full list at once. The engine pulls items from the scatter source in batches of capacity, releasing a slot only when an item completes.

Add a reservoir block to the ScatterNode in the .dag.jsonld:

json
{
  "@type":     "ScatterNode",
  "name":      "scrape",
  "source":    "urls",
  "body":      { "dag": "myplugin:page" },
  "container": "worker",
  "itemKey":   "currentItem",
  "reservoir": {
    "keyField": "currentItem",
    "capacity": 50,
    "idleMs":   30000
  },
  "gather":  { "strategy": "partition", "partitions": { "success": "succeeded", "error": "failed" } },
  "outputs": { "all-success": "done", "partial": "done", "all-error": "done", "empty": "done" }
}

keyField must match itemKey. capacity is the concurrency window. idleMs (default 30000) is how long the engine waits with an idle, source-exhausted reservoir before marking it complete.

The reservoir is engine-native — no Ripperoni code is involved. Document the intended capacity in the matching reservoir block in state.json for discoverability.

Parallel parse

Route scatter items to a worker-thread pool with two changes:

  1. Set container: "worker" on the ScatterNode.
  2. Set parallelWorkers: true in state.json.
  3. Build the worker registry: npm run build:workers.

The WorkerThreadContainer pool is sized by NodeSystemInfo.recommendedWorkerCount. When the worker registry is absent, the scatter falls back to in-process execution automatically.

Plugin DAG registration order

Leaves (plugin DAGs with no external dependencies) register before dependents. PluginLoader.pluginDagsInRegistrationOrder topologically sorts the DAG documents found in plugins/<namespace>/ so that an embedded DAG reference is always registered before the DAG that embeds it. The orchestration DAG is registered last.

Released under the MIT License.