Skip to main content

Dispatch Nodes

A Dispatch node is a stage on a Spec's pipeline rail that decides when and what a run delivers. It runs no extraction work of its own. When it triggers, it freezes a selection of rows and a field projection onto a dispatch record and publishes a dispatch.<grain>.requested signal. Delivery bindings that target the node pick that signal up and ship the payload through their destination, serializer, and field map exactly as any other binding does. Dispatch nodes are the primary way to deliver Spec results: the node decides the timing and scope, and the bindings decide where and how the data goes.

A binding targets one specific node. Its signal_filter.event_type is the node's signal (for example dispatch.row.requested), and its signal_filter.match must name both the Spec (spec_id) and the node (stage_id). The platform rejects a dispatch.* binding without both with a 400, because a node id alone is not unique across a workspace and a filter without them would receive every Dispatch node's events. One node can carry several bindings, for example a JSON webhook for an operational system and a CSV file drop for an archive, and each binding keeps its own serializer, field map, and retry policy.

Grains

The grain sets what one dispatched record is and fixes the signal name. The compiler checks the grain against the rail, so a grain that needs a stage the Spec does not have is reported as a diagnostic before publishing rather than failing at run time.

Dispatch grains

ParameterTypeDescription
rowdispatch.row.requestedOne record per row. Rows come from documents by default; product rows need an Assembly stage on the rail.
pipelinedispatch.pipeline.requestedOne payload covering the pipeline's rows as a whole.
assemblydispatch.assembly.requestedThe assembled (composed) rows. Requires an Assembly stage.
matcherdispatch.matcher.requestedMatcher results and verdicts. Requires a reconcile (matching) stage.
data_productdispatch.data_product.requestedThe pipeline's data product. Requires the deliver stage.

Trigger modes

The mode decides when the node fires. Without a mode a node is manual. on_run_completed reads the rail frozen when the pipeline was created; the early modes read the Spec's published version, so they react to per-document progress instead of waiting for the whole run.

Trigger modes

ParameterTypeDescription
manualdefaultFires only when someone triggers it from the app or the API.
on_run_completedany grainFires once each time the pipeline run completes.
on_stage_completedrow grainFires per document the moment the anchor stage (anchorStageId, a per-document stage such as extraction, resolve, or validation) finishes for it.
on_request_extractedrow grainAnchors like on_stage_completed but fires once per POST /v1/run request, after every document of the request has cleared the anchor or a deadline has passed. A document that did not arrive through a run request keeps the per-document behaviour.
on_matcher_verdictmatcher grainFires per document whenever the matcher verdict for it advances; a revised verdict fires again. on_matcher_done is a deprecated alias that is accepted on stored Specs and behaves the same way.
on_app_verdictmatcher grainFires per document when an App seals a decision for it. A newer verdict supersedes older dispatches for the same document that have not shipped yet.

A node also carries a selection and a projection. The selection scope is all or changed_since (relative to the last_dispatch or the run_start), and rows come from documents or from the product. The projection can limit the payload to chosen fields and, with changedFieldsOnly, to the fields that changed. Use changed_since with last_dispatch for incremental feeds that should only ship what is new since the previous delivery.

Frozen selection and idempotency

When a node triggers, the dispatch record freezes what the dispatch meant: the selected document or product-row ids, the time window, the projected fields, any per-request destination overrides, and a cutoff time. For the per-document early modes the cutoff comes from the database clock at trigger time. Every attempt, chunk, and re-dispatch resolves the same payload from that frozen record, so a retry three hours later ships the rows that were selected at trigger time, not whatever the table holds by then.

Triggers are idempotent. Each automatic trigger derives a key from the pipeline, the node, and the occasion (the run completion, the document, the run request, or the verdict version), and the key is unique per pipeline and node, so a duplicate trigger returns the existing dispatch instead of creating a second one. A manual trigger accepts an optional Idempotency-Key header; repeating it returns 200 with existing: true. Below the dispatch, every delivery attempt still carries the binding-level X-Talonic-Idempotency-Key header for receivers to deduplicate on.

