0.1.0 - ci-build

SENAITEFHIRImplementationGuide - Local Development build (v0.1.0) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions

Lab Request and Results

Lab Request and Results

Actors:

  • Provider: SENAITE will function as the API provider.
  • External Consumer or simply Consumer: Can be an EHR directly or via a middleware layer. Could also be another consumer too.

This is what most people would consider the primary workflow in a LIMS integration: posting lab requests to SENAITE (the Provider) and making results available to an EHR (the External Consumer) either directly or via a middleware layer.

This workflow handles the following actions:

  1. Order Lab test: A new lab test has been ordered in the consumer and its details need to be shared with SENAITE for processing. This will include details about the sample/specimen along with test information like profiles/panels and tests.
  2. Revoke Existing Order: When lab test that has been ordered needs to be revoked. Please see the valid state transitions to see when it is possible to revoke and how they will transition.
  3. Fetch Results: Once processing a sample is complete, results need to be made available to the consumer. Due to complexities around structuring the vast array of possible test results across multiple categories, this report can be a PDF as published from SENAITE. Additionally, the quantitative results can be represented in a structured form, including reference ranges made available for parsing by the external software.

Artefacts for Request/Results Workflow

See:

  • Profiles: these define the structure of particular resources as they vary from the base type.
  • Instances or Examples: these are examples of each of the profiles above. Available in JSON or XML.

Sequence Diagram & Explanation

Example workflow of request/results exchange

The diagram describes a request-in, poll-for-results integration pattern, where the consumer pushes a lab order, waits for the lab to process it, and then retrieves results either as structured FHIR data or as a PDF report.

  1. Pushing the request: The workflow begins with the External Consumer (e.g. a hospital system or ordering application) POSTing a FHIR Transaction Bundle to the SENAITE FHIR API. This bundle contains all the resources needed to create a lab request, including the ServiceRequest, Patient, Specimen, Practitioner, and Organization (Client). The submitting Client Organisation is identified directly on the ServiceRequest via the SenaiteClient extension — unlike previous SENAITE IGs no Encounter is required. For an example Request Bundle see here. Please see Panel and orderDetail Behaviour in notes below on the relationship between panels and individual tests.

  2. Internal processing: Once received, the FHIR API processes the Bundle and instructs the SENAITE Application to generate a new Sample — translating the FHIR resources into SENAITE's internal data model. SENAITE generates the Sample ID at this point; it is not present anywhere in the submitted Bundle and must not be supplied on POST. The optional Client Sample ID, if the consumer included one on the ServiceRequest and Specimen, is preserved as-is. Both identifiers appear on the DiagnosticReport retrieved in step 5. See Identifiers.

  3. Acknowledgement: The FHIR API returns a FHIR Transaction Response Bundle back to the External Consumer, confirming that the request was received and processed successfully.

  4. Sample processing (SENAITE internal): Shown as a parallel block, this step happens independently and asynchronously. A lab clerk works through the sample processing workflow in SENAITE — receiving, analysing, and resulting the sample.

  5. Results polling: The Consumer regularly polls the FHIR API using a GET search of the following structure:

   GET {fhir_api_root}/DiagnosticReport
     ?_lastUpdated=gt<since>
     &_summary=true
     &_include=Observation:result
  • _lastUpdated=gt<since> — consumers should store the timestamp of the last successful fetch and pass it on each subsequent poll to retrieve only new or changed reports.
  • _summary=truerequired on all search requests. For performance reasons the server will not return the full PDF payload during polling. Under _summary=true the presentedForm.data field (the base64-encoded PDF) is excluded; only metadata such as contentType and title is returned.
  • _include=Observation:result — instructs the server to return the associated Observation resources as sibling entries in the same Bundle, avoiding a second round-trip to retrieve discrete results.

