Cancellation guide

This guide walks planner-side developers through cancelling work after it has been dispatched. It covers the contract surface, the four outcomes the executor returns, what happens physically on the executor side, and how reconciliation closes the loop.

The patterns here apply equally to FG.AI WES (the reference executor) and any other conforming WES vendor.

TL;DR

  1. To cancel a dispatched Order, POST dispatch.cancelled.v1 to the executor with the original order_id.

  2. The executor returns one of four outcomes synchronously: ACCEPTED, ACCEPTED_PARTIAL, TOO_LATE, or UNKNOWN.

  3. Outcomes are computed per-Task-binding and aggregated to the Order level. Tasks past custody confirmation at the receiving party cannot be cancelled by the executor — they’re reported as TOO_LATE for those bindings, and any downstream reversal is your responsibility.

  4. For ACCEPTED and ACCEPTED_PARTIAL outcomes, compensation runs asynchronously at the executor side — the realtime stream carries movement events with compensation: true until terminal state.

  5. Reconciliation closes the loop at window-close: cancelled Orders should net to zero inventory delta on the executor side (modulo TOO_LATE bindings, which require your downstream handling).

When cancellation is in scope

Send dispatch.cancelled.v1 when:

  • A customer cancels a sales order upstream of your planner

  • A stockout is discovered post-release that prevents fulfillment

  • A re-route to a different fulfillment site requires recalling the existing dispatch

  • A WorkOrder is no longer needed (product reformulated, customer decisions, etc.)

  • A misconfigured release needs to be rolled back

Don’t use cancellation to amend an Order. v1 of the contract has no dispatch.amended.v1 event (deferred to v1.x). To change an in-flight Order’s contents, cancel and re-release with a new event_uuid.

The four cancel outcomes

The dispatch.cancelled.v1 ack returns a structured cancel_outcome (see openapi-reference and errors for the JSON schema):

Outcome

Meaning

Your action

ACCEPTED

All bound work was pre-custody; cancellation applied cleanly; any in-flight inventory entered compensation flow

Mark Order CANCELLED upstream. Watch the realtime stream for compensation movements until terminal. Reconciliation expects net inventory delta zero.

ACCEPTED_PARTIAL

Some bound Tasks accepted cancel (still pre-custody); others past custody and beyond AMR-side compensation

Mark non-custody Orders/lines CANCELLED. For each entry in custody_confirmed_for, you own the downstream reversal (refund/return-merchandise/etc.).

TOO_LATE

All bound Tasks past custody confirmation

Cannot be cancelled at the executor. You own the entire downstream reversal. The Order on the executor side stays at its pre-cancel terminal state (typically COMPLETED).

UNKNOWN

Executor cannot determine state (audit gap, persistence error, etc.)

Surface to your supervisor as a reconciliation discrepancy. Coordinate with the executor team to investigate.

The ack payload also carries two arrays — custody_confirmed_for (populated for ACCEPTED_PARTIAL and TOO_LATE) and compensation_initiated_for (populated for ACCEPTED and ACCEPTED_PARTIAL) — that let you reconcile at the binding level without re-querying executor state.

Aggregation rule

For Orders with multiple bound Tasks, the Order-level outcome aggregates per-binding outcomes:

  • All bindings ACCEPTED → Order outcome ACCEPTED

  • All bindings TOO_LATE → Order outcome TOO_LATE

  • Mixed accepted/too-late → Order outcome ACCEPTED_PARTIAL

  • Any UNKNOWN binding → Order outcome UNKNOWN (most-conservative aggregation)

Custody confirmation — when “too late” kicks in

The executor cannot reverse work after the receiving party has confirmed custody. Different document types have different custody-confirmation points:

Document type

Custody confirmation event

Shipper

PackStation (or equivalent) scans inventory into outbound staging

WorkOrder

Output (labeled cases, kitted boxes, repacked containers) reaches declared destination location

Receiver

Inbound goods scanned into destination zone bin

TransferOut

Inventory handed to carrier for transit (scan or BoL signature equivalent)

TransferIn

Dock-receipt scan at destination Site

