Example 13: Multi-Backend Roles
What It Is
Multi-Backend Roles let one application DAG send different placements to different execution backends. The Cartographer assigns stream processing to cpu and summary generation to io while preserving the same JSON-LD graph.
The role names are deployment labels, not new workflow primitives. The graph stays portable because it asks for cpu and io; the host decides whether those roles mean browser workers, Node worker threads, forked processes, or in-process execution.
How It Works
Each placement declares only a logical role name. The host decides what backend satisfies that role: a browser worker pool, Node worker threads, forked processes, or in-process execution. The JSON-LD graph remains portable because topology references roles, not concrete transports.
Runtime behavior
Run the browser demo from the docs dev server:
pnpm run docs:devThen open The Cartographer, click Run, and watch the DAG pane. The graph expands the same registered DAGs shown above:
process-streamfans out throughstream-eventon thecpurole.summarize-insightsinvokesinsights-summaryon theiorole.- The parent DAG stays a JSON-LD graph of placements, routes, and container role names.
Diagrams, Examples, and Outputs
The diagrams are generated from the Cartographer worker DAGs the browser demo executes, so role labels and embedded body DAGs stay visible beside their JSON-LD.
DAG registration and diagram
The in-browser Cartographer demo is the executable example for multi-backend role binding. The same JSON-LD assembly expresses both:
process-streamis aScatterNodedelegated to container rolecpu.summarize-insightsis anEmbeddedDAGNodedelegated to container roleio.
The browser runner binds both roles to real WebWorkerContainer pools. The DAG does not change when a role is in-process, in a worker, or supplied by a plugin registry; the canonical assembly remains JSON-LD produced by the builder.
Top-level browser DAG
cartographerWorkersDAG is the DAG rendered and executed by the Cartographer page. The Mermaid diagram is generated from the JSON-LD below it, so the container-role labels are visible in the same shape the dispatcher executes.
Cartographer workers DAG
6 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:cartographer",
"@type": "DAG",
"name": "dag:cartographer",
"version": "1.0",
"entrypoints": {
"position-ping": "urn:noocodec:dag:cartographer/node/intake-gather",
"facility-scan": "urn:noocodec:dag:cartographer/node/intake-gather",
"sensor-reading": "urn:noocodec:dag:cartographer/node/intake-gather",
"customs-event": "urn:noocodec:dag:cartographer/node/intake-gather",
"delivery-confirmation": "urn:noocodec:dag:cartographer/node/intake-gather"
},
"nodes": [
{
"@id": "urn:noocodec:dag:cartographer/node/intake-gather",
"@type": "GatherNode",
"name": "dag:cartographer/node/intake-gather",
"sources": {
"urn:noocodec:dag:cartographer/entrypoint/position-ping": {},
"urn:noocodec:dag:cartographer/entrypoint/facility-scan": {},
"urn:noocodec:dag:cartographer/entrypoint/sensor-reading": {},
"urn:noocodec:dag:cartographer/entrypoint/customs-event": {},
"urn:noocodec:dag:cartographer/entrypoint/delivery-confirmation": {}
},
"gather": {
"strategy": "source-intake"
},
"outputs": {
"success": "urn:noocodec:dag:cartographer/node/process-stream",
"error": "urn:noocodec:dag:cartographer/node/process-stream",
"empty": "urn:noocodec:dag:cartographer/node/failed"
}
},
{
"@id": "urn:noocodec:dag:cartographer/node/process-stream",
"@type": "ScatterNode",
"name": "dag:cartographer/node/process-stream",
"source": "sources",
"body": {
"dag": "urn:noocodec:dag:stream-event"
},
"outputs": {
"all-success": "urn:noocodec:dag:cartographer/node/fold-insights",
"partial": "urn:noocodec:dag:cartographer/node/fold-insights",
"all-error": "urn:noocodec:dag:cartographer/node/fold-insights",
"empty": "urn:noocodec:dag:cartographer/node/summarize-insights"
},
"itemKey": "source-payload",
"reducer": "aggregate",
"container": "cpu",
"execution": {
"mode": "reservoir",
"concurrency": 16,
"reservoir": {
"keyField": "eventType",
"capacity": 1000
}
}
},
{
"@id": "urn:noocodec:dag:cartographer/node/fold-insights",
"@type": "GatherNode",
"name": "dag:cartographer/node/fold-insights",
"sources": {
"urn:noocodec:dag:cartographer/node/process-stream": {}
},
"gather": {
"strategy": "insights-fold"
},
"outputs": {
"success": "urn:noocodec:dag:cartographer/node/summarize-insights",
"error": "urn:noocodec:dag:cartographer/node/summarize-insights",
"empty": "urn:noocodec:dag:cartographer/node/summarize-insights"
}
},
{
"@id": "urn:noocodec:dag:cartographer/node/summarize-insights",
"@type": "EmbeddedDAGNode",
"name": "dag:cartographer/node/summarize-insights",
"outputs": {
"success": "urn:noocodec:dag:cartographer/node/done",
"error": "urn:noocodec:dag:cartographer/node/failed"
},
"dag": "urn:noocodec:dag:insights-summary",
"container": "io"
},
{
"@id": "urn:noocodec:dag:cartographer/node/done",
"@type": "TerminalNode",
"name": "dag:cartographer/node/done",
"outcome": "completed"
},
{
"@id": "urn:noocodec:dag:cartographer/node/failed",
"@type": "TerminalNode",
"name": "dag:cartographer/node/failed",
"outcome": "failed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% dag:cartographer (v1.0)
entry_position-ping(["position-ping"])
entry_position-ping --> urn_noocodec_dag_cartographer/node/intake-gather
entry_facility-scan(["facility-scan"])
entry_facility-scan --> urn_noocodec_dag_cartographer/node/intake-gather
entry_sensor-reading(["sensor-reading"])
entry_sensor-reading --> urn_noocodec_dag_cartographer/node/intake-gather
entry_customs-event(["customs-event"])
entry_customs-event --> urn_noocodec_dag_cartographer/node/intake-gather
entry_delivery-confirmation(["delivery-confirmation"])
entry_delivery-confirmation --> urn_noocodec_dag_cartographer/node/intake-gather
urn_noocodec_dag_cartographer/node/intake-gather{"dag_cartographer/node/intake-gather"}
urn_noocodec_dag_cartographer/node/intake-gather -->|success| urn_noocodec_dag_cartographer/node/process-stream
urn_noocodec_dag_cartographer/node/intake-gather -->|error| urn_noocodec_dag_cartographer/node/process-stream
urn_noocodec_dag_cartographer/node/intake-gather -->|empty| urn_noocodec_dag_cartographer/node/failed
urn_noocodec_dag_cartographer/node/process-stream[/"dag:cartographer/node/process-stream ▣ eventType ×1000"/]
urn_noocodec_dag_cartographer/node/process-stream -->|all-success| urn_noocodec_dag_cartographer/node/fold-insights
urn_noocodec_dag_cartographer/node/process-stream -->|partial| urn_noocodec_dag_cartographer/node/fold-insights
urn_noocodec_dag_cartographer/node/process-stream -->|all-error| urn_noocodec_dag_cartographer/node/fold-insights
urn_noocodec_dag_cartographer/node/process-stream -->|empty| urn_noocodec_dag_cartographer/node/summarize-insights
urn_noocodec_dag_cartographer/node/fold-insights{"dag_cartographer/node/fold-insights"}
urn_noocodec_dag_cartographer/node/fold-insights -->|success| urn_noocodec_dag_cartographer/node/summarize-insights
urn_noocodec_dag_cartographer/node/fold-insights -->|error| urn_noocodec_dag_cartographer/node/summarize-insights
urn_noocodec_dag_cartographer/node/fold-insights -->|empty| urn_noocodec_dag_cartographer/node/summarize-insights
urn_noocodec_dag_cartographer/node/summarize-insights[["dag:cartographer/node/summarize-insights"]]
urn_noocodec_dag_cartographer/node/summarize-insights -->|success| urn_noocodec_dag_cartographer/node/done
urn_noocodec_dag_cartographer/node/summarize-insights -->|error| urn_noocodec_dag_cartographer/node/failed
urn_noocodec_dag_cartographer/node/done((("dag:cartographer/node/done")))
urn_noocodec_dag_cartographer/node/failed>"dag:cartographer/node/failed"]
classDef contained-cpu fill:#b45309,stroke:#d97706,color:#eef3f7
class urn_noocodec_dag_cartographer/node/process-stream contained-cpu
classDef contained-io fill:#7c3aed,stroke:#8b5cf6,color:#eef3f7
class urn_noocodec_dag_cartographer/node/summarize-insights contained-io
classDef reservoir fill:#1e3a5f,stroke:#3b82f6,color:#bfdbfe
class urn_noocodec_dag_cartographer/node/process-stream reservoir/** Default reservoir capacity for the process-stream scatter in the workers DAG. */
export const DEFAULT_RESERVOIR_CAPACITY = 1000;
/**
* CartographerWorkersDag: static factory for the cartographer-workers DAG and
* its associated dispatcher bundle. Consumers call CartographerWorkersDag.build()
* (DAG only) or CartographerWorkersDag.bundle() (full dispatcher bundle) with an
* optional reservoir capacity override.
*
* DAG topology — identical to cartographerDAG with containerized boundaries:
* - container: 'cpu' so each stream-event body runs inside a
* WorkerThreadContainer/WebWorkerContainer rather than in-process.
* - container: 'io' so the final summary runs through the same embedded-DAG
* interface used by plugins and nested flows.
* - reservoir.capacity is parameterised; callers pass their UI-controlled
* batch size rather than relying on the compile-time default.
*
* 5 data-type entrypoints → gather('intake-gather', source-intake)
* → scatter('process-stream', 'sources', { dag: 'stream-event' },
* concurrency: 16, container: 'cpu', reservoir: { capacity })
* → gather('fold-insights', strategy: insights-fold)
* → embed('summarize-insights', 'insights-summary', container: 'io')
* → done
*/
export class CartographerWorkersDag {
private constructor() { /* static-only */ }
/**
* Build the cartographer-workers DAG with the given reservoir capacity.
* CLI, smoke tests, and dag-validate consumers use cartographerWorkersDAG
* (the pre-built constant); the browser demo calls this with a UI-controlled value.
*/
static build(capacity: number = DEFAULT_RESERVOIR_CAPACITY): DAGType {
return new DAGBuilder(CARTOGRAPHER_DAG_IRI, '1.0')
.gather(
CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'intake-gather'),
CARTOGRAPHER_IRIS.intakeSources(CARTOGRAPHER_DAG_IRI),
{ 'strategy': 'source-intake' },
{
'success': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'process-stream'),
'error': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'process-stream'),
'empty': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'failed'),
},
)
.scatter(
CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'process-stream'),
'sources',
{ 'dag': STREAM_EVENT_DAG_IRI },
{
'all-success': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'fold-insights'),
'partial': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'fold-insights'),
'all-error': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'fold-insights'),
'empty': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'summarize-insights'),
},
{
'itemKey': 'source-payload',
'container': 'cpu',
'execution': { 'mode': 'reservoir', 'concurrency': 16, 'reservoir': { 'keyField': 'eventType', 'capacity': capacity } },
},
)
.gather(CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'fold-insights'), {
[CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'process-stream')]: {},
}, { 'strategy': 'insights-fold' }, {
'success': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'summarize-insights'),
'error': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'summarize-insights'),
'empty': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'summarize-insights'),
})
.embed<CartographerState, CartographerState>(CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'summarize-insights'), INSIGHTS_SUMMARY_DAG_IRI, {
'success': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'done'),
'error': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'failed'),
}, {
'container': 'io',
})
.terminal(CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'done'), { outcome: 'completed' })
.terminal(CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'failed'), { outcome: 'failed' })
.entrypoints({
'position-ping': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'intake-gather'),
'facility-scan': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'intake-gather'),
'sensor-reading': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'intake-gather'),
'customs-event': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'intake-gather'),
'delivery-confirmation': CARTOGRAPHER_IRIS.placementIri(CARTOGRAPHER_DAG_IRI, 'intake-gather'),
})
.build();
}
/**
* Build the workers bundle with a configurable reservoir capacity. The returned
* bundle is identical to cartographerWorkersBundle except that its cartographer
* DAG is built with CartographerWorkersDag.build(capacity) so the process-stream
* scatter uses the caller-supplied batch size.
*
* Used by the browser demo to wire UI-controlled knobs into each run() without
* mutating the shared default-capacity constants.
*/
static bundle(
capacity: number = DEFAULT_RESERVOIR_CAPACITY,
): DispatcherBundleType<CartographerState> {
return {
'nodes': [
...cartographerWorkerRuntimeBundle.nodes,
],
'dags': [
...cartographerWorkerRuntimeBundle.dags,
CartographerWorkersDag.build(capacity),
],
};
}
}
/**
* cartographerWorkersDAG: pre-built workers DAG at DEFAULT_RESERVOIR_CAPACITY.
* CLI, smoke tests, and dag-validate consumers use this constant; the browser
* demo uses CartographerWorkersDag.build(capacity) with a UI-controlled value.
*/
export const cartographerWorkersDAG: DAGType = CartographerWorkersDag.build();cpu body DAG
The cpu role runs the stream-event body for every source payload. This is not a synthetic worker sample; it is the live Cartographer decode and routing pipeline.
stream-event body DAG
9 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:stream-event",
"@type": "DAG",
"name": "dag:stream-event",
"version": "1.0",
"entrypoints": {
"main": "urn:noocodec:dag:stream-event/node/decode-payload"
},
"nodes": [
{
"@id": "urn:noocodec:dag:stream-event/node/decode-payload",
"@type": "SingleNode",
"name": "dag:stream-event/node/decode-payload",
"node": "urn:noocodec:node:decode-payload",
"outputs": {
"decoded": "urn:noocodec:dag:stream-event/node/route-event-type-variant",
"invalid": "urn:noocodec:dag:stream-event/node/rejected"
}
},
{
"@id": "urn:noocodec:dag:stream-event/node/route-event-type-variant",
"@type": "SingleNode",
"name": "dag:stream-event/node/route-event-type-variant",
"node": "urn:noocodec:node:route-event-type-variant",
"outputs": {
"position-ping": "urn:noocodec:dag:stream-event/node/pipeline-position-ping",
"sensor-reading": "urn:noocodec:dag:stream-event/node/pipeline-sensor-reading",
"customs-event": "urn:noocodec:dag:stream-event/node/pipeline-customs-event",
"facility-scan": "urn:noocodec:dag:stream-event/node/pipeline-facility-scan",
"delivery-confirmation": "urn:noocodec:dag:stream-event/node/pipeline-delivery-confirmation"
}
},
{
"@id": "urn:noocodec:dag:stream-event/node/pipeline-position-ping",
"@type": "EmbeddedDAGNode",
"name": "dag:stream-event/node/pipeline-position-ping",
"outputs": {
"success": "urn:noocodec:dag:stream-event/node/done",
"error": "urn:noocodec:dag:stream-event/node/rejected"
},
"dag": "urn:noocodec:dag:pipeline-position-ping",
"stateMapping": {
"output": {
"canonicalVariant": "canonicalVariant",
"raw": "raw",
"normalized": "normalized",
"currentEvent": "currentEvent",
"geoContext": "geoContext",
"resolvedGeo": "resolvedGeo",
"legKm": "legKm",
"routing": "routing",
"enriched": "enriched",
"capturedErrors": "capturedErrors"
}
}
},
{
"@id": "urn:noocodec:dag:stream-event/node/pipeline-sensor-reading",
"@type": "EmbeddedDAGNode",
"name": "dag:stream-event/node/pipeline-sensor-reading",
"outputs": {
"success": "urn:noocodec:dag:stream-event/node/done",
"error": "urn:noocodec:dag:stream-event/node/rejected"
},
"dag": "urn:noocodec:dag:pipeline-sensor-reading",
"stateMapping": {
"output": {
"canonicalVariant": "canonicalVariant",
"raw": "raw",
"normalized": "normalized",
"currentEvent": "currentEvent",
"geoContext": "geoContext",
"resolvedGeo": "resolvedGeo",
"coldChainBreach": "coldChainBreach",
"legKm": "legKm",
"routing": "routing",
"enriched": "enriched",
"capturedErrors": "capturedErrors"
}
}
},
{
"@id": "urn:noocodec:dag:stream-event/node/pipeline-customs-event",
"@type": "EmbeddedDAGNode",
"name": "dag:stream-event/node/pipeline-customs-event",
"outputs": {
"success": "urn:noocodec:dag:stream-event/node/done",
"error": "urn:noocodec:dag:stream-event/node/rejected"
},
"dag": "urn:noocodec:dag:pipeline-customs-event",
"stateMapping": {
"output": {
"canonicalVariant": "canonicalVariant",
"raw": "raw",
"normalized": "normalized",
"currentEvent": "currentEvent",
"geoContext": "geoContext",
"resolvedGeo": "resolvedGeo",
"customsDwellHours": "customsDwellHours",
"legKm": "legKm",
"routing": "routing",
"enriched": "enriched",
"capturedErrors": "capturedErrors"
}
}
},
{
"@id": "urn:noocodec:dag:stream-event/node/pipeline-facility-scan",
"@type": "EmbeddedDAGNode",
"name": "dag:stream-event/node/pipeline-facility-scan",
"outputs": {
"success": "urn:noocodec:dag:stream-event/node/done",
"error": "urn:noocodec:dag:stream-event/node/rejected"
},
"dag": "urn:noocodec:dag:pipeline-facility-scan",
"stateMapping": {
"output": {
"canonicalVariant": "canonicalVariant",
"raw": "raw",
"normalized": "normalized",
"currentEvent": "currentEvent",
"geoContext": "geoContext",
"resolvedGeo": "resolvedGeo",
"pricedOrder": "pricedOrder",
"shippingQuote": "shippingQuote",
"deliveryEstimate": "deliveryEstimate",
"legKm": "legKm",
"gdprResult": "gdprResult",
"routing": "routing",
"enriched": "enriched",
"capturedErrors": "capturedErrors"
}
}
},
{
"@id": "urn:noocodec:dag:stream-event/node/pipeline-delivery-confirmation",
"@type": "EmbeddedDAGNode",
"name": "dag:stream-event/node/pipeline-delivery-confirmation",
"outputs": {
"success": "urn:noocodec:dag:stream-event/node/done",
"error": "urn:noocodec:dag:stream-event/node/rejected"
},
"dag": "urn:noocodec:dag:pipeline-delivery-confirmation",
"stateMapping": {
"output": {
"canonicalVariant": "canonicalVariant",
"raw": "raw",
"normalized": "normalized",
"currentEvent": "currentEvent",
"geoContext": "geoContext",
"resolvedGeo": "resolvedGeo",
"legKm": "legKm",
"gdprResult": "gdprResult",
"routing": "routing",
"enriched": "enriched",
"capturedErrors": "capturedErrors"
}
}
},
{
"@id": "urn:noocodec:dag:stream-event/node/done",
"@type": "TerminalNode",
"name": "dag:stream-event/node/done",
"outcome": "completed"
},
{
"@id": "urn:noocodec:dag:stream-event/node/rejected",
"@type": "TerminalNode",
"name": "dag:stream-event/node/rejected",
"outcome": "failed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% dag:stream-event (v1.0)
entry_main(["main"])
entry_main --> urn_noocodec_dag_stream-event/node/decode-payload
urn_noocodec_dag_stream-event/node/decode-payload["dag:stream-event/node/decode-payload"]
urn_noocodec_dag_stream-event/node/decode-payload -->|decoded| urn_noocodec_dag_stream-event/node/route-event-type-variant
urn_noocodec_dag_stream-event/node/decode-payload -->|invalid| urn_noocodec_dag_stream-event/node/rejected
urn_noocodec_dag_stream-event/node/route-event-type-variant["dag:stream-event/node/route-event-type-variant"]
urn_noocodec_dag_stream-event/node/route-event-type-variant -->|position-ping| urn_noocodec_dag_stream-event/node/pipeline-position-ping
urn_noocodec_dag_stream-event/node/route-event-type-variant -->|sensor-reading| urn_noocodec_dag_stream-event/node/pipeline-sensor-reading
urn_noocodec_dag_stream-event/node/route-event-type-variant -->|customs-event| urn_noocodec_dag_stream-event/node/pipeline-customs-event
urn_noocodec_dag_stream-event/node/route-event-type-variant -->|facility-scan| urn_noocodec_dag_stream-event/node/pipeline-facility-scan
urn_noocodec_dag_stream-event/node/route-event-type-variant -->|delivery-confirmation| urn_noocodec_dag_stream-event/node/pipeline-delivery-confirmation
urn_noocodec_dag_stream-event/node/pipeline-position-ping[["dag:stream-event/node/pipeline-position-ping"]]
urn_noocodec_dag_stream-event/node/pipeline-position-ping -->|success| urn_noocodec_dag_stream-event/node/done
urn_noocodec_dag_stream-event/node/pipeline-position-ping -->|error| urn_noocodec_dag_stream-event/node/rejected
urn_noocodec_dag_stream-event/node/pipeline-sensor-reading[["dag:stream-event/node/pipeline-sensor-reading"]]
urn_noocodec_dag_stream-event/node/pipeline-sensor-reading -->|success| urn_noocodec_dag_stream-event/node/done
urn_noocodec_dag_stream-event/node/pipeline-sensor-reading -->|error| urn_noocodec_dag_stream-event/node/rejected
urn_noocodec_dag_stream-event/node/pipeline-customs-event[["dag:stream-event/node/pipeline-customs-event"]]
urn_noocodec_dag_stream-event/node/pipeline-customs-event -->|success| urn_noocodec_dag_stream-event/node/done
urn_noocodec_dag_stream-event/node/pipeline-customs-event -->|error| urn_noocodec_dag_stream-event/node/rejected
urn_noocodec_dag_stream-event/node/pipeline-facility-scan[["dag:stream-event/node/pipeline-facility-scan"]]
urn_noocodec_dag_stream-event/node/pipeline-facility-scan -->|success| urn_noocodec_dag_stream-event/node/done
urn_noocodec_dag_stream-event/node/pipeline-facility-scan -->|error| urn_noocodec_dag_stream-event/node/rejected
urn_noocodec_dag_stream-event/node/pipeline-delivery-confirmation[["dag:stream-event/node/pipeline-delivery-confirmation"]]
urn_noocodec_dag_stream-event/node/pipeline-delivery-confirmation -->|success| urn_noocodec_dag_stream-event/node/done
urn_noocodec_dag_stream-event/node/pipeline-delivery-confirmation -->|error| urn_noocodec_dag_stream-event/node/rejected
urn_noocodec_dag_stream-event/node/done((("dag:stream-event/node/done")))
urn_noocodec_dag_stream-event/node/rejected>"dag:stream-event/node/rejected"]const STREAM_EVENT_DAG_IRI = CARTOGRAPHER_IRIS.dag.streamEvent;
export const streamEventDAG: DAGType = new DAGBuilder(STREAM_EVENT_DAG_IRI, '1.0')
// 1. decode-payload: reads 'source-payload' metadata, decodes wire format
// (json/csv/ndjson/yaml, optionally gzip), builds CanonicalEventVariant,
// and sets state.canonicalVariant. Routes 'decoded' or 'invalid'.
.node(CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'decode-payload'), decodePayload, {
'decoded': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'route-event-type-variant'),
'invalid': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'rejected'),
})
// 2. route-event-type-variant: reads state.canonicalVariant.eventType and
// dispatches to the corresponding per-type sub-DAG. Sets state.routing.
.node(CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'route-event-type-variant'), routeEventType, {
'position-ping': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'pipeline-position-ping'),
'sensor-reading': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'pipeline-sensor-reading'),
'customs-event': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'pipeline-customs-event'),
'facility-scan': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'pipeline-facility-scan'),
'delivery-confirmation': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'pipeline-delivery-confirmation'),
})
// 3a. pipeline-position-ping: geo + leg measurement.
.embed<CartographerState, CartographerState>(CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'pipeline-position-ping'), CARTOGRAPHER_IRIS.dag.pipelinePositionPing, {
'success': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'done'),
'error': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'rejected'),
}, {
'outputs': {
'canonicalVariant': 'canonicalVariant',
'raw': 'raw',
'normalized': 'normalized',
'currentEvent': 'currentEvent',
'geoContext': 'geoContext',
'resolvedGeo': 'resolvedGeo',
'legKm': 'legKm',
'routing': 'routing',
'enriched': 'enriched',
'capturedErrors': 'capturedErrors',
},
})
// 3b. pipeline-sensor-reading: geo + cold-chain + leg measurement.
.embed<CartographerState, CartographerState>(CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'pipeline-sensor-reading'), CARTOGRAPHER_IRIS.dag.pipelineSensorReading, {
'success': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'done'),
'error': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'rejected'),
}, {
'outputs': {
'canonicalVariant': 'canonicalVariant',
'raw': 'raw',
'normalized': 'normalized',
'currentEvent': 'currentEvent',
'geoContext': 'geoContext',
'resolvedGeo': 'resolvedGeo',
'coldChainBreach': 'coldChainBreach',
'legKm': 'legKm',
'routing': 'routing',
'enriched': 'enriched',
'capturedErrors': 'capturedErrors',
},
})
// 3c. pipeline-customs-event: geo + customs-dwell + leg measurement.
.embed<CartographerState, CartographerState>(CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'pipeline-customs-event'), CARTOGRAPHER_IRIS.dag.pipelineCustomsEvent, {
'success': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'done'),
'error': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'rejected'),
}, {
'outputs': {
'canonicalVariant': 'canonicalVariant',
'raw': 'raw',
'normalized': 'normalized',
'currentEvent': 'currentEvent',
'geoContext': 'geoContext',
'resolvedGeo': 'resolvedGeo',
'customsDwellHours': 'customsDwellHours',
'legKm': 'legKm',
'routing': 'routing',
'enriched': 'enriched',
'capturedErrors': 'capturedErrors',
},
})
// 3d. pipeline-facility-scan: geo + facility canonicalization + order enrichment
// + GDPR-gated redaction.
.embed<CartographerState, CartographerState>(CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'pipeline-facility-scan'), CARTOGRAPHER_IRIS.dag.pipelineFacilityScan, {
'success': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'done'),
'error': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'rejected'),
}, {
'outputs': {
'canonicalVariant': 'canonicalVariant',
'raw': 'raw',
'normalized': 'normalized',
'currentEvent': 'currentEvent',
'geoContext': 'geoContext',
'resolvedGeo': 'resolvedGeo',
'pricedOrder': 'pricedOrder',
'shippingQuote': 'shippingQuote',
'deliveryEstimate': 'deliveryEstimate',
'legKm': 'legKm',
'gdprResult': 'gdprResult',
'routing': 'routing',
'enriched': 'enriched',
'capturedErrors': 'capturedErrors',
},
})
// 3e. pipeline-delivery-confirmation: geo + recipient canonicalization +
// delivery confirmation + GDPR-gated redaction.
.embed<CartographerState, CartographerState>(CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'pipeline-delivery-confirmation'), CARTOGRAPHER_IRIS.dag.pipelineDeliveryConfirmation, {
'success': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'done'),
'error': CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'rejected'),
}, {
'outputs': {
'canonicalVariant': 'canonicalVariant',
'raw': 'raw',
'normalized': 'normalized',
'currentEvent': 'currentEvent',
'geoContext': 'geoContext',
'resolvedGeo': 'resolvedGeo',
'legKm': 'legKm',
'gdprResult': 'gdprResult',
'routing': 'routing',
'enriched': 'enriched',
'capturedErrors': 'capturedErrors',
},
})
.terminal(CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'done'), { outcome: 'completed' })
.terminal(CARTOGRAPHER_IRIS.placementIri(STREAM_EVENT_DAG_IRI, 'rejected'), { outcome: 'failed' })
.build();
/**
* Bundle fragment for dag.ts. Registers decode-payload, route-event-type-variant,
* and the stream-event DAG with the dispatcher. routeEventType re-registration is
* a no-op (same instance; the bundle registrar is idempotent).
*/
export const streamEventBundle: DispatcherBundleType<CartographerState> = {
'nodes': [decodePayload, routeEventType],
'dags': [streamEventDAG],
};io body DAG
The io role runs the summary body as an embedded DAG after the scatter gather fold completes. Packaging the summary as a DAG keeps plugins, embedded flows, and container delegation on one interface.
insights-summary body DAG
2 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:insights-summary",
"@type": "DAG",
"name": "dag:insights-summary",
"version": "1.0",
"entrypoints": {
"main": "urn:noocodec:dag:insights-summary/node/summarize"
},
"nodes": [
{
"@id": "urn:noocodec:dag:insights-summary/node/summarize",
"@type": "SingleNode",
"name": "dag:insights-summary/node/summarize",
"node": "urn:noocodec:node:summarize",
"outputs": {
"success": "urn:noocodec:dag:insights-summary/node/done"
}
},
{
"@id": "urn:noocodec:dag:insights-summary/node/done",
"@type": "TerminalNode",
"name": "dag:insights-summary/node/done",
"outcome": "completed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% dag:insights-summary (v1.0)
entry_main(["main"])
entry_main --> urn_noocodec_dag_insights-summary/node/summarize
urn_noocodec_dag_insights-summary/node/summarize["dag:insights-summary/node/summarize"]
urn_noocodec_dag_insights-summary/node/summarize -->|success| urn_noocodec_dag_insights-summary/node/done
urn_noocodec_dag_insights-summary/node/done((("dag:insights-summary/node/done")))/**
* insights-summary: embedded summary body for the browser workers topology.
*
* The top-level workers DAG delegates this single-cardinality stage to the
* `io` container role after the `cpu` scatter finishes. The body is the same
* summarizeInsights node used by the in-process cartographer DAG, packaged as a
* registered DAG so the worker registry and JSON-LD assembly use the same
* canonical embed/plugin surface.
*/
export const insightsSummaryDAG: DAGType = new DAGBuilder(INSIGHTS_SUMMARY_DAG_IRI, '1.0')
.node(CARTOGRAPHER_IRIS.placementIri(INSIGHTS_SUMMARY_DAG_IRI, 'summarize'), summarizeInsights, {
'success': CARTOGRAPHER_IRIS.placementIri(INSIGHTS_SUMMARY_DAG_IRI, 'done'),
})
.terminal(CARTOGRAPHER_IRIS.placementIri(INSIGHTS_SUMMARY_DAG_IRI, 'done'), { outcome: 'completed' })
.build();What It Lets You Do
Multi-backend roles let applications bind different parts of one DAG to different execution backends without changing the canonical graph. Use this when CPU-bound stream processing, IO-bound summary work, and in-process orchestration need separate pools, quotas, or deployment targets.
Code Samples
Read the snippets with the diagrams nearby so the TypeScript behavior, JSON-LD graph shape, and runtime output line up as one contract.
Browser role binding
The runnable page creates two role bindings from the same registry-backed worker entry. The registry contains every DAG the worker can execute: the stream-event tree for cpu and the insights-summary DAG for io.
// Separate role bindings use the same worker entry and registry while still
// keeping the DAG's execution boundaries explicit: process-stream runs on
// `cpu`, summarize-insights runs on `io`.
const cpuContainer = new CartographerWorkerContainer({
'registryModule': new URL('./cartographerWorkerEntry.ts', import.meta.url).href,
'registryVersion': '1.0.0',
'servicesConfig': { 'useRecordedIp': true },
'poolSize': clampedPoolSize.value,
});
const ioContainer = new CartographerWorkerContainer({
'registryModule': new URL('./cartographerWorkerEntry.ts', import.meta.url).href,
'registryVersion': '1.0.0',
'servicesConfig': { 'useRecordedIp': true },
'poolSize': 1,
});
dispatcher = new CartographerBrowserObserver(_cartographerLogger, {
'containers': {
'cpu': cpuContainer,
'io': ioContainer,
},
});const registry: RegistryModuleInterface = {
async instantiate(servicesConfig: JsonObjectType): Promise<RegistryBundleInterface> {
const useRecorded = servicesConfig['useRecordedIp'] !== false;
const services = useRecorded ? GeoResolvers.recorded() : GeoResolvers.live();
const geoBundle = GeoSourceResolveDAG.build(services.ipGeolocator, services.addressGeocoder);
return {
'bundle': {
'nodes': [...geoBundle.nodes, ...cartographerWorkerRuntimeBundle.nodes],
'dags': [...geoBundle.dags, ...cartographerWorkerRuntimeBundle.dags],
},
'registryVersion': '1.0.0',
'restoreState': {
restore(snapshot: JsonObjectType) {
return CartographerState.restore(snapshot);
},
},
};
},
};
export default registry;Details for Nerds
Node CLI companion
The repository also keeps examples/13-multibackend.ts as a Node CLI companion for worker-thread plus fork-container execution:
pnpm example:13That CLI exercises the same renderer role-color path, but the browser runnable for this principle is the Cartographer code above.
- Role names preserve portability.
cpuandioare deployment labels, not new placement types. - Multiple backends share one registry interface. The worker registry contains every DAG either role can execute.
- JSON-LD remains canonical. Container delegation is a placement attribute in the same document the builder emits and the dispatcher consumes.
- Browser and Node hosts choose different backends. The docs runnable uses
WebWorkerContainer; the CLI companion can exercise Node container implementations.
Related Concepts
- The Cartographer - in-browser runnable demo that owns the multi-backend worker role pattern
- Example 12: Worker Containers - single worker-role container binding
- Guide: Distribution and Cloud - container and deployment patterns