# Unified FHIR Async Operations Pattern Some operations are computationally intensive and can take minutes or hours (bulk export, search-parameter reindexing, full resource revalidation). Running them asynchronously prevents blocking the client and lets the server manage the load. In an async flow the client kicks off the job, the server runs it in the background, and the client either polls for status or receives a callback when it completes. FHIR already provides [async support](https://hl7.org/fhir/async.html) through [Bulk Data Access](https://build.fhir.org/ig/HL7/bulk-data/en/export.html) and the [R5 Asynchronous Interaction Request pattern](https://hl7.org/fhir/async-bundle.html). They share the same lifecycle: * Kick-off request -> `202 Accepted` with `Content-Location`. * Status request (while running) -> `202 Accepted` with `Retry-After` and optionally `X-Progress`. * Optional cancel -> `DELETE` to the status URL returns `202 Accepted`. * Terminal status -> either `200 OK` (payload inline) or `303 See Other` (redirect to the payload). Inline modes (Bundle, Bulk manifest) always return `200 OK` on completion; success vs. failure is indicated by the payload (e.g., a result Bundle vs. an `OperationOutcome`, or error links inside a manifest). Redirect mode surfaces the original synchronous status codes on the redirected request (e.g., `200/201` on success, `4XX/5XX` on failure). The patterns mainly differ by the envelope for the final payload: * Bulk Data Access returns a custom JSON manifest. * Asynchronous Interaction Request returns a FHIR Bundle. * Redirect mode returns whatever the synchronous interaction would have returned (Parameters, Bundle, Binary, etc.). This breaks down when the payload cannot be expressed as a Bundle (e.g., streaming outputs or binary content). R6 work aims to standardize async across all interactions. Each interaction now allows `202 Accepted`, and [Josh's draft spec](https://hackmd.io/@jmandel/async-pattern-simplified) proposes an additional redirect pattern: the terminal status response is `303 See Other` with a `Location` pointing to the result; that endpoint returns the actual payload and its status. Ideally we can unify these patterns into a single parameterized one. Use `Prefer: respond-async` plus a custom `Prefer` token to request the mode. For backward compatibility the default is `bundle`; if `_outputFormat` is present, treat the request as Bulk and return a manifest (servers MAY reject a conflicting `async-mode` with `400 Bad Request`). ```http GET /fhir/[$operation] HTTP/1.1 Prefer: respond-async, async-mode=[redirect|bundle] # async-mode is a proposed extension token ``` Servers SHOULD echo honored preferences via `Preference-Applied`. Clients should assume unknown `Prefer` tokens are ignored and be prepared to fall back to the default bundle mode. When the operation is completed: * `async-mode=bundle`: server SHOULD respond with `200 OK` and a Bundle body; failures use an `OperationOutcome` payload. * `_outputFormat` parameter present: server SHOULD respond with `200 OK` and a Bulk Manifest body; failures surface via manifest error links and/or an `OperationOutcome` entry. * `async-mode=redirect`: server SHOULD respond with `303 See Other` and a `Location` URL pointing to the result of the operation; the redirected request returns the same status codes as the synchronous interaction. Clients can infer completion and the mode from the status code (`200` or `303`); success vs. failure depends on the final payload or the redirected response status. ## Extensions Here is a list of extra extensions which can be added to improve support for async operations: ### Long polling extension Servers may use [long polling](https://en.wikipedia.org/wiki/Long_polling) for status requests to minimize latency. The server keeps the connection open until the operation is completed or a timeout occurs. On timeout the client retries, and the server controls the frequency of incoming status requests. Long polling reduces needless polling traffic while still delivering fast notification when the state changes (completion or cancellation). ### Callback extension Purpose: allow the server to notify the client when a job finishes so the client can pause or stop polling. Motivation: callbacks keep response latency low without aggressive polling. Clients can back off to slow, cheap polls (or none) while still getting near-real-time notification when the job finishes. Flow: 1) Client includes `callback-url` with `Prefer: respond-async` (and optional `async-mode`). 2) When the job completes or is cancelled, the server sends one POST to that URL. 3) If the callback fails or never arrives, the client relies on polling. Example kick-off with callback: ```http POST /fhir/$operation HTTP/1.1 Prefer: respond-async, async-mode=redirect, callback-url=https://example.com/callback Accept: application/fhir+json Content-Type: application/fhir+json { ...normal operation body... } ``` Payload: a FHIR `Parameters` resource containing: * `status` (`completed | failed | cancelled`) * `resultUrl` (URL the client can fetch for the final payload) * Optional `OperationOutcome` parameter when the job failed Delivery: best-effort, no retries; callbacks are a signal, not the primary delivery channel (polling is). Callbacks SHOULD be idempotent so duplicates are harmless. Security: servers SHOULD authenticate callbacks (e.g., OAuth2 bearer token, mTLS, or HMAC). Client handling: respond with `2XX` on success; non-`2XX` just means the client will poll. Example callback request: ```http POST https://example.com/callback HTTP/1.1 Content-Type: application/fhir+json Authorization: Bearer eyJhbGciOi... { "resourceType": "Parameters", "parameter": [ { "name": "status", "valueCode": "completed" }, { "name": "resultUrl", "valueUrl": "https://fhir.example.com/whatever/path/1ab7162f-result" } ] } ``` ## Example Visual Flows Here are some example visual flows for the different modes. ## Bulk Manifest Mode ```mermaid sequenceDiagram participant Client participant Server Note over Client,Server: Step 1: Kick-off (_outputFormat parameter) Client->>Server: POST /$operation?_outputFormat=application/fhir+ndjson (Prefer: respond-async) Server->>Client: 202 Accepted (Content-Location: /whatever/path/1ab7162f-status) loop Polling Loop (Step 2) Client->>Server: GET /whatever/path/1ab7162f-status Server->>Client: 202 Accepted (Retry-After: 60) end Client->>Server: GET /whatever/path/1ab7162f-status (Final poll) alt Job Succeeded Server->>Client: 200 OK (Body: Bulk Manifest JSON with output links) else Job Failed Server->>Client: 200 OK (Body: Bulk Manifest JSON with error links and/or OperationOutcome) end ``` ## Bundle Mode ```mermaid sequenceDiagram participant Client participant Server Note over Client,Server: Step 1: Kick-off (async-mode=bundle) Client->>Server: POST /$operation (Prefer: respond-async, async-mode=bundle) Server->>Client: 202 Accepted (Content-Location: /whatever/path/1ab7162f-status) loop Polling Loop (Step 2) Client->>Server: GET /whatever/path/1ab7162f-status Server->>Client: 202 Accepted (Retry-After: 60) end Client->>Server: GET /whatever/path/1ab7162f-status (Final poll) alt Job Succeeded Server->>Client: 200 OK (Body: Bundle) else Job Failed Server->>Client: 200 OK (Body: OperationOutcome or error Bundle) end ``` ## Redirect Mode ```mermaid sequenceDiagram participant Client participant Server Note over Client,Server: Step 1: Kick-off (async-mode=redirect) Client->>Server: POST /$operation (Prefer: respond-async, async-mode=redirect) Server->>Client: 202 Accepted (Content-Location: /whatever/path/1ab7162f-status) loop Polling Loop (Step 2) Client->>Server: GET /whatever/path/1ab7162f-status Server->>Client: 202 Accepted (Retry-After: 60) end Client->>Server: GET /whatever/path/1ab7162f-status (Final poll) Server->>Client: 303 See Other (Location: /whatever/path/1ab7162f-result) Note over Client,Server: Fetch result from redirect endpoint. alt Job Succeeded Client->>Server: GET /whatever/path/1ab7162f-result Server->>Client: 200 OK (Body: Parameters/Bundle/Binary/etc) else Job Failed Client->>Server: GET /whatever/path/1ab7162f-result Server->>Client: 4XX/5XX (Body: OperationOutcome) end ``` ## Cancellation Flow ```mermaid sequenceDiagram participant Client participant Server Note over Client,Server: Step 1: Kick-off Client->>Server: POST /$operation (Prefer: respond-async) Server->>Client: 202 Accepted (Content-Location: /whatever/path/1ab7162f-status) Note over Client,Server: Optional polling while job runs Client->>Server: GET /whatever/path/1ab7162f-status Server->>Client: 202 Accepted (Retry-After: 60) Note over Client,Server: Step 2: Cancel Client->>Server: DELETE /whatever/path/1ab7162f-status Server->>Client: 202 Accepted (Cancellation acknowledged) Note over Client,Server: Step 3: Confirm cancellation Client->>Server: GET /whatever/path/1ab7162f-status Server->>Client: 404 Not Found ``` ## Callback Mode ```mermaid sequenceDiagram participant Client participant Server participant CallbackEndpoint Note over Client,Server: Step 1: Kick-off (with callback-url) Client->>Server: POST /$operation[_format=...] (Prefer: respond-async, async-mode=[redirect|bundle], callback-url=https://example.com/callback) Server->>Client: 202 Accepted (Content-Location: /whatever/path/1ab7162f-status) Note over Client,Server: Client can optionally poll for status opt Polling (optional) Client->>Server: GET /whatever/path/1ab7162f-status Server->>Client: 202 Accepted (Retry-After: 60) end Note over Server,CallbackEndpoint: Server sends one callback when job completes alt Job Succeeded Server->>CallbackEndpoint: POST https://example.com/callback (Body: Status=completed, result-url=...) else Job Failed Server->>CallbackEndpoint: POST https://example.com/callback (Body: Status=failed, OperationOutcome=...) end Note over Client,CallbackEndpoint: Client receives notification and fetches result Client->>Server: GET /whatever/path/1ab7162f-result Server->>Client: 200/201/4XX/5XX (Final payload or OperationOutcome) ```