Cancellations arriving before the custody event compensate cleanly. Cancellations arriving after return TOO_LATE for that binding.

WIP at intermediate workstations (mid-pick, mid-label, mid-kit) is NOT past custody — those Tasks can still be cancelled with compensation flow.

Sequence example — Shipper cancellation

Scenario. Manhattan-WMOS sends a Shipper for 5 lines to Customer-X. Mid-execution, the customer cancels their sales order. Manhattan-WMOS sends dispatch.cancelled.v1 to the WES executor.

Executor-side state at cancel arrival:

Task

Bound to

State

Physical reality

Task-1A

line 1

COMPLETED

At PackStation-3 (custody confirmed)

Task-1B

line 2

COMPLETED

At PackStation-3 (custody confirmed)

Task-2

line 3

EXECUTING

Item loaded on AMR; AMR in transit

Task-3

line 4

EXECUTING

Picker mid-pick

Task-4

line 5

DISPATCHED

Not yet claimed

Request

POST /wes/v1/dispatch/cancelled HTTP/1.1
Host: api.flexgalaxy.com
Content-Type: application/json
X-Cert-Thumbprint: <mTLS-thumbprint>

{
  "event_uuid": "M-cancel-7321",
  "planner_id": "manhattan-wmos-tenant-abc",
  "order_id": "frn:wes:tokyo-01:order/Order-1",
  "site_id": "frn:wes:tokyo-01",
  "cancelled_at": "2026-05-28T14:25:00Z",
  "reason": "customer-cancelled-sales-order"
}

Response (synchronous; returned in seconds)

HTTP/1.1 200 OK
Content-Type: application/json

{
  "cancel_outcome": "ACCEPTED_PARTIAL",
  "custody_confirmed_for": [
    {
      "task_id": "frn:wes:tokyo-01:task/Task-1A",
      "order_id": "frn:wes:tokyo-01:order/Order-1",
      "order_line_no": 1,
      "custody_confirmed_at": "2026-05-28T14:23:11Z",
      "custody_confirmer": "frn:wes:tokyo-01:packstation/PackStation-3"
    },
    {
      "task_id": "frn:wes:tokyo-01:task/Task-1B",
      "order_id": "frn:wes:tokyo-01:order/Order-1",
      "order_line_no": 2,
      "custody_confirmed_at": "2026-05-28T14:23:14Z",
      "custody_confirmer": "frn:wes:tokyo-01:packstation/PackStation-3"
    }
  ],
  "compensation_initiated_for": [
    {
      "task_id": "frn:wes:tokyo-01:task/Task-2",
      "order_id": "frn:wes:tokyo-01:order/Order-1",
      "compensation_task_id": "frn:wes:tokyo-01:task/CompTask-2"
    },
    {
      "task_id": "frn:wes:tokyo-01:task/Task-3",
      "order_id": "frn:wes:tokyo-01:order/Order-1",
      "compensation_task_id": "frn:wes:tokyo-01:task/CompTask-3"
    }
  ]
}

Planner-side actions

  1. Lines 1 & 2 (TOO_LATE): Surface to your supervisor as “Order-1 cancelled, but lines 1 & 2 already at PackStation — downstream reversal needed.” Initiate your return-merchandise / refund / re-route flow for the already-shipped lines.

  2. Lines 3 & 4 (compensation initiated): Mark these lines as CANCELLED on your side. Watch the realtime stream — you’ll see compensation: true movements as items return to source.

  3. Line 5 (clean ACCEPTED): Mark as CANCELLED; no further action.

What you’ll see on the realtime stream

Over the following minutes, expect these events:

wes.movement.recorded.v1
  task_id: Task-2, order_id: Order-1, sku: SKU-X, qty: 1
  from: AMR-R7, to: RecoveryStation-7
  compensation: true
  compensation_for_task_id: Task-2
  recorded_at: 2026-05-28T14:30:18Z

wes.compensation.routed.v1
  task_id: Task-2, recovery_station_id: RecoveryStation-7
  recorded_at: 2026-05-28T14:30:45Z

