Dispatch — planner to executor¶
Dispatch carries work-release events the moment a document in the planner transitions into an executable state. The executor receives them, expands them into floor tasks, and assigns workers, robots, or equipment.
Event kinds¶
Kind |
Trigger in the planner |
|---|---|
|
A shipper has moved to a picking-ready state. |
|
A receiver has been created from a PO or ASN. |
|
A work order is released for execution. |
|
A transfer order is confirmed at the source warehouse. |
|
A transfer order has entered in-transit toward this warehouse. |
|
A value-added-services order is released. |
|
A refurbish work order is released. |
|
A previously released document is cancelled, amended, or reprioritized. |
The dispatch payload carries the full Routing recipe inline — ordered Operations with expected time/cost, BOM deltas per Op, and skill / resource requirements. The executor does not call back to the planner at runtime to fetch the recipe. See WMS ADR-0022 amendment 2026-05-24 and ADR-0028 for the rationale.
Payload size is well under 10 KB for typical v1 Routings (3–8 Operations). Complex Routings with 50+ Operations approach 100 KB; both transports below tolerate that.
Choosing a transport¶
The planner-executor pair picks one transport per warehouse. Mixing transports for the same warehouse is not supported.
Transport |
When to choose |
Cost on the implementer |
|---|---|---|
Webhook POST |
Default. The planner POSTs each event to a URL the executor registers. |
Low. Outbound HTTPS + HMAC over body. |
Kafka subscription |
Both sides reach the same Kafka cluster and want ordered, high-throughput streaming. |
Medium. Shared Kafka access + topic ownership decision. |
Polling |
The executor cannot accept inbound, or the planner cannot publish to a shared Kafka. The executor pulls from a planner-hosted queue. |
Medium. Planner exposes |
For most third-party integrations, webhook is the recommended default. Kafka becomes attractive when both sides co-locate in shared cloud infrastructure. Polling exists for restricted networks that only permit outbound HTTPS from one side.
Ordering¶
Order is preserved per warehouse, not globally.
Kafka — the partition key is
warehouse_frn. Events for the same warehouse land in the same partition and are consumed in publish order.Webhook — delivery is serialized per
(planner_id, warehouse_id)pair. The next event for a warehouse is not sent until the previous one is acknowledged or finally fails to the DLQ.Polling — the planner returns events ordered by an internal cursor. The executor must process pages strictly in order and acknowledge only after durable processing.
Cross-warehouse ordering is not guaranteed and should not be relied on. If a planner releases two shippers in the same wall-clock millisecond into different warehouses, executors may see them in either order.
Delivery semantics¶
At-least-once. A planner must assume any single delivery attempt may fail and be retried. An executor must therefore be idempotent on (planner_id, correlation_id) — see Idempotency and ordering.
Webhook retry policy (typical):
0s → 5s → 30s → 2m → 10m → 1h → ... → 24h cap → DLQ
The planner DLQs an event after 24h of failed retries; supervisors are expected to be alerted and to triage from there.
Kafka and polling rely on the consumer’s own commit / acknowledgement semantics — the planner re-delivers everything past the last committed cursor.
Cancellation, amendment, reprioritization¶
These three operations all surface as a CANCELLED event referencing the original correlation_id. The semantics are deliberately collapsed into one kind to keep the executor’s reaction simple: stop the document if active, drop pending tasks if not yet started, and wait for a fresh release if applicable.
Amendments and reprioritizations land as a CANCELLED followed by a fresh release event (SHIPPER_RELEASED, WORKORDER_RELEASED, …) with a new correlation_id. The executor must not attempt to mutate in-flight work — only fresh releases get new tasks.
Webhook payload shape¶
{
"kind": "SHIPPER_RELEASED",
"correlation_id": "01J7Y6K1NQ3W2C0X4V0R5T6E7N",
"planner_id": "fgai-wms",
"warehouse_id": "WH-Tokyo-01",
"document_ref": { "type": "SHIPPER", "id": "SH-2026-000183" },
"routing": {
"ops": [
{ "op_id": "op-1", "kind": "PICK", "sku": "SKU-WIDGET-RED-LG", "qty": 12, "from_location": "A.12.3.1" },
{ "op_id": "op-2", "kind": "PACK", "carton": "CTN-S" },
{ "op_id": "op-3", "kind": "SHIP", "dock": "DOCK-3" }
],
"expected_duration_seconds": 420
},
"meta": {
"released_at": "2026-05-24T03:14:01Z",
"priority": "NORMAL"
}
}
correlation_id is the partner-generated idempotency key — UUID v4 or v7. The full payload schema lives in the OpenAPI reference; per-event-kind schemas vary in the routing and meta blocks but the envelope is constant.
Label bindings on the dispatch envelope (v1.3+)¶
Every *_RELEASED envelope (Shipper, Receiver, WorkOrder, Transfer-Out, Transfer-In) gains an optional label_bindings[] array as of OpenAPI v1.3, per WES ADR-0022. Present when the planner pre-prints labels at dispatch (Patterns A / B / C); absent or empty for post-print at PackStation (Patterns D / E) or external/carrier-provided labels (Pattern H).
"label_bindings": [
{
"label_id": "1Z999AA10123456784",
"external_label_uri": "https://carrier.example/track/1Z999AA10123456784",
"bound_to_type": "shipper",
"bound_to_id": "SH-2026-000183",
"print_mode": "pre-pick"
}
]
print_mode enum: pre-pick | post-pick | external | at-cartonization (v1.4, per WES ADR-0023 — cartonizer-with-label sub-pattern).
bound_to_type enum: shipper | container | tote | slot | work_order_output | cluster | external.
See the Label printing guide for the 9 patterns and end-to-end flow examples; see the Advanced cartonization guide for the at-cartonization sub-pattern.
dispatch.label-issued.v1 (v1.3+)¶
A new dispatch-class event closes the post-print round trip (Patterns D / E and the cartonizer-with-label sub-pattern). After the executor emits label_print_requested on the realtime channel, the planner replies via the synchronous POST /wes/v1/labels/issue endpoint and then publishes dispatch.label-issued.v1 with the resulting label_bindings[]. The executor treats it as an upsert of print_state=printed against existing binding rows.
{
"kind": "DISPATCH_LABEL_ISSUED",
"correlation_id": "01J7Y6K2NQ3W2C0X4V0R5T6E7M",
"planner_id": "fgai-wms",
"warehouse_id": "WH-Tokyo-01",
"in_response_to_label_print_request_uuid": "01J7Y6K1NQ3W2C0X4V0R5T6E7Q",
"label_bindings": [ ... ]
}
Webhook recipients must verify the X-FGAI-Signature: sha256=<hex> header over the raw request body before parsing — see Authentication.
Poll-mode shape¶
GET /wes/v1/dispatch/pending?since={cursor}&limit=100
POST /wes/v1/dispatch/ack { "cursor": "..." }
The planner returns an ordered page of dispatch events and a next_cursor. The executor processes them durably, then POSTs the cursor to /dispatch/ack. The next GET /dispatch/pending?since={cursor} advances past the acked events.
Without an ack, the planner re-delivers the same page on the next pull. Pages are bounded in size; pull again immediately if next_cursor is non-empty.
Cancellation Outcome — Order-Level Aggregation¶
Per wes-contract ADR-0002 §3 + amendment 2026-05-28.
When the executor acknowledges a dispatch.cancelled.v1 event, the response carries a cancel_outcome enum plus binding-level breakdown arrays (custody_confirmed_for[], compensation_initiated_for[]). Because an Order can be bound to multiple Tasks (N:1 cardinality per WES ADR-0013 §1), the Order-level outcome is an aggregation across all bound Tasks.
Aggregation rule¶
Bound-Task states at cancel time |
Order-level |
|---|---|
All bindings ACCEPTED (every Task was pre-custody and accepted the cancel) |
|
All bindings TOO_LATE (every Task was past custody at cancel time) |
|
Mixed — some bindings ACCEPTED, some TOO_LATE |
|
Any binding UNKNOWN (executor cannot determine state) |
|
The UNKNOWN rule is intentionally conservative: a single binding the executor cannot evaluate poisons the Order-level outcome, surfacing the case for supervisor reconciliation rather than reporting partial success that the planner might trust.
Sub-line binary rule¶
Within a single order_line_no, custody confirmation is binary. An order_line_no appears in custody_confirmed_for[] ONLY when the ENTIRE quantity of that line has crossed the Scheme’s custody primitive. Sub-line partial physical confirmation (for example, an operator has voice-checked 1 of 3 SKU-99 from a split-bin sub-pick WorkItem before the cancellation arrives) routes the whole line through compensation_initiated_for[].
Rationale (per ADR-0002 §3 amendment 2026-05-28):
WorkItem IDs are WES-internal and not meaningful to the planner; the wire shape must not leak them.
The planner reasons about Order line items, not sub-line execution micro-state.
Any sub-line physical inventory is recoverable via the compensation flow’s RecoveryStationArtifact disposition, so collapsing partial-line state to compensation widens the recovery surface in a planner-meaningful way.
This rule is uniform across all Schemes (Shipper, Receiver, WorkOrder, TransferOut, TransferIn) and is not per-Scheme-overridable.
Consequence: there is no “partially confirmed line” representation in the wire payload. A line either qualifies entirely for custody_confirmed_for[] or it routes entirely through compensation_initiated_for[].
Operational walkthrough¶
See the Cancellation guide for an end-to-end walkthrough — how the executor evaluates per-binding state, populates the two arrays, and signals the Order-level outcome the supervisor UI consumes.