Trigger a manual Dispatch node and list its dispatches
# Fire a node by hand. document_ids narrows the node's own selection
# (intersected, never unioned). Repeat with the same key -> existing: true.
curl -X POST https://api.talonic.com/v1/pipelines/PIPELINE_ID/dispatches/NODE_ID \
  -H "Authorization: Bearer $TALONIC_API_KEY" \
  -H "Idempotency-Key: nightly-2026-10-06" \
  -H "Content-Type: application/json" \
  -d '{ "document_ids": ["c3d4e5f6-a7b8-9012-cdef-123456789012"] }'

# History for one node (default 50 rows):
curl -s "https://api.talonic.com/v1/pipelines/PIPELINE_ID/dispatches?stage_id=NODE_ID" \
  -H "Authorization: Bearer $TALONIC_API_KEY"
Manual trigger response
{
  "dispatch_id": "7d1f0c2e-5b8a-4c3e-9f21-0a6b4d8e2c11",
  "grain": "row",
  "status": "published",
  "record_count": 1,
  "event_count": 1,
  "existing": false
}

Statuses, history and re-dispatch

Dispatch statuses

ParameterTypeDescription
pendingstatusCreated; the selection is frozen and events are being published.
publishedstatusEvents were published to the node's bindings.
emptystatusThe selection matched no rows, so nothing was published.
failedstatusPublishing failed. Re-dispatching repairs the row in place.
supersededstatusA newer dispatch for the same subject replaced this one before it shipped.
skippedstatusA run request told this node not to deliver (dispatch=false or a null binding override). Terminal and not re-dispatchable.

Each pipeline keeps the dispatch history of its nodes: GET /v1/pipelines/{id}/dispatches lists dispatches (filter by stage_id), and each row includes per-binding attempt counts. Re-dispatch (POST /v1/pipelines/{id}/dispatches/{dispatchId}/redispatch) replays a dispatch with the same frozen selection, projection, and cutoff. A failed or stale pending dispatch is repaired in place; any other status creates a new manual dispatch that records replay_of. A skipped dispatch answers 409 DISPATCH_SKIPPED_BY_REQUEST: the request chose not to deliver, so trigger the node manually instead. Manual triggers never read per-request controls.

A single run request can change what its Dispatch nodes do without editing the Spec. POST /v1/run accepts dispatch=false to skip every node for that request, and bindings to re-point a node's bindings at another destination or to skip one node. Skipped nodes still record a skipped dispatch so the history shows the decision. See Run Delivery Controls in the API reference (/v1/run) for the rules, warnings, and failure behaviour.

A dispatch freezes its selection at trigger time. If you correct data after a node fired, re-dispatching ships the original selection again; to ship the corrected rows, trigger the node manually (or let the next automatic trigger fire) so a new selection is frozen.

Frequently asked questions

What is a Dispatch node?+
A stage on a Spec's rail that decides when and what a run delivers. When it fires it freezes a selection of rows onto a dispatch record and publishes a dispatch.<grain>.requested signal that the node's bindings deliver to their destinations.
Why does my dispatch.* binding fail with a 400?+
A binding on a dispatch.* signal must name the node it targets with signal_filter.match.spec_id and signal_filter.match.stage_id. Without both it would receive every Dispatch node's events, so the platform refuses it.
Which trigger mode should I use?+
Use on_run_completed for a single delivery when the whole run is done, on_stage_completed to ship each document as soon as it clears a stage, on_request_extracted to ship once per /v1/run request, and manual for operator-driven exports. Matcher-grain nodes use on_matcher_verdict or on_app_verdict.
Can I re-dispatch a skipped dispatch?+
No. A skipped dispatch is terminal and re-dispatching it returns 409 DISPATCH_SKIPPED_BY_REQUEST. Trigger the node manually with POST /v1/pipelines/{id}/dispatches/{stageId}; manual triggers ignore per-request delivery controls.
Will a duplicate trigger deliver twice?+
No. Automatic triggers derive a key per pipeline, node, and occasion, and a manual trigger can pass an Idempotency-Key header; a repeat returns the existing dispatch. Delivery attempts also carry X-Talonic-Idempotency-Key for receiver-side deduplication.