wes.compensation.resolved.v1
  task_id: Task-2, resolution: return-to-source
  destination: frn:wes:tokyo-01:location/loc-3
  recorded_at: 2026-05-28T14:34:12Z

wes.movement.recorded.v1
  task_id: CompTask-2, order_id: Order-1, sku: SKU-X, qty: 1
  from: RecoveryStation-7, to: loc-3
  compensation: true
  compensation_for_task_id: Task-2
  recorded_at: 2026-05-28T14:48:30Z

wes.task.state-changed.v1
  task_id: Task-2, state: CANCELLED_COMPENSATED
  recorded_at: 2026-05-28T14:48:35Z

Same pattern for Task-3 if a partial pick occurred (otherwise Task-3 → CANCELLED cleanly).

Confirmation summary at window-close

At the end of the reconciliation window (per-warehouse-configurable; default 24h), the executor posts a wes.confirmation.shift-summary.v1. For Order-1 expect:

  • Forward movements for lines 3, 4 (compensation: false) — the original picks

  • Reverse movements for lines 3, 4 (compensation: true) — the returns

  • Forward movements for lines 1, 2 (compensation: false) — the shipped lines (TOO_LATE; still at PackStation)

Net inventory delta at the source bins for lines 3, 4: zero. Net at PackStation for lines 1, 2: +1 each. Your reconciliation should match.

Sequence example — WorkOrder cancellation

Scenario. Manhattan-WMOS sends a WorkOrder for VAS labeling of 100 cases. Mid-execution: 30 cases labeled (still at workstation); 5 mid-label; 65 pending. Customer cancels the upstream order; Manhattan-WMOS sends dispatch.cancelled.v1.

Key difference from Shipper

The labeled cases physically exist but are transformed inventory — the labels are consumed; you cannot un-label. “Return to source” is often impossible. The compensation resolution surface at the executor is correspondingly richer.

Request — same shape as Shipper

POST /wes/v1/dispatch/cancelled HTTP/1.1
Host: api.flexgalaxy.com
Content-Type: application/json
X-Cert-Thumbprint: <mTLS-thumbprint>

{
  "event_uuid": "M-cancel-8420",
  "planner_id": "manhattan-wmos-tenant-abc",
  "order_id": "frn:wes:tokyo-01:order/WorkOrder-2",
  "site_id": "frn:wes:tokyo-01",
  "cancelled_at": "2026-05-28T16:10:00Z",
  "reason": "upstream-order-cancelled"
}

Response

{
  "cancel_outcome": "ACCEPTED",
  "custody_confirmed_for": [],
  "compensation_initiated_for": [
    {
      "task_id": "frn:wes:tokyo-01:task/Task-WO-2",
      "order_id": "frn:wes:tokyo-01:order/WorkOrder-2",
      "compensation_task_id": "frn:wes:tokyo-01:task/CompTask-WO-2"
    }
  ]
}

ACCEPTED (not ACCEPTED_PARTIAL) because none of the labeled cases reached the declared output destination yet — they were still at the workstation, which doesn’t count as custody for WorkOrders.

How the executor handles the labeled cases

The operator at the RecoveryStation gets a richer resolution choice than Shipper:

Resolution

Typical use

return-to-source

Only if labels are generic and can be reused; rare for VAS

divert-to-order

Best fit when another in-flight Order has the same SKU + branding needs

hold-for-investigation

Default when labels are customer-specific and supervisor needs to decide

dispose

Last resort: labels can’t be removed, no eligible target Order, product perishable

The chosen resolution flows back on the realtime stream:

wes.compensation.resolved.v1
  task_id: Task-WO-2, resolution: hold-for-investigation
  recorded_at: 2026-05-28T16:25:00Z

Or:

wes.workorder.diverted.v1
  task_id: Task-WO-2
  diverted_to_order_id: frn:wes:tokyo-01:order/Order-99
  diverted_qty: 30
  recorded_at: 2026-05-28T16:28:00Z

Or:

wes.workorder.disposed.v1
  task_id: Task-WO-2
  disposed_qty: 30
  disposal_reason: "customer-specific-labels-cannot-be-reused"
  recorded_at: 2026-05-28T16:30:00Z

