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

SHIPPER_RELEASED

A shipper has moved to a picking-ready state.

RECEIVER_EXPECTED

A receiver has been created from a PO or ASN.

WORKORDER_RELEASED

A work order is released for execution.

TRANSFER_OUT_RELEASED

A transfer order is confirmed at the source warehouse.

TRANSFER_IN_EXPECTED

A transfer order has entered in-transit toward this warehouse.

VASORDER_RELEASED

A value-added-services order is released.

REFURBISH_ORDER_RELEASED

A refurbish work order is released.

CANCELLED

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 /dispatch/pending + /dispatch/ack.

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 cancel_outcome

All bindings ACCEPTED (every Task was pre-custody and accepted the cancel)

ACCEPTED

All bindings TOO_LATE (every Task was past custody at cancel time)

TOO_LATE

Mixed — some bindings ACCEPTED, some TOO_LATE

ACCEPTED_PARTIAL

Any binding UNKNOWN (executor cannot determine state)

UNKNOWN (most-conservative aggregation)

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.