Monadic Node
What It Is
Monadic Node is a small base-class pattern for application nodes that must return declared output ports on every code path. The example registers SearchCatalogueNode on a two-placement DAG and runs it through both success and error routes.
Use this when node authors want structure around execute: declared outputs, a protected run hook, and a consistent error route instead of uncaught throws escaping the node boundary.
How It Works
MonadicNode requires concrete subclasses to declare name and outputs. A logging intermediate class can wrap the protected run method, while the concrete node owns domain behavior. The dispatcher sees a normal NodeInterface.
Diagrams, Examples, and Outputs
The example is intentionally tiny: one SingleNode placement followed by one completed terminal. The CLI output demonstrates both routes.
Run
npx tsx examples/monadic-node.tsWhat It Lets You Do
The monadic node pattern lets applications build node classes where every code path returns a declared output port. Use it when node authors need a small base class that catches implementation errors, routes failures explicitly, and keeps node behavior testable outside the dispatcher.
MonadicNode is the abstract base for canonical DAG node patterns. Concrete subclasses declare name, outputs, and implement execute (or, as shown here, a protected run method called by a logging intermediate class). Every code path must return a declared output port — nothing throws past the node boundary.
SearchCatalogueNode is registered on a two-placement DAG (search → end) and executed twice:
- With a real query — routes
'success'. - With an empty query — routes
'error'.
Code Samples
/**
* monadic-node: runs a concrete MonadicNode subclass end-to-end in a DAG.
*
* MonadicNode is the abstract base for every canonical DAG pattern. Concrete
* subclasses declare `name`, `outputs`, and implement `execute` (or, as here,
* a protected `run` method called by a logging intermediate class). Every code
* path must return a declared output port — nothing throws past the node
* boundary.
*
* The SearchCatalogueNode from dags/monadic-node.ts is registered on a tiny
* two-placement DAG (search → end) and executed twice: once with a real query
* (routes 'success') and once with an empty query (routes 'error').
*
* DAG definition (state, abstract base, concrete node): examples/dags/monadic-node.ts
*
* Run: npx tsx examples/monadic-node.ts
*/
import { DAG_CONTEXT, Dagonizer } from '@studnicky/dagonizer';
import type { DAGType } from '@studnicky/dagonizer';
import { CatalogueState, SearchCatalogueNode } from './dags/monadic-node.js';
// ── Build a minimal DAG: search-catalogue → end ─────────────────────────────
const dag: DAGType = {
'@context': DAG_CONTEXT,
'@id': 'urn:noocodec:dag:catalogue-search',
'@type': 'DAG',
name: 'catalogue-search',
version: '1',
entrypoints: { main: 'urn:noocodec:dag:catalogue-search/node/search' },
nodes: [
{
'@id': 'urn:noocodec:dag:catalogue-search/node/search',
'@type': 'SingleNode',
name: 'search',
node: 'urn:noocodec:node:search-catalogue',
outputs: {
success: 'urn:noocodec:dag:catalogue-search/node/end',
empty: 'urn:noocodec:dag:catalogue-search/node/end',
error: 'urn:noocodec:dag:catalogue-search/node/end',
},
},
{
'@id': 'urn:noocodec:dag:catalogue-search/node/end',
'@type': 'TerminalNode',
name: 'end',
outcome: 'completed',
},
],
};
// ── Dispatcher ───────────────────────────────────────────────────────────────
const dispatcher = new Dagonizer<CatalogueState>();
dispatcher.registerNode(new SearchCatalogueNode());
dispatcher.registerDAG(dag);
// ── Run 1: valid query → 'success' output, results populated ─────────────────
process.stdout.write('\n=== MonadicNode: concrete subclass in a live DAG ===\n\n');
const validState = new CatalogueState();
validState.query = 'Mechanicus Codex';
await dispatcher.execute('urn:noocodec:dag:catalogue-search', validState);
process.stdout.write(`results: ${JSON.stringify(validState.results)}\n`);
// ── Run 2: empty query → 'error' output, results empty ───────────────────────
const emptyState = new CatalogueState();
emptyState.query = '';
await dispatcher.execute('urn:noocodec:dag:catalogue-search', emptyState);
process.stdout.write(`results (empty query): ${JSON.stringify(emptyState.results)}\n`);
process.stdout.write('\nLesson: MonadicNode subclasses declare typed outputs;\n');
process.stdout.write(' every path returns a named port — nothing throws past the node.\n');Details for Nerds
MonadicNodeabstract base. Subclass it to get structural enforcement:nameandoutputsare required abstract members;executeis the hook point. The base class catches any unhandled throws fromrunand routes them to'error'automatically.- Protected
runmethod. A logging intermediate class can overrideexecuteto wraprunwith metrics or tracing. The node implementation goes inrun; the infrastructure goes inexecute. - Every path returns a declared output. Returning a non-declared output port is a type error. The node must handle all cases (including empty input, network failure) by routing to a declared port rather than throwing.
- Two-placement DAG. A
SingleNodeplacement followed by aTerminalNodewithoutcome: 'completed'. The minimal shape that verifies the node's routing behavior end-to-end.
Related Concepts
- Example 01: Linear Intake - simplest node implementation
- Example 02: DAGBuilder - DAGBuilder chainable API
- Reference: Contracts, NodeInterface - NodeInterface contract