Planner-side considerations for WorkOrder cancellation

  • divert-to-order — the executor reassigns inventory to another Order. You may need to update your inventory state to reflect the new owner. Listen for wes.workorder.diverted.v1 and update accordingly.

  • hold-for-investigation — inventory sits in a hold area pending supervisor decision. You may or may not be involved depending on your operational model. Don’t assume the hold resolves quickly.

  • dispose — inventory is scrapped. You should expect a downstream cost-accounting entry. Don’t expect the inventory back.

For all three resolutions, the WorkOrder’s Order outcome on your side is CANCELLED — but the physical inventory disposition varies.

Sequence example — TransferOut / TransferIn cancellation (cross-Site)

Scenario. Manhattan-WMOS coordinates an internal transfer: 100 cases of SKU-X from Site A (Tokyo-01) to Site B (Osaka-02). The planner issues paired events: dispatch.transfer-out.v1 to Site A and dispatch.transfer-in.v1 to Site B, linked by transfer_twin_id. Carrier transit takes ~8 hours by truck. Mid-transit, the planner needs to cancel the transfer.

Architectural baseline

Per WES ADR-0003, each Site’s execution plane is independent. The transfer_twin_id link in task_order_binding is informational only — neither Site can mutate the other Site’s state. Coordination is purely planner-mediated: the planner sends two separate dispatch.cancelled.v1 events, one per Order, and is responsible for reconciling the two outcomes.

Carrier transit is invisible to WES. After Site A’s carrier-handoff custody event, Site A loses sight of the inventory; before Site B’s dock-receipt scan, Site B has not yet seen anything. The hours-long gap between these events is the carrier’s responsibility, not WES’s.

State at cancel arrival (the interesting case)

  • Site A — TransferOut Tasks COMPLETED; carrier-handoff scan confirmed custody; inventory in truck mid-transit

  • Site B — TransferIn Tasks DISPATCHED; carrier ETA in ~4 hours

Cancellation requests — two separate POSTs

The planner sends both:

POST /wes/v1/dispatch/cancelled  (to Site A)
{
  "event_uuid": "M-cancel-T1-A",
  "planner_id": "manhattan-wmos-tenant-abc",
  "order_id": "frn:wes:tokyo-01:order/Order-T1-out",
  "site_id": "frn:wes:tokyo-01",
  "cancelled_at": "2026-05-28T18:00:00Z",
  "reason": "destination-customer-cancelled"
}
POST /wes/v1/dispatch/cancelled  (to Site B)
{
  "event_uuid": "M-cancel-T1-B",
  "planner_id": "manhattan-wmos-tenant-abc",
  "order_id": "frn:wes:osaka-02:order/Order-T1-in",
  "site_id": "frn:wes:osaka-02",
  "cancelled_at": "2026-05-28T18:00:01Z",
  "reason": "destination-customer-cancelled"
}

Site A response (TOO_LATE — inventory in carrier custody)

{
  "cancel_outcome": "TOO_LATE",
  "custody_confirmed_for": [
    {
      "task_id": "frn:wes:tokyo-01:task/Task-T1-out-A",
      "order_id": "frn:wes:tokyo-01:order/Order-T1-out",
      "order_line_no": 1,
      "custody_confirmed_at": "2026-05-28T10:23:00Z",
      "custody_confirmer": "frn:wes:tokyo-01:carrier/yamato-7821"
    }
  ],
  "compensation_initiated_for": []
}

Site A’s books stay at COMPLETED for Order-T1-out. Inventory is at the carrier; AMR-side compensation is impossible from Site A.

Site B response (ACCEPTED with pending-inbound disposition)

Site B’s TransferInScheme recognizes the cross-Site cancellation case via the transfer_twin_id link to the paired TransferOut Order. It cleanly cancels the local Tasks (pre-claim) AND proactively spawns a pending-inbound-disposition CompensationTask to handle the future arrival.

