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¶
To cancel a dispatched Order, POST
dispatch.cancelled.v1to the executor with the originalorder_id.The executor returns one of four outcomes synchronously:
ACCEPTED,ACCEPTED_PARTIAL,TOO_LATE, orUNKNOWN.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_LATEfor those bindings, and any downstream reversal is your responsibility.For
ACCEPTEDandACCEPTED_PARTIALoutcomes, compensation runs asynchronously at the executor side — the realtime stream carries movement events withcompensation: trueuntil terminal state.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 |
|---|---|---|
|
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. |
|
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 |
|
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). |
|
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¶
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.
Lines 3 & 4 (compensation initiated): Mark these lines as CANCELLED on your side. Watch the realtime stream — you’ll see
compensation: truemovements as items return to source.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 |
|---|---|
|
Only if labels are generic and can be reused; rare for VAS |
|
Best fit when another in-flight Order has the same SKU + branding needs |
|
Default when labels are customer-specific and supervisor needs to decide |
|
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 forwes.workorder.diverted.v1and 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-transitSite 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 shippedSite B:
ACCEPTEDwith 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_attimestamp 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.v1request + ack