The response is a SenaiteResultsBundle containing the SenaiteDiagnosticReport and any associated SenaiteObservation entries.

  1. PDF report fetch: After discovering a new report in the polling response, the Consumer retrieves the full PDF by performing a direct read of the DiagnosticReport by id — without _summary:

    GET {fhir_api_root}/DiagnosticReport/{id}
    

    The response is the complete DiagnosticReport resource with presentedForm.data populated as a base64-encoded PDF attachment.

Request Notes

Panel and orderDetail Behaviour

The ServiceRequest.code element identifies the ordered panel. The ServiceRequest.orderDetail element enumerates the individual tests to run and is governed by the following rules:

Scenario code orderDetail Server behaviour
Defer to panel Panel code Empty (omitted) Sample is created for all tests defined in the panel
Full elaboration Panel code All tests in the panel enumerated Sample is created for exactly the tests listed in orderDetail
Panel plus extras Panel code All panel tests enumerated, plus additional test codes Sample is created for exactly the tests listed in orderDetail
Partial subset ❌ Panel code One or more panel tests are missing Request is rejected400 Bad Request with an OperationOutcome
Defer to order detail Default panel code (30954-2) One or more test codes Sample is created for exactly the tests listed in orderDetail; no panel validation is performed
Default panel, no tests ❌ Default panel code (30954-2) Empty (omitted) Request is rejected400 Bad Request with an OperationOutcome

When a named panel code is supplied, orderDetail is treated as a complete elaboration of the order: the server validates that all tests belonging to the panel are present, then creates the sample for exactly those tests — plus any additional tests listed beyond the panel's definition. Omitting orderDetail entirely defers to the panel's own definition. Supplying a partial subset is always rejected, as the server cannot infer which panel tests were intentionally excluded.

Note: This design intentionally diverges from the strict R5 multi-ServiceRequest pattern for requesting multiple procedures. Rather than requiring one ServiceRequest per test, this server uses orderDetail as a structured elaboration of the order, keeping the request self-contained and aligned with SENAITE's internal model of a single Sample with a list of analyses. See orderDetail Encoding below for the R5-conformant structure used.

If the consumer supplies a named panel code with an orderDetail that omits tests from the panel, the request is rejected. The OperationOutcome will identify the missing codes, for example:

{
  "resourceType": "OperationOutcome",
  "issue": [{
    "severity": "error",
    "code": "business-rule",
    "details": {
      "text": "orderDetail is a partial subset of panel 57021-8 (CBC). Missing tests: [4544-3 Platelets [#/volume] in Blood]. Either omit orderDetail to use the full panel definition, or include all panel tests."
    }
  }]
}

To order the full panel without customisation, omit orderDetail entirely. To order a specific set of tests within or extending a panel, enumerate all intended tests in orderDetail. To order an arbitrary collection of individual tests without reference to any panel, use the default panel code (30954-2) and enumerate the desired tests in orderDetail. Ordering multiple named panels in a single ServiceRequest is not supported; consumers requiring tests from multiple panels should instead enumerate the individual tests using the default panel code.

orderDetail Encoding

In FHIR R5, orderDetail is a backbone element comprising an optional parameterFocus and one or more parameter entries, each with a typed code and a value[x]. This server uses one orderDetail entry per test, with a single parameter carrying the LOINC test code as a valueCodeableConcept. The parameter.code identifies the parameter type using the SenaiteOrderDetailParameterCS code system defined in this IG:

"orderDetail": [
  {
    "parameter": [{
      "code": {
        "coding": [{
          "system": "http://fhir.senaite.org/CodeSystem/senaite-order-detail-parameter-cs",
          "code": "test-code",
          "display": "Test Code"
        }]
      },
      "valueCodeableConcept": {
        "coding": [{
          "system": "http://loinc.org",
          "code": "718-7",
          "display": "Hemoglobin [Mass/volume] in Blood"
        }]
      }
    }]
  }
]
The Default Panel Code

Because ServiceRequest.code is required when orderDetail is present in FHIR R5, consumers ordering a free-form selection of tests rather than a named panel must supply a sentinel value. The server recognises LOINC code 30954-2 (Relevant diagnostic tests/laboratory data note) as this sentinel:

