Getting Started
What It Is
Getting Started is the shortest honest path from install to a running Dagonizer flow. It uses the same two-node DAG that powers Example 02: DAGBuilder, then shows the JSON-LD shape it compiles to in Example 01: Linear DAG.
Read this page when you want one clean loop in your hands: install the package, define a tiny state object, build a DAG, register it, execute it, and understand what comes back. The rest of the docs scale that same loop into browser demos, embedded DAGs, plugin-defined flows, scatter/gather, streaming producers, and checkpoint resume.
How It Works
Dagonizer splits a workflow into two things that are easy to reason about separately. The DAG document declares placement IRIs, entrypoints, and routes. The registered nodes contain the TypeScript behavior. The dispatcher joins them at runtime: it validates the graph, looks up registered nodes by their expanded IRI, runs the entrypoint placement, and follows the output route returned by each node.
That separation is the whole trick. You can author with DAGBuilder, ship JSON-LD over the wire, inspect the shape as Mermaid, and still keep your actual work in normal TypeScript classes. The tiny example here is a classify/respond chain, but the same registration pattern is what the Archivist, Cartographer, and Dispatcher demos use for real agent and data-pipeline flows. The builder is the spellbook; JSON-LD is the sigil the engine actually reads after DAGDocument.load(json) validates it.
What execute returns
dispatcher.execute() returns an Execution<TState> that is both awaitable and async-iterable.
Awaitable form:
// Awaitable form: await the execution for the final ExecutionResultType.
// result.state the final state (same reference as input)
// result.cursor null if completed; a node name if interrupted
// result.executedNodes ordered array of nodes that ran
// result.skippedNodes nodes that were skipped (e.g. empty scatter source)
const awaitDispatcher = new Dagonizer<ChatState>();
awaitDispatcher.registerNode(new ClassifyNode());
awaitDispatcher.registerNode(new RespondNode());
awaitDispatcher.registerDAG(dag);
const awaitState = new ChatState();
awaitState.input = 'How do I await a Promise in TypeScript?';
const result = await awaitDispatcher.execute('urn:noocodec:dag:chat', awaitState);
void result; // result.state, result.cursor, result.executedNodes, result.skippedNodesAsync-iterable form, one event per node:
// Async-iterable form: one NodeResult event per node as the flow runs.
// The flow body runs once; both modes share the same internal generator.
const iterDispatcher = new Dagonizer<ChatState>();
iterDispatcher.registerNode(new ClassifyNode());
iterDispatcher.registerNode(new RespondNode());
iterDispatcher.registerDAG(dag);
const iterState = new ChatState();
iterState.input = 'Explain generics in TypeScript';
const execution = iterDispatcher.execute('urn:noocodec:dag:chat', iterState);
for await (const nodeResult of execution) {
process.stdout.write(` node=${nodeResult.nodeName} output=${String(nodeResult.output)}\n`);
}
const iterResult = await execution; // cached — generator ran once; returns same result
void iterResult;Diagrams, Examples, and Outputs
The first runnable graph is intentionally small: a start node routes to a response node, then the DAG ends. The point is not the business logic; it is the shape of a Dagonizer flow.
The builder version and the JSON-LD version register the same topology. That is the mental model to keep: builder code is the ergonomic authoring surface, JSON-LD is the canonical assembly, absolute placement IRIs are runtime identity, and Mermaid is the readable shape generated from that same assembly. Names stay for display and observability.
Next Places To Open
Follow these pages in order if you want the quickstart to expand without changing concepts:
- Example 02: DAGBuilder - the fluent builder version used below.
- Example 01: Linear DAG - the same flow as direct JSON-LD.
- The Archivist demo - the same engine running an LLM-agent bookstore assistant.
- The Cartographer demo - the same engine running streaming ETL with no LLM.
What It Lets You Do
This page gets you to the first useful checkpoint: a DAG you can run, inspect, and modify. After that, the rest of the system stops looking abstract. Scatter is “run this body for each item.” Gather is “join these producer IRIs at a visible barrier.” Embedded DAGs are “call this registered subflow.” Plugins are “ship reusable registered DAG parts.” Checkpointing is “persist the cursor and state when execution stops early.”
It also gives you the right DevEx habit early: keep graph shape explicit. That matters for LLM agents, data science pipelines, ETL jobs, and service orchestration because reviewers can see the route map instead of reverse-engineering control flow from nested callbacks.
Code Samples
Install
npm install @studnicky/dagonizerRequires Node.js 24 or later and TypeScript 5.6 or later with strict: true.
Smallest DAG that runs
The docs use runnable examples as the source of truth. The smallest focused runner is examples/02-builder.ts: a two-node chain that picks a route at the first node and ends at the second. It is deliberately small, but it is still a real executable example, not a separate doc-only topology.
DAGBuilder (from @studnicky/dagonizer/builder) is the recommended authoring surface: a compile-checked fluent API that catches unwired outputs and invalid routing at compile time, before any schema validation runs. The same pattern scales directly into The Archivist, The Cartographer, and The Dispatcher, where the built DAGs are the canonical JSON-LD inputs consumed by the dispatcher.
The focused builder walkthrough ships in the repo as examples/dags/02-builder.topology.ts and examples/02-builder.ts.
State and nodes:
import {
Batch,
DAGBuilder,
DAGIdentity,
MonadicNode,
NodeOutput,
NodeStateBase,
RoutedBatch,
} from '@studnicky/dagonizer';
import type { SchemaObjectType } from '@studnicky/dagonizer';export class ClassifyNode extends MonadicNode<ChatState, 'on_topic' | 'off_topic'> {
readonly name = 'classify';
readonly '@id' = 'urn:noocodec:node:classify';
readonly outputs = ['on_topic', 'off_topic'] as const;
override get outputSchema(): Record<'on_topic' | 'off_topic', SchemaObjectType> {
return { 'on_topic': { 'type': 'object' }, 'off_topic': { 'type': 'object' } };
}
override async execute(batch: Batch<ChatState>) {
const entries: Array<readonly ['on_topic' | 'off_topic', Batch<ChatState>]> = [];
for (const item of batch) {
const state = item.state;
state.topic = state.input.toLowerCase().includes('weather') ? 'off_topic' : 'on_topic';
const output = NodeOutput.create(state.topic);
for (const error of output.errors) state.collectError(error);
entries.push([output.output, Batch.from([item])]);
}
return RoutedBatch.create(entries);
}
}
export class RespondNode extends MonadicNode<ChatState, 'success'> {
readonly name = 'respond';
readonly '@id' = 'urn:noocodec:node:respond';
readonly outputs = ['success'] as const;
override get outputSchema(): Record<'success', SchemaObjectType> {
return { 'success': { 'type': 'object' } };
}
override async execute(batch: Batch<ChatState>) {
for (const item of batch) {
const state = item.state;
state.reply = state.topic === 'on_topic'
? `Echo: ${state.input}`
: `I only talk about coding, not the weather.`;
}
return RoutedBatch.create(NodeOutput.create('success').output, batch);
}
}The DAG definition, built via DAGBuilder:
export const dag = new DAGBuilder(chatDAGIri, '1')
// First .node() call → entrypoint is set to 'classify' automatically.
.node(placement(chatDAGIri, 'classify'), new ClassifyNode(), {
"on_topic": placement(chatDAGIri, 'respond'),
"off_topic": placement(chatDAGIri, 'respond'),
})
// routes for 'respond' must cover exactly { success }, no more, no less.
.node(placement(chatDAGIri, 'respond'), new RespondNode(), { "success": placement(chatDAGIri, 'end') })
.terminal(placement(chatDAGIri, 'end'))
.build(); // materialises the canonical JSON-LD DAG documentRegister, then execute:
const dispatcher = new Dagonizer<ChatState>();
dispatcher.registerNode(new ClassifyNode());
dispatcher.registerNode(new RespondNode());
dispatcher.registerDAG(dag); // same API as with a literal; build() returns a valid DAG
const state = new ChatState();
state.input = 'What is a generic type parameter?';
await dispatcher.execute(dagIri, state);
process.stdout.write('\nBuilder DAG: same shape as 01-linear, constructed via DAGBuilder (02-builder)\n');
process.stdout.write(` input: "${state.input}"\n`);
process.stdout.write(` reply: "${state.reply}"\n`);
process.stdout.write(`\n built DAG @id: ${dag['@id']}\n`);
process.stdout.write(` nodes: ${dag.nodes.map(n => `${n['@type']}(${n.name})`).join(' → ')}\n`);
process.stdout.write('\nLesson: DAGBuilder.build() produces the canonical JSON-LD DAG document;\n');
process.stdout.write(' routes are exhaustiveness-checked.\n');Run it directly:
npx tsx examples/02-builder.tsSee the DAGBuilder guide for the full API including scatter, .embed(), and phase placements.
The same DAG as JSON-LD
DAGBuilder.build() returns a plain JSON-LD document — the canonical wire format DAGDocument.load(json) accepts before dispatcher.registerDAG(dag) stores it. The DAG built above is identical, field for field, to this hand-written literal (from examples/dags/01-linear.ts, the same two-node classify/respond chain):
export const dag: DAGType = {
'@context': DAG_CONTEXT, // JSON-LD 1.1 ontology context
'@id': 'urn:noocodec:dag:chat', // globally unique URN for this DAG
'@type': 'DAG', // RDF class: top-level DAG document
"name": 'chat',
"version": '1',
"entrypoints": { "main": 'urn:noocodec:dag:chat/node/classify' }, // first placement to execute
"nodes": [
{
'@id': 'urn:noocodec:dag:chat/node/classify',
'@type': 'SingleNode', // run exactly one registered node
"name": 'classify',
"node": 'urn:noocodec:node:classify', // registered node IRI
"outputs": {
"on_topic": 'urn:noocodec:dag:chat/node/respond',
"off_topic": 'urn:noocodec:dag:chat/node/respond',
}, // both routes converge
},
{
'@id': 'urn:noocodec:dag:chat/node/respond',
'@type': 'SingleNode',
"name": 'respond',
"node": 'urn:noocodec:node:respond',
"outputs": { "success": 'urn:noocodec:dag:chat/node/end' }, // routes to canonical terminal
},
{
'@id': 'urn:noocodec:dag:chat/node/end',
'@type': 'TerminalNode',
"name": 'end',
"outcome": 'completed',
},
],
};Author the wire format directly for advanced use: hand-authored fixtures, interop with non-TypeScript tooling that emits or consumes JSON-LD, or understanding exactly what ships over the wire. Both forms register and execute identically:
const dispatcher = new Dagonizer<ChatState>();
dispatcher.registerNode(new ClassifyNode());
dispatcher.registerNode(new RespondNode());
dispatcher.registerDAG(dag);
// On-topic: flows classify → respond, reply is an echo
const onTopic = new ChatState();
onTopic.input = 'How do I declare a const in TypeScript?';
await dispatcher.execute('urn:noocodec:dag:chat', onTopic);
// Off-topic: same route (both outputs → respond), but different branch taken
const offTopic = new ChatState();
offTopic.input = 'What is the weather like today?';
await dispatcher.execute('urn:noocodec:dag:chat', offTopic);
process.stdout.write('\nLinear DAG: classify -> respond -> END\n');
process.stdout.write(` on_topic → "${onTopic.reply}"\n`);
process.stdout.write(` off_topic → "${offTopic.reply}"\n`);
process.stdout.write('\nLesson: both outputs of classify route to the same placement;\n');
process.stdout.write(' a TerminalNode marks the explicit end of the flow.\n');Details for Nerds
The quickstart hides almost nothing. DAGBuilder is not a separate runtime; it produces the JSON-LD document the dispatcher already accepts. The dispatcher does not scan your module graph; it only runs nodes and DAGs you register. The graph is portable data, and the behavior stays in normal TypeScript classes.
This is closer to a small in-process workflow engine than to a prompt-chain helper. There is no external scheduler, no mandatory queue, and no invisible global registry. When you need those things, you compose them around Dagonizer: a web handler, a worker, Temporal, a database-backed checkpoint store, or a plugin package that exports reusable DAG IRIs.
Next destination
Three in-browser demos show the same engine in different domains:
- The Archivist — LLM agents. A multi-stage bibliographic-assistant DAG that exercises tool DAGs, embedded search bodies, retry, cancellation, and checkpoint resume.
- The Cartographer — data orchestration / ETL / streaming. Multiple source entrypoints feed a first-class intake gather, then scatter through typed event pipelines with conditional routing, geo-resolution, GDPR redaction, and streaming backpressure. No LLM.
- The Dispatcher — HITL support workflow. Customer messages route through routine AI response, operator escalation with park/resume, or off-topic decline.
Related Concepts
Read these next based on what you want to build:
- Concepts - the vocabulary behind nodes, placements, lifecycle, scatter, state, and checkpoints.
- Architecture - how the dispatcher, lifecycle machine, validators, and public subpaths fit together.
- DAGBuilder - the fluent authoring API used in the quickstart.
- Example 02: DAGBuilder - focused runnable example for the quickstart pattern.
- The Archivist - browser runnable for agent memory, tools, retry, and response composition.
- The Cartographer - browser runnable for streaming data orchestration, scatter/gather, and plugin-style DAG parts.