Skip to main content

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:

StateSet whenNotes
PendingThe API queues the operation as a messagePending and Processing both count as "active" for the pending-task queries.
ProcessingA worker consumes the messageSets StartTime (first time only) and Progress = 0.
WaitingOrder/inventory workers only — another operation is in flight on the same targetThe worker polls every 10 seconds for up to 120 seconds, then returns to Processing.
CompleteThe worker finishes successfullyTerminal. Sets EndTime, Progress = 100, and optionally TaskOutput.
PendingRetryThe worker hit an errorStores 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.
ErroredAll retries exhaustedTerminal. 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 — set true to run in the background.
  • NotificationRecipientIdmust equal your own (the caller's) user id. If it doesn't match, the request is silently processed synchronously — no error, no BackgroundTaskId; 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 immediate response is a placeholder

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:

EndpointUse
GetMyPendingBackgroundTasksAll 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.
GetFilteredBackgroundTasksOData-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.