{
  "cancel_outcome": "ACCEPTED",
  "custody_confirmed_for": [],
  "compensation_initiated_for": [
    {
      "task_id": "frn:wes:osaka-02:task/Task-T1-in-A",
      "order_id": "frn:wes:osaka-02:order/Order-T1-in",
      "compensation_task_id": "frn:wes:osaka-02:task/CompTask-T1-in-pending-disposition"
    }
  ]
}

The compensation_task is not actually doing anything yet — it’s waiting for the carrier to arrive. The Order state at Site B is CANCELLED_PENDING_RECOVERY for the carrier transit duration. This is normal, not an exception.

What happens when goods arrive at Site B

When the carrier arrives ~4 hours later, Site B’s Receiver agent dock-scans the inbound inventory. It detects the match against the cancelled Order via transfer_twin_id, activates the pending CompensationTask, and routes the inventory to a RecoveryStation.

You’ll see realtime stream events:

wes.movement.recorded.v1
  site_id: frn:wes:osaka-02
  task_id: CompTask-T1-in-pending-disposition (activated)
  sku: SKU-X, qty: 100
  from: dock-zone-1, to: RecoveryStation-2
  compensation: true
  compensation_for_task_id: Task-T1-in-A
  recorded_at: 2026-05-28T22:08:00Z

wes.compensation.routed.v1
  task_id: CompTask-T1-in-pending-disposition
  recovery_station_id: RecoveryStation-2

wes.compensation.resolved.v1
  task_id: CompTask-T1-in-pending-disposition
  resolution: divert-to-order
  diverted_to_order_id: frn:wes:osaka-02:order/Order-99
  diverted_qty: 100

wes.task.state-changed.v1
  task_id: CompTask-T1-in-pending-disposition, state: COMPLETED

wes.order.state-changed.v1
  order_id: Order-T1-in, state: CANCELLED  (terminal)

Carrier-loss case

If the carrier never arrives within the configured ETA + tolerance window (default 72 hours), the pending CompensationTask transitions to NEVER_ARRIVED and a SupervisorAgent escalation fires at Site B. You’ll see:

wes.compensation.never-arrived.v1
  task_id: CompTask-T1-in-pending-disposition
  expected_arrival_by: 2026-05-31T18:00:00Z
  escalation_to: SupervisorAgent

This case is rare but real (carrier theft, accident, misrouting). The supervisor coordinates with the carrier and the planner to reconcile.

Cross-Site reconciliation (planner-side)

The planner sees outcomes from both Sites independently:

  • Site A: TOO_LATE — Order COMPLETED on Site A’s books; 100 units shipped

  • Site B: ACCEPTED with pending disposition — Order CANCELLED_PENDING_RECOVERY → CANCELLED on Site B’s books; disposition handled at destination

The planner reconciler combines these into a single “transfer cancelled; downstream disposition at destination Site” record. Reconciliation matches when:

  • Site A reports 100 units shipped (forward)

  • Site B reports 100 units arrived (forward) + 100 units disposition-routed (compensation: true)

  • Net inventory delta across the two Sites: zero for the original SKU/Site-A-source pair; +100 units at whatever destination the disposition produced (a different Order at Site B, or general inventory, or scrapped per disposition choice)

Asymmetric cancellation (planner protocol error)

If the planner cancels only one half:

  • Site A cancel only — Site A handles per state; Site B’s TransferIn stays active expecting goods. If goods don’t arrive: Site B reconciliation eventually surfaces overdue receipt → supervisor escalation. If goods arrive partially (Site A had partial cancel): Site B receives the partial qty normally; reconciliation discrepancy.

  • Site B cancel only — Site A’s TransferOut continues; truck departs with full quantity; arrives at Site B which has CANCELLED state with proactive disposition. Disposition runs as designed.

WES handles both cases gracefully — each Site does the right thing for its half. The planner-side reconciler is responsible for detecting and resolving the asymmetry.

Connected vs disconnected variants

If the WES execution plane has been disconnected from cloud (cold-chain outage, WAN failure, planned maintenance), planner-side cancellations queue at your end until WES reconnects. On reconnect, your queued cancellations drain to WES in original order.

