Example 09: Terminal Nodes
What It Is
Terminal nodes are explicit graph endpoints. They tell Dagonizer whether a branch completes or fails, and they make "the flow stops here" visible in JSON-LD, Mermaid, lifecycle events, and embedded-DAG parent routing.
The runnable examples show the two shapes application code usually needs: a shared successful endpoint in The Dispatcher, and completed/failed child endpoints in The Cartographer's GDPR compliance sub-DAG.
How It Works
A terminal placement does not execute user code. It declares the endpoint reached by a named route and sets the lifecycle outcome for that branch. When a parent embeds a child DAG, the child's terminal outcome becomes the parent embedded placement's success or error output.
That means application code should route to terminals deliberately. A missing route is a graph bug; a terminal route is a documented outcome.
Diagrams, Examples, and Outputs
DAG registration and diagram
TerminalNode placements name the endpoints of a flow and carry an outcome declaration (completed or failed). Every flow branch must end at a named TerminalNode. The runnable examples show the two common patterns: shared completed terminals in a parent flow, and completed/failed terminals inside a reusable sub-DAG whose parent routes on success or error.
support-dispatcher
7 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:support-dispatcher",
"@type": "DAG",
"name": "dag:support-dispatcher",
"version": "1",
"entrypoints": {
"main": "urn:noocodec:dag:support-dispatcher/node/classify-message"
},
"nodes": [
{
"@id": "urn:noocodec:dag:support-dispatcher/node/setup",
"@type": "PhaseNode",
"name": "dag:support-dispatcher/node/setup",
"node": "urn:noocodec:node:dispatcher-setup",
"phase": "pre"
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/classify-message",
"@type": "SingleNode",
"name": "dag:support-dispatcher/node/classify-message",
"node": "urn:noocodec:node:classify-message",
"outputs": {
"routine": "urn:noocodec:dag:support-dispatcher/node/ai-compose",
"escalate": "urn:noocodec:dag:support-dispatcher/node/park-for-operator",
"off-topic": "urn:noocodec:dag:support-dispatcher/node/decline"
}
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/ai-compose",
"@type": "SingleNode",
"name": "dag:support-dispatcher/node/ai-compose",
"node": "urn:noocodec:node:ai-compose",
"outputs": {
"drafted": "urn:noocodec:dag:support-dispatcher/node/send-response"
}
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/park-for-operator",
"@type": "SingleNode",
"name": "dag:support-dispatcher/node/park-for-operator",
"node": "urn:noocodec:node:park-for-operator",
"outputs": {
"parked": "urn:noocodec:dag:support-dispatcher/node/end",
"ready": "urn:noocodec:dag:support-dispatcher/node/send-response"
}
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/send-response",
"@type": "SingleNode",
"name": "dag:support-dispatcher/node/send-response",
"node": "urn:noocodec:node:send-response",
"outputs": {
"sent": "urn:noocodec:dag:support-dispatcher/node/end"
}
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/decline",
"@type": "SingleNode",
"name": "dag:support-dispatcher/node/decline",
"node": "urn:noocodec:node:decline",
"outputs": {
"declined": "urn:noocodec:dag:support-dispatcher/node/end"
}
},
{
"@id": "urn:noocodec:dag:support-dispatcher/node/end",
"@type": "TerminalNode",
"name": "dag:support-dispatcher/node/end",
"outcome": "completed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% dag:support-dispatcher (v1)
entry_main(["main"])
entry_main --> urn_noocodec_dag_support-dispatcher/node/classify-message
urn_noocodec_dag_support-dispatcher/node/setup(["dag:support-dispatcher/node/setup (pre)"])
urn_noocodec_dag_support-dispatcher/node/classify-message["dag:support-dispatcher/node/classify-message"]
urn_noocodec_dag_support-dispatcher/node/classify-message -->|routine| urn_noocodec_dag_support-dispatcher/node/ai-compose
urn_noocodec_dag_support-dispatcher/node/classify-message -->|escalate| urn_noocodec_dag_support-dispatcher/node/park-for-operator
urn_noocodec_dag_support-dispatcher/node/classify-message -->|off-topic| urn_noocodec_dag_support-dispatcher/node/decline
urn_noocodec_dag_support-dispatcher/node/ai-compose["dag:support-dispatcher/node/ai-compose"]
urn_noocodec_dag_support-dispatcher/node/ai-compose -->|drafted| urn_noocodec_dag_support-dispatcher/node/send-response
urn_noocodec_dag_support-dispatcher/node/park-for-operator["dag:support-dispatcher/node/park-for-operator"]
urn_noocodec_dag_support-dispatcher/node/park-for-operator -->|parked| urn_noocodec_dag_support-dispatcher/node/end
urn_noocodec_dag_support-dispatcher/node/park-for-operator -->|ready| urn_noocodec_dag_support-dispatcher/node/send-response
urn_noocodec_dag_support-dispatcher/node/send-response["dag:support-dispatcher/node/send-response"]
urn_noocodec_dag_support-dispatcher/node/send-response -->|sent| urn_noocodec_dag_support-dispatcher/node/end
urn_noocodec_dag_support-dispatcher/node/decline["dag:support-dispatcher/node/decline"]
urn_noocodec_dag_support-dispatcher/node/decline -->|declined| urn_noocodec_dag_support-dispatcher/node/end
urn_noocodec_dag_support-dispatcher/node/end((("dag:support-dispatcher/node/end")))gdpr-compliance
5 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:gdpr-compliance",
"@type": "DAG",
"name": "dag:gdpr-compliance",
"version": "1.0",
"entrypoints": {
"main": "urn:noocodec:dag:gdpr-compliance/node/consent-gate"
},
"nodes": [
{
"@id": "urn:noocodec:dag:gdpr-compliance/node/consent-gate",
"@type": "SingleNode",
"name": "dag:gdpr-compliance/node/consent-gate",
"node": "urn:noocodec:node:consent-gate",
"outputs": {
"classify": "urn:noocodec:dag:gdpr-compliance/node/classify-pii"
}
},
{
"@id": "urn:noocodec:dag:gdpr-compliance/node/classify-pii",
"@type": "SingleNode",
"name": "dag:gdpr-compliance/node/classify-pii",
"node": "urn:noocodec:node:classify-pii",
"outputs": {
"redact": "urn:noocodec:dag:gdpr-compliance/node/redact-pii"
}
},
{
"@id": "urn:noocodec:dag:gdpr-compliance/node/redact-pii",
"@type": "SingleNode",
"name": "dag:gdpr-compliance/node/redact-pii",
"node": "urn:noocodec:node:redact-pii",
"outputs": {
"ok": "urn:noocodec:dag:gdpr-compliance/node/compliant",
"violation": "urn:noocodec:dag:gdpr-compliance/node/violation"
}
},
{
"@id": "urn:noocodec:dag:gdpr-compliance/node/compliant",
"@type": "TerminalNode",
"name": "dag:gdpr-compliance/node/compliant",
"outcome": "completed"
},
{
"@id": "urn:noocodec:dag:gdpr-compliance/node/violation",
"@type": "TerminalNode",
"name": "dag:gdpr-compliance/node/violation",
"outcome": "failed"
}
]
}Mermaid source
%%{init: {"flowchart":{"nodeSpacing":92,"rankSpacing":104,"padding":28}}}%%
flowchart TB
%% dag:gdpr-compliance (v1.0)
entry_main(["main"])
entry_main --> urn_noocodec_dag_gdpr-compliance/node/consent-gate
urn_noocodec_dag_gdpr-compliance/node/consent-gate["dag:gdpr-compliance/node/consent-gate"]
urn_noocodec_dag_gdpr-compliance/node/consent-gate -->|classify| urn_noocodec_dag_gdpr-compliance/node/classify-pii
urn_noocodec_dag_gdpr-compliance/node/classify-pii["dag:gdpr-compliance/node/classify-pii"]
urn_noocodec_dag_gdpr-compliance/node/classify-pii -->|redact| urn_noocodec_dag_gdpr-compliance/node/redact-pii
urn_noocodec_dag_gdpr-compliance/node/redact-pii["dag:gdpr-compliance/node/redact-pii"]
urn_noocodec_dag_gdpr-compliance/node/redact-pii -->|ok| urn_noocodec_dag_gdpr-compliance/node/compliant
urn_noocodec_dag_gdpr-compliance/node/redact-pii -->|violation| urn_noocodec_dag_gdpr-compliance/node/violation
urn_noocodec_dag_gdpr-compliance/node/compliant((("dag:gdpr-compliance/node/compliant")))
urn_noocodec_dag_gdpr-compliance/node/violation>"dag:gdpr-compliance/node/violation"]Run
npm run docs:devWhat It Lets You Do
Terminal nodes let applications name every graph endpoint and attach an explicit lifecycle outcome to it. They belong on completed, failed, rejected, or declined paths that must be visible in the DAG instead of implied by "no next node."
For application builders, terminals make support escalation, policy rejection, compliance failure, and normal completion all observable without special-case code at the runner boundary.
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.
Dispatcher: shared completed terminal
const setup = new PlaceholderNode<DispatcherState, 'ready'>('urn:noocodec:node:dispatcher-setup', ['ready']);
const classifyMessage = new PlaceholderNode<DispatcherState, 'routine' | 'escalate' | 'off-topic'>('urn:noocodec:node:classify-message', ['routine', 'escalate', 'off-topic']);
const aiCompose = new PlaceholderNode<DispatcherState, 'drafted'>('urn:noocodec:node:ai-compose', ['drafted']);
const parkForOperator = new PlaceholderNode<DispatcherState, 'parked' | 'ready'>('urn:noocodec:node:park-for-operator', ['parked', 'ready']);
const sendResponse = new PlaceholderNode<DispatcherState, 'sent'>('urn:noocodec:node:send-response', ['sent']);
const decline = new PlaceholderNode<DispatcherState, 'declined'>('urn:noocodec:node:decline', ['declined']);
const supportDispatcherDagIri = 'urn:noocodec:dag:support-dispatcher' as const;
const placement = (placementIdentifier: string): string =>
DAGIdentity.placementId(supportDispatcherDagIri, placementIdentifier);
export const supportDispatcherDAG: DAGType = new DAGBuilder(supportDispatcherDagIri, '1')
// Pre-phase: stamps runId before the entrypoint runs.
.phase(placement('setup'), 'pre', setup)
// Entrypoint: classify the inbound message.
.node(placement('classify-message'), classifyMessage, {
'routine': placement('ai-compose'),
'escalate': placement('park-for-operator'),
'off-topic': placement('decline'),
})
// Routine branch: AI composes a canned reply -> send -> done.
.node(placement('ai-compose'), aiCompose, {
'drafted': placement('send-response'),
})
// Escalation branch: HITL suspension point.
.node(placement('park-for-operator'), parkForOperator, {
'parked': placement('end'),
'ready': placement('send-response'),
})
// Shared convergence: both routine and escalated paths flow through send-response.
.node(placement('send-response'), sendResponse, {
'sent': placement('end'),
})
// Off-topic branch: decline and close.
.node(placement('decline'), decline, {
'declined': placement('end'),
})
.terminal(placement('end'), { 'outcome': 'completed' })
.build();The Dispatcher has one end terminal. Routine AI replies, operator-handled escalations, and off-topic declines all converge there. The terminal declares the lifecycle outcome; the nodes only route.
Cartographer: completed and failed child terminals
import { consentGate, classifyPii, redactPii } from '../nodes/gdprNodes.ts';
import { CARTOGRAPHER_IRIS } from '../cartographerIds.ts';
import type { CartographerState } from '../CartographerState.ts';
import type { DAGType, DispatcherBundleType } from '@studnicky/dagonizer';
import { DAGBuilder } from '@studnicky/dagonizer';
const GDPR_COMPLIANCE_DAG_IRI = CARTOGRAPHER_IRIS.dag.gdprCompliance;
export const gdprComplianceDAG: DAGType = new DAGBuilder(GDPR_COMPLIANCE_DAG_IRI, '1.0')
// ── 1. consent-gate ──────────────────────────────────────────────────────
// Resolves the consent status from marketingConsent + simulated expiry.
// Always routes 'classify' (both consented and non-consented proceed;
// the consent status drives redaction rules downstream).
.node(CARTOGRAPHER_IRIS.placementIri(GDPR_COMPLIANCE_DAG_IRI, 'consent-gate'), consentGate, {
'classify': CARTOGRAPHER_IRIS.placementIri(GDPR_COMPLIANCE_DAG_IRI, 'classify-pii'),
})
// ── 2. classify-pii ──────────────────────────────────────────────────────
// Records which fields are personal/sensitive; no routing decision yet.
.node(CARTOGRAPHER_IRIS.placementIri(GDPR_COMPLIANCE_DAG_IRI, 'classify-pii'), classifyPii, {
'redact': CARTOGRAPHER_IRIS.placementIri(GDPR_COMPLIANCE_DAG_IRI, 'redact-pii'),
})
// ── 3. redact-pii ────────────────────────────────────────────────────────
// Applies GdprRedactor.redact. Routes to 'compliant' (ok) or 'violation'.
.node(CARTOGRAPHER_IRIS.placementIri(GDPR_COMPLIANCE_DAG_IRI, 'redact-pii'), redactPii, {
'ok': CARTOGRAPHER_IRIS.placementIri(GDPR_COMPLIANCE_DAG_IRI, 'compliant'),
'violation': CARTOGRAPHER_IRIS.placementIri(GDPR_COMPLIANCE_DAG_IRI, 'violation'),
})
// ── Terminals ─────────────────────────────────────────────────────────────
.terminal(CARTOGRAPHER_IRIS.placementIri(GDPR_COMPLIANCE_DAG_IRI, 'compliant'), { outcome: 'completed' })
.terminal(CARTOGRAPHER_IRIS.placementIri(GDPR_COMPLIANCE_DAG_IRI, 'violation'), { outcome: 'failed' })
.build();The GDPR sub-DAG owns its internal terminal semantics: compliant means the child lifecycle completes, while violation marks the child as failed. Any parent that embeds this DAG receives success or error from that lifecycle outcome.
Parent routing from embedded outcome
/**
* event-pipeline-typed: the live enrichment scatter body for process-events.
*
* Reads the scattered CanonicalEventVariant from metadata key 'canonical-event'
* and routes it to one of five per-type embedded DAGs via route-event-type-variant.
* Each per-type DAG starts with parse-variant (which also reads from metadata),
* embeds geo-pipeline for geo resolution, runs type-specific enrichment nodes,
* and converges on aggregate-event → done.
*
* route-event-type-variant
* ├─position-ping──────────► pipeline-position-ping (embedded)
* ├─sensor-reading─────────► pipeline-sensor-reading (embedded)
* ├─customs-event──────────► pipeline-customs-event (embedded)
* ├─facility-scan──────────► pipeline-facility-scan (embedded)
* └─delivery-confirmation──► pipeline-delivery-confirmation (embedded)
* Each per-type DAG:
* parse-variant → geo-pipeline → canonicalize-core → [type-specific] → aggregate-event → done
*
* Metadata propagation: the scatter sets 'canonical-event' on each clone's
* metadata. NodeStateBase.clone() copies _metadata, so metadata propagates
* to embedded child clones. Both route-event-type-variant and parse-variant
* read 'canonical-event' from metadata.
*/
export const eventPipelineTypedDAG: DAGType = new DAGBuilder(EVENT_PIPELINE_TYPED_DAG_IRI, '1.0')
// 1. route-event-type-variant: read eventType from 'canonical-event' metadata
// and dispatch to the corresponding per-type sub-DAG.
.node(CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'route-event-type-variant'), routeEventType, {
'position-ping': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'pipeline-position-ping'),
'sensor-reading': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'pipeline-sensor-reading'),
'customs-event': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'pipeline-customs-event'),
'facility-scan': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'pipeline-facility-scan'),
'delivery-confirmation': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'pipeline-delivery-confirmation'),
})
// 2a. pipeline-position-ping: geo + leg measurement.
.embed<CartographerState, CartographerState>(CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'pipeline-position-ping'), CARTOGRAPHER_IRIS.dag.pipelinePositionPing, {
'success': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'done'),
'error': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'rejected'),
}, {
'outputs': {
'canonicalVariant': 'canonicalVariant',
'raw': 'raw',
'normalized': 'normalized',
'currentEvent': 'currentEvent',
'geoContext': 'geoContext',
'resolvedGeo': 'resolvedGeo',
'legKm': 'legKm',
'routing': 'routing',
'enriched': 'enriched',
'capturedErrors': 'capturedErrors',
},
})
// 2b. pipeline-sensor-reading: geo + cold-chain + leg measurement.
.embed<CartographerState, CartographerState>(CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'pipeline-sensor-reading'), CARTOGRAPHER_IRIS.dag.pipelineSensorReading, {
'success': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'done'),
'error': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_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',
},
})
// 2c. pipeline-customs-event: geo + customs-dwell + leg measurement.
.embed<CartographerState, CartographerState>(CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'pipeline-customs-event'), CARTOGRAPHER_IRIS.dag.pipelineCustomsEvent, {
'success': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'done'),
'error': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_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',
},
})
// 2d. pipeline-facility-scan: geo + facility canonicalization + order enrichment
// + GDPR-gated redaction.
.embed<CartographerState, CartographerState>(CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'pipeline-facility-scan'), CARTOGRAPHER_IRIS.dag.pipelineFacilityScan, {
'success': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'done'),
'error': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_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',
},
})
// 2e. pipeline-delivery-confirmation: geo + recipient canonicalization +
// delivery confirmation + GDPR-gated redaction.
.embed<CartographerState, CartographerState>(CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'pipeline-delivery-confirmation'), CARTOGRAPHER_IRIS.dag.pipelineDeliveryConfirmation, {
'success': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'done'),
'error': CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_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(EVENT_PIPELINE_TYPED_DAG_IRI, 'done'), { outcome: 'completed' })
.terminal(CARTOGRAPHER_IRIS.placementIri(EVENT_PIPELINE_TYPED_DAG_IRI, 'rejected'), { outcome: 'failed' })
.build();Each per-event pipeline routes an embedded child DAG's success to done and error to rejected; rejected is a failed terminal. The parent does not inspect child internals.
Details for Nerds
- Shared completed terminal. The Dispatcher routes routine, escalated, and off-topic support paths to one
endterminal withoutcome: 'completed'. - Explicit failed terminal. The Cartographer
gdpr-compliancesub-DAG routesredact-piito eithercompliant(completed) orviolation(failed). - Embedded-DAG routing to named terminals. Cartographer parent DAGs place
gdpr-compliancewith.embed(...); the child terminal outcome becomes the parent placement'ssuccessorerrorroute.
Related Concepts
- DAGBuilder,
.terminal()- full method reference and signature - Visualization - render DAGs with TerminalNode endpoints as Mermaid
- Example 05: Embedded DAGs - scatter routing with named terminal targets