"code": {
  "concept": {
    "coding": [{
      "system": "http://loinc.org",
      "code": "30954-2",
      "display": "Relevant diagnostic tests/laboratory data note"
    }],
    "text": "Relevant diagnostic tests/laboratory data note"
  }
}

When this code is present, the server bypasses panel validation entirely and creates the Sample for exactly the tests listed in orderDetail. Individual test codes should be LOINC-coded where available. Omitting or leaving orderDetail empty when using the default panel code is an error, as there is no panel definition to fall back on.

Default Panel Code on DiagnosticReport

The same default panel code appears on outbound DiagnosticReport resources when the underlying SENAITE Sample does not correspond to exactly one named panel. This covers two cases:

  • Individual tests only — the Sample was created from a ServiceRequest using the default panel code.
  • Multiple panels — the Sample has more than one panel assigned in SENAITE, which can occur when a sample is managed directly within SENAITE rather than via an inbound ServiceRequest.

In both cases, DiagnosticReport.code is set to 30954-2 and the individual test results carried as Observation resources (referenced via DiagnosticReport.result) are the authoritative record of what was ordered and resulted. Consumers should treat the Observation entries as the primary source of test identity in these reports, using DiagnosticReport.code only to recognise that a single named panel does not apply.

Secondary Workflow: Revoke Lab Request

Revoke lab request sequence diagram
  1. The External Consumer first submits the ServiceRequest Bundle to the SENAITE FHIR API and receives a transaction response bundle.
  2. When a lab request must be revoked, the Consumer then POSTs to ServiceRequest/{id}/$revoke with optional reason(s) as to why this is being revoked as the body, for example:
    {
      "resourceType": "Parameters",
      "parameter": [
     {
       "name": "reason",
       "valueString": "Sample lost in transit"
     },
     {
       "name": "reason",
       "valueString": "Patient requested revocation of processing"
     }
      ]
    }
    
  3. The SENAITE FHIR API forwards this revoke action to the SENAITE Application, which withdraws the associated sample and updates its state to Cancelled, and notifies downstream systems as needed.
  4. The application confirms the revocation and the FHIR API returns 200 OK with the updated ServiceRequest resource set to status: revoked and an incremented versionId.

In the diagram, this revoke workflow is effectively a conditional branch from the sample processing stage: the main workflow continues toward result publication, while the revoke workflow terminates the request cleanly and signals the external consumer that the ServiceRequest has been revoked.

Status Mappings

Inbound: POST ServiceRequest → SENAITE Sample

A posted ServiceRequest can only ever create a Sample in the sample_received (Received) state. The registered and sample_due states are therefore unreachable via this workflow.

FHIR ServiceRequest Status SENAITE Transition Side Effects Condition Notes
active (none) → sample_received New Sample created    
$revoke sample_receivedcancelled Sample cancelled No reason included This transition is configurable per deployment
$revoke sample_receivedrejected Sample rejected Reason included This transition is configurable per deployment
$revoke to_be_verified | verifiedrejected Sample rejected    

Outbound: SENAITE Sample → GET DiagnosticReport

When a consumer fetches a DiagnosticReport, its status is derived from the current SENAITE Sample state. Because Samples in this workflow are always created via an inbound ServiceRequest, the registered and sample_due states are not reachable and have no mapping.

SENAITE Sample State Label FHIR DiagnosticReport Status
sample_received Received preliminary
to_be_verified To be Verified preliminary
verified Verified preliminary
published Published final
invalid Invalid entered-in-error
rejected Rejected cancelled
cancelled Cancelled cancelled
retracted Retracted entered-in-error
dispatched Dispatched (previous valid state)

Note on dispatched: Dispatched is a configurable state indicating that the sample has been sent from the lab. Because a sample can be dispatched from multiple upstream states, the DiagnosticReport.status is derived from the sample's last valid state prior to dispatch rather than being fixed. For example, a sample dispatched after published will yield a final report.