Scatter Extensions
What It Is
Scatter Extensions shows how an application adds domain-specific scatter behavior without changing dispatcher code. The CLI example installs TopNGatherStrategy and ThresholdReducer, then references both by registry key from a normal scatter DAG.
This is the extension seam behind custom gather and reducer policy: register the implementation once, then keep DAG assembly declarative.
How It Works
GatherStrategies.register(...) and OutcomeReducers.register(...) install custom implementations into global registries. A scatter placement references them through gather.strategy and reducer. The dispatcher resolves those names at execution time.
Diagrams, Examples, and Outputs
The DAG below is the same reservoirDag exported by the CLI example. It shows the custom gather strategy and reducer as graph configuration, not code branches inside the dispatcher.
scatter-extensions reservoir DAG
3 placements{
"@context": {
"@version": 1.1,
"name": {
"@id": "https://noocodec.dev/ontology/dag/name"
},
"version": {
"@id": "https://noocodec.dev/ontology/dag/version"
},
"entrypoints": {
"@id": "https://noocodec.dev/ontology/dag/entrypoints",
"@container": "@index"
},
"nodes": {
"@id": "https://noocodec.dev/ontology/dag/nodes",
"@container": "@set"
},
"outputs": {
"@id": "https://noocodec.dev/ontology/dag/outputs"
},
"node": {
"@id": "https://noocodec.dev/ontology/dag/node"
},
"dag": {
"@id": "https://noocodec.dev/ontology/dag/dag"
},
"body": {
"@id": "https://noocodec.dev/ontology/dag/body"
},
"source": {
"@id": "https://noocodec.dev/ontology/dag/source"
},
"sources": {
"@id": "https://noocodec.dev/ontology/dag/sources",
"@container": "@index"
},
"itemKey": {
"@id": "https://noocodec.dev/ontology/dag/itemKey"
},
"execution": {
"@id": "https://noocodec.dev/ontology/dag/execution"
},
"concurrency": {
"@id": "https://noocodec.dev/ontology/dag/concurrency"
},
"throttle": {
"@id": "https://noocodec.dev/ontology/dag/throttle"
},
"reservoir": {
"@id": "https://noocodec.dev/ontology/dag/reservoir"
},
"gather": {
"@id": "https://noocodec.dev/ontology/dag/gather"
},
"dagReference": {
"@id": "https://noocodec.dev/ontology/dag/dagReference",
"@type": "@id"
},
"DagReference": {
"@id": "https://noocodec.dev/ontology/dag/DagReference"
},
"from": {
"@id": "https://noocodec.dev/ontology/dag/from"
},
"path": {
"@id": "https://noocodec.dev/ontology/dag/path"
},
"candidates": {
"@id": "https://noocodec.dev/ontology/dag/candidates",
"@container": "@set"
},
"candidateDag": {
"@id": "https://noocodec.dev/ontology/dag/candidateDag",
"@type": "@id"
},
"selectedDag": {
"@id": "https://noocodec.dev/ontology/dag/selectedDag",
"@type": "@id"
},
"resultField": {
"@id": "https://noocodec.dev/ontology/dag/resultField"
},
"policy": {
"@id": "https://noocodec.dev/ontology/dag/policy"
},
"reducer": {
"@id": "https://noocodec.dev/ontology/dag/reducer"
},
"outcome": {
"@id": "https://noocodec.dev/ontology/dag/outcome"
},
"phase": {
"@id": "https://noocodec.dev/ontology/dag/phase"
},
"stateMapping": {
"@id": "https://noocodec.dev/ontology/dag/stateMapping"
},
"container": {
"@id": "https://noocodec.dev/ontology/dag/container"
},
"DAG": {
"@id": "https://noocodec.dev/ontology/dag/DAG"
},
"Placement": {
"@id": "https://noocodec.dev/ontology/dag/Placement"
},
"SingleNode": {
"@id": "https://noocodec.dev/ontology/dag/SingleNode"
},
"ScatterNode": {
"@id": "https://noocodec.dev/ontology/dag/ScatterNode"
},
"EmbeddedDAGNode": {
"@id": "https://noocodec.dev/ontology/dag/EmbeddedDAGNode"
},
"GatherNode": {
"@id": "https://noocodec.dev/ontology/dag/GatherNode"
},
"TerminalNode": {
"@id": "https://noocodec.dev/ontology/dag/TerminalNode"
},
"PhaseNode": {
"@id": "https://noocodec.dev/ontology/dag/PhaseNode"
}
},
"@id": "urn:noocodec:dag:reservoir-demo",
"@type": "DAG",
"name": "reservoir-demo",
"version": "1",
"entrypoints": {
"main": "urn:noocodec:dag:reservoir-demo/node/batch-score"
},
"nodes": [
{
"@id": "urn:noocodec:dag:reservoir-demo/node/batch-score",
"@type": "ScatterNode",
"name": "batch-score",
"body": {
"node": "urn:noocodec:node:score"
},
"source": "items",
"itemKey": "item",
"execution": {
"mode": "reservoir",
"concurrency": 4,
"reservoir": {
"keyField": "route",
"capacity": 10,
"idleMs": 500
}
},
"outputs": {
"all-success": "urn:noocodec:dag:reservoir-demo/node/collect-top",
"partial": "urn:noocodec:dag:reservoir-demo/node/collect-top",
"all-error": "urn:noocodec:dag:reservoir-demo/node/collect-top",
"empty": "urn:noocodec:dag:reservoir-demo/node/end"
}
},
{
"@id": "urn:noocodec:dag:reservoir-demo/node/collect-top",
"@type": "GatherNode",
"name": "collect-top",
"sources": {
"urn:noocodec:dag:reservoir-demo/node/batch-score": {}
},
"gather": {
"strategy": "top-n",
"target": "topCandidates"
},
"outputs": {
"success": "urn:noocodec:dag:reservoir-demo/node/end",
"error": "urn:noocodec:dag:reservoir-demo/node/end",
"empty": "urn:noocodec:dag:reservoir-demo/node/end"
}
},
{
"@id": "urn:noocodec:dag:reservoir-demo/node/end",
"@type": "TerminalNode",
"name": "end",
"outcome": "completed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% reservoir-demo (v1)
entry_main(["main"])
entry_main --> urn_noocodec_dag_reservoir-demo/node/batch-score
urn_noocodec_dag_reservoir-demo/node/batch-score[/"batch-score ▣ route ×10"/]
urn_noocodec_dag_reservoir-demo/node/batch-score -->|all-success| urn_noocodec_dag_reservoir-demo/node/collect-top
urn_noocodec_dag_reservoir-demo/node/batch-score -->|partial| urn_noocodec_dag_reservoir-demo/node/collect-top
urn_noocodec_dag_reservoir-demo/node/batch-score -->|all-error| urn_noocodec_dag_reservoir-demo/node/collect-top
urn_noocodec_dag_reservoir-demo/node/batch-score -->|empty| urn_noocodec_dag_reservoir-demo/node/end
urn_noocodec_dag_reservoir-demo/node/collect-top{"collect-top"}
urn_noocodec_dag_reservoir-demo/node/collect-top -->|success| urn_noocodec_dag_reservoir-demo/node/end
urn_noocodec_dag_reservoir-demo/node/collect-top -->|error| urn_noocodec_dag_reservoir-demo/node/end
urn_noocodec_dag_reservoir-demo/node/collect-top -->|empty| urn_noocodec_dag_reservoir-demo/node/end
urn_noocodec_dag_reservoir-demo/node/end((("end")))
classDef reservoir fill:#1e3a5f,stroke:#3b82f6,color:#bfdbfe
class urn_noocodec_dag_reservoir-demo/node/batch-score reservoirRun
npx tsx examples/scatter-extensions.tsWhat It Lets You Do
Scatter extensions let applications add domain-specific gather and reducer behavior without changing dispatcher code. Use them when built-in strategies do not match the merge policy or success threshold your DAG needs.
GatherStrategies.register and OutcomeReducers.register install custom plugins into the global registries. A scatter placement then references them by strategy/reducer key. No dispatcher or DAG document changes are needed beyond the gather.strategy and reducer keys.
This example registers two plugins:
TopNGatherStrategy— collects the top-3 candidates by score from each clone's state intostate.topCandidates.ThresholdReducer— gates'success'on ≥ 75% of clones returning'success'; routes'partial'below that threshold.
Code Samples
/**
* scatter-extensions: registers and uses the custom TopNGatherStrategy and
* ThresholdReducer from dags/scatter-extensions.ts in a live scatter DAG.
*
* Importing dags/scatter-extensions.ts triggers the two side-effect
* GatherStrategies.register / OutcomeReducers.register calls that install the
* plugins into the global registries. A scatter DAG then references them by
* name ('top-n' gather strategy, 'threshold-75' reducer).
*
* The worker node writes a scored { title, score } candidate to each clone's
* state. TopNGatherStrategy collects the top-3 by score into state.topCandidates.
* ThresholdReducer gates success on >= 75% of clones returning 'success'.
*
* DAG definition (GatherStrategy + OutcomeReducer registrations, ScoreNode, RankingState): examples/dags/scatter-extensions.ts
*
* Run: npx tsx examples/scatter-extensions.ts
*/
// Import triggers the registry.register calls for 'top-n' and 'threshold-75'.
import {
RankingState,
ScoreNode,
} from './dags/scatter-extensions.js';
import {
DAG_CONTEXT,
Dagonizer,
GatherStrategies,
OutcomeReducers,
} from '@studnicky/dagonizer';
import type { DAGType } from '@studnicky/dagonizer';
// ── DAG: scatter over items, gather with 'top-n', reduce with 'threshold-75' ─
const TOP_N_RANKING_DAG_IRI = 'urn:noocodec:dag:top-n-ranking';
const placement = (placementIdentifier: string): string => `${TOP_N_RANKING_DAG_IRI}/node/${encodeURIComponent(placementIdentifier)}`;
const dag: DAGType = {
'@context': DAG_CONTEXT,
'@id': TOP_N_RANKING_DAG_IRI,
'@type': 'DAG',
name: 'top-n-ranking',
version: '1',
entrypoints: { main: placement('rank-all') },
nodes: [
{
'@id': placement('rank-all'),
'@type': 'ScatterNode',
name: 'rank-all',
body: { node: 'urn:noocodec:node:score' },
source: 'items',
itemKey: 'item',
execution: { mode: 'item', concurrency: 5 },
reducer: 'threshold-75', // custom OutcomeReducer: >= 75% success
outputs: {
'all-success': placement('collect-top'),
partial: placement('collect-top'),
'all-error': placement('collect-top'),
empty: placement('end'),
},
},
{
'@id': placement('collect-top'),
'@type': 'GatherNode',
name: 'collect-top',
sources: { [placement('rank-all')]: {} },
gather: {
strategy: 'top-n',
target: 'topCandidates',
},
outputs: { success: placement('end'), error: placement('end'), empty: placement('end') },
},
{
'@id': placement('end'),
'@type': 'TerminalNode',
name: 'end',
outcome: 'completed',
},
],
};
// ── Verify plugins registered before dispatch ─────────────────────────────────
process.stdout.write('\n=== scatter-extensions: custom GatherStrategy + OutcomeReducer ===\n\n');
process.stdout.write(`Registered gather strategies: ${GatherStrategies.list().join(', ')}\n`);
process.stdout.write(`Registered outcome reducers: ${OutcomeReducers.list().join(', ')}\n\n`);
// ── Run ──────────────────────────────────────────────────────────────────────
const dispatcher = new Dagonizer<RankingState>();
dispatcher.registerNode(new ScoreNode());
dispatcher.registerDAG(dag);
const state = new RankingState();
// Items with varying title lengths → varying scores
state.items = [
'A',
'Medium length title',
'This is a longer candidate title',
'Short',
'Another medium item here',
'x',
];
await dispatcher.execute(TOP_N_RANKING_DAG_IRI, state);
process.stdout.write(`topCandidates (top-3 by score):\n`);
for (const c of state.topCandidates) {
process.stdout.write(` { title: "${c.title}", score: ${String(c.score)} }\n`);
}
process.stdout.write('\nLesson: GatherStrategies.register + OutcomeReducers.register install\n');
process.stdout.write(' extension keys globally; DAG topology still routes by placement IRI.\n');Details for Nerds
GatherStrategies.register(strategy). Installs a customGatherStrategyimplementation into the global registry understrategy.name. Any gather configuration whosestrategyfield matches the key uses this implementation.- Custom
GatherStrategy. Implementreduce(config, batch, state, accessor)(called per clone as it completes) and overridefinalize(config, execution)for all-at-once processing after every clone is done. TheTopNGatherStrategyaccumulates nothing inreduceand sorts by score infinalize, keeping only the top 3 and merging intostate.topCandidates. OutcomeReducers.register(reducer). Installs a customOutcomeReducerinto the global registry underreducer.name. A scatter placement'sreducerfield references that key.- Custom
OutcomeReducer. Receives the array of clone outcomes ('success'/'error'/'empty') and returns the routing token for the parent's output port.ThresholdReducercomputes the success ratio and routes'success'at ≥ 75%,'partial'below. - Side-effect registration. Importing
examples/dags/scatter-extensions.tstriggers theregistercalls. The convention is to register in the module that defines the plugin, so any application that imports the module gets the plugin installed automatically.
Related Concepts
- Example 14: Gather strategies - collect vs discard: built-in strategy comparison
- Example 15: Incremental gather - reduce and finalize hook timing
- Example 04: Scatter Scout - scatter mechanics: source, body DAG, gather placement, reduce
- Reference: Core, GatherStrategies