Asynchronous operations
Long-running workflow operations — order processing and stocktake resolution being the big two — can run in the background instead of holding your HTTP request open. You opt in per request, get a background task id back immediately, and then either poll the background-task endpoints or subscribe to the completion events. This page covers both halves: the background task's own state machine, and the caller-facing request pattern.
The background task state machine
Every queued operation is tracked as a background task with a State:
| State | Set when | Notes |
|---|---|---|
Pending | The API queues the operation as a message | Pending and Processing both count as "active" for the pending-task queries. |
Processing | A worker consumes the message | Sets StartTime (first time only) and Progress = 0. |
Waiting | Order/inventory workers only — another operation is in flight on the same target | The worker polls every 10 seconds for up to 120 seconds, then returns to Processing. |
Complete | The worker finishes successfully | Terminal. Sets EndTime, Progress = 100, and optionally TaskOutput. |
PendingRetry | The worker hit an error | Stores an ErrorLogId. The messaging layer (MassTransit) redelivers with incremental backoff — 3 attempts at 30 s / 1 m 30 s / 2 m 30 s; each redelivery moves the task back to Processing. |
Errored | All retries exhausted | Terminal. Cleans up the auto-delete event subscriptions, raises the InventoryOperationCallback / OrderOperationCallback system events, and records the ErrorLogId. |
There are no transitions out of Complete or Errored.
ReplayBackgroundTask
does not resurrect a task — it creates a new Pending task from any
existing one (there is no state gate on which tasks can be replayed). Task rows
are eventually hard-deleted by retention cleanup, so don't rely on ids
staying resolvable forever.
Requesting an operation asynchronously
Operations that support the pattern (order processing, stocktake snapshot adds and resolves, cycle-count resolves) accept two fields on the request DTO:
NotifyOnceComplete— settrueto run in the background.NotificationRecipientId— must equal your own (the caller's) user id. If it doesn't match, the request is silently processed synchronously — no error, noBackgroundTaskId; the response is simply the synchronous result. This is by design.
When the async path engages, the endpoint returns 200 immediately with a
result DTO carrying a BackgroundTaskId (e.g. OrderOperationResultDto,
InventoryOperationResultDto, ResolveCycleCountResultDto), and the server
auto-creates auto-delete event subscriptions for you — the relevant
completion event plus an operation callback (for example
InventoryCompleted + InventoryOperationCallback for a stocktake resolve;
cycle counts raise CycleCountResolved only, with no callback). The
subscriptions are removed automatically once the task finishes or errors.
The body you get back with the BackgroundTaskId is not the final state of
the entity — it is fabricated to acknowledge the request (the inventory path,
for instance, returns a synthetic InventoryState: "Processing" that was never
read from the database). Never treat the immediate 200 as the outcome; the
outcome arrives via polling or the subscribed events.
Tracking the outcome
Poll or fetch via the background-task endpoints:
| Endpoint | Use |
|---|---|
GetMyPendingBackgroundTasks | All of your active (Pending/Processing) tasks. |
GetPendingBackgroundTasksFor/{targetId} | Active tasks for a specific target entity (an order, an inventory) — useful before starting another operation on the same target. |
GetBackgroundTask/{id} | One task by id — state, progress, TaskOutput, error log reference. |
GetFilteredBackgroundTasks | OData-filtered listing. |
Alternatively, rely on the auto-created subscriptions and consume the completion/callback events instead of polling.
Because a busy target parks the worker in Waiting rather than failing, two
overlapping operations on the same order or inventory queue behind each other
(up to the 120-second window) instead of erroring — but the synchronous API
path refuses a busy target with a 400 InvalidOperation. If you mix modes,
check GetPendingBackgroundTasksFor before issuing a synchronous call.
Who uses this pattern
- Order lifecycle — append-tags, add-snapshot,
allocate, dispatch, receive, and complete all accept
NotifyOnceComplete. - Stocktakes & counting — inventory snapshot adds, resolves, manage, and cycle-count resolves.
Related
- Errors & response conventions — where the immediate-200-with-placeholder behaviour fits among the platform's other response conventions.