The cancellation flow is otherwise identical. However:

  • If the disconnect was long enough that the work completed locally at WES while your cancellation was queued, the outcome may be TOO_LATE for some or all bindings.

  • The custody_confirmed_at timestamp in the response will reflect the original custody event (during the disconnect), not the time the cancellation was processed.

  • Subsequent realtime stream events for the original Order will arrive in causal order — you may see the original forward picks completing followed by your cancellation’s outcome.

Your reconciler should handle this gracefully — TOO_LATE during a long disconnect is the normal case, not an exception.

Pitfalls and best practices

Don’t retry cancellations under different event_uuids

If your first dispatch.cancelled.v1 POST times out (network failure, retry policy), use the same event_uuid on retry. WES dedupes idempotent replays and returns the original ack. A retry under a different event_uuid for the same order_id is a protocol violation that surfaces in the executor’s last_protocol_violation state.

This pattern is robust under WES-side cloud-side disconnects too. If your cancellation request times out because the WES execution plane lost cloud connectivity mid-processing, WES still completes the cancellation locally and caches the ack envelope. Keep retrying with the same event_uuid — when WES reconnects, your retry returns the cached outcome without re-processing or duplicating compensation work. This is the chaos-during-cancellation case covered by FG.AI WES ADR-0016 sub-scenario CT-H + the transactional invariant in ADR-0017 §8.

Don’t assume ACCEPTED means physical inventory is back at source

The executor returns ACCEPTED as soon as it has accepted the cancellation and spawned compensation. Compensation work runs asynchronously and may take minutes (typical AMR transit + RecoveryStation operator decision) to hours (queued, low-priority compensation; supervisor-investigated holds). Watch the realtime stream and confirmation summary for terminal state.

Don’t ship a new Order with the same order_id as a cancelled one

order_ids are intended to be unique to the planner-side decision context. Re-using an order_id for a different fulfillment intent confuses reconciliation. If you need to re-release the same logical work, assign a new order_id.

Cancel before custody confirmation when possible

If you know cancellation is likely (e.g., a customer cancellation in your UI is provisional pending a 60s undo window), consider the cancellation timing carefully. Cancellations that arrive after custody confirmation create TOO_LATE outcomes that require downstream reversal flows — operationally expensive on your side.

TOO_LATE is not a failure

If you see TOO_LATE outcomes in production, that’s normal — your cancellation arrived after the executor had successfully completed the work past the custody point. The right reaction is to trigger your downstream reversal flow (refund, return-merchandise, etc.), not to escalate as an error.

UNKNOWN warrants a coordinated investigation

UNKNOWN outcomes indicate the executor couldn’t determine state. This is rare and signals either an executor-side persistence issue or an audit-log gap. Coordinate with the executor team; don’t attempt automatic recovery.

See also

  • Dispatch — sending Order events (dispatch.shipper-released.v1, etc.)

  • Realtime — the stream that carries compensation movement events

  • Confirmation — window-close reconciliation

  • Errors — protocol-violation and other error shapes

  • Idempotency — replay semantics and dedup

  • openapi-reference — full schema for dispatch.cancelled.v1 request + ack

Authoritative ADRs

  • wes-contract ADR-0002 — the cancel_outcome shape this guide documents

  • FG.AI WES ADR-0013 §10 — custody-confirmation primitive (referenced from this guide; per-Scheme primitives)

  • FG.AI WES ADR-0013 §8 — CompensationTask + per-Scheme compensation-resolution preferences

  • FG.AI WES ADR-0017 §7 — PlannerPortalArtifact cancellation choreography (executor side)

  • FG.AI WES ADR-0019 §6.5 — per-Scheme custody-confirmation primitives + compensation-resolution matrix

  • FG.AI WES ADR-0019 §6.6 — TransferInScheme cancellation with inventory en route (cross-Site case; pending-inbound-disposition CompensationTask)

  • FG.AI WES ADR-0013 §8 — pending-inbound-disposition compensation_type + activation_trigger: inbound-arrival

  • FG.AI WES ADR-0016 §CT-B — chaos-test sub-scenario for cancellation arriving after local completion (the connected/disconnected variant)