Creating e-referral form with SDC
Electronic referrals (aka e-referral) are still one of the pain points of the modern healthcare system.
Today I am showing you how you can implement an e-referral form with FHIR SDC IG.
On the FHIR AU connectathon that happened on the 12th of December 2024 in Melbourne,
I have implemented a full-featured workflow for e-referral on top of https://build.fhir.org/ig/hl7au/au-fhir-erequesting/
and tested it with Sonic Health software.
The easiest way to design an SDC form is to use a form builder or create FHIR Questionnaire manually.
Beda EMR provides all these options:
- First of all, you can use an AI-driven form builder embedded in Beda EMR.
- If you prefer to have everything under your control but still need a user interface you can use Aidbox Form Builder which is integrated with Beda EMR as well.
- Finally, for technical users, we provide SDC IDE, which provides the ability to define and debug complex forms.
Here is a form I created to capture information about e-referral:
Defining a form
Here is its definition as FHIR Questionnaire: https://github.com/beda-software/fhir-emr/blob/e-requests/resources/seeds/Questionnaire/place-order.yaml
Let's review this form step by step.
Since Encounter context is required for e-referral we have to link a for to Encounter by specifying subjectType
parameter:
subjectType:
- Encounter
We also define http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext extension
to bind a variable name with the provided Encounter
.
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext
extension:
- url: name
valueCoding:
code: Encounter
- url: type
valueCode: Encounter
We also need to provide a context of the Author who is a Practitioner:
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext
extension:
- url: name
valueCoding:
code: Author
- url: type
valueCode: Practitioner
and Patient:
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext
extension:
- url: name
valueCoding:
code: Patient
- url: type
valueCode: Patient
Now let's review step by step all form fields:
- The first question is a read-only representation of the requester.
- text: Requester
type: string
linkId: practiioner-name
readOnly: true
extension:
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression
valueExpression:
language: text/fhirpath
expression: %Author.name.first().given.first() + ' ' + %Author.name.first().family
This question uses http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression extension to populate this field with the practitioner's name.
- The second question is also a read-only item.
item:
- text: Patient
type: string
linkId: patient-name
readOnly: true
extension:
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression
valueExpression:
language: text/fhirpath
expression: %Patient.name.first().given.first() + ' ' + %Patient.name.first().family
It represents information about a patient.
Already known extension http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression
is used to populate a patient's name.
- The next three questions are pretty similar. They are using terminology binding to display a list of options:
- text: Test
type: choice
linkId: test-code
required: true
answerValueSet: https://www.rcpa.edu.au/fhir/ValueSet/spia-requesting-refset-3
- text: Intent
type: choice
linkId: intent
required: true
answerValueSet: http://hl7.org/fhir/ValueSet/request-intent
- text: Priority
type: choice
linkId: priority
required: true
answerValueSet: http://hl7.org/fhir/ValueSet/request-priority
answerValueSet
is used to define a valuset that will be loaded from the terminology server and expanded to a choosable list.
- The fourth question is a select for an organization responsible for the requested test:
- text: Assigned organization
type: reference
linkId: assigned-organization
required: true
extension:
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn
extension:
- url: path
valueString: name
- url: forDisplay
valueBoolean: true
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression
valueExpression:
language: application/x-fhir-query
expression: Organization?type=lab
- url: >-
http://hl7.org/fhir/StructureDefinition/questionnaire-referenceResource
valueCode: Organization
This question is a reference to the FHIR resource.
There are a bunch of extensions that allow you to define a logic
of how FHIR resources will be loaded and formatted for this question.
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression is used to define
a x-fhir-query
to load a Bundle of resources from the server.
http://hl7.org/fhir/StructureDefinition/questionnaire-referenceResource is used to identify
what resource type should be represented in a select.
It is important for cases when you are using _include or _revinclude to load multiple resource types.
Finally, http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn defines
an FHIRPath expression that converts a resource into a string.
This string will be displayed in a select and also will be used for display in the reference type.
- The final question is the trickiest one. It is a barcode representation.
item:
- text: Barcode
type: string
linkId: barcode
readOnly: true
extension:
- url: http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl
valueCodeableConcept:
coding:
- code: barcode
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression
valueExpression:
language: text/fhirpath
expression: >- 'BEDA1212-' & (%SR.entry.resource.total+1).select('00000'&toString()).toChars().skip(count()-6).join()
contained:
- id: SR
type: batch
entry:
- request:
url: /ServiceRequest
method: GET
resourceType: Bundle
The barcode needs to be scanned by a laboratory to claim an e-request.
The barcode should be unique across all requests. That's why we are using a couple of tricks to calculate it.
First of all, have a look at http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression
This FHIRPath expression is used to calculate a barcode that consists of two parts.
The static one: is BEDA1212 and dynamic. It is 6 digits with leading zeros.
To make this part unique I need a variable that constantly increases over time.
Since each e-request is represented by ServiceRequest
I am using a number of ServiceRequest existing in the system.
To do so, I am referring to the SR variable that contains a bundle of service requests.
To use this custom variable, I need to define it.
I am doing it by specifying http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-sourceQueries like this:
extension:
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-sourceQueries
valueReference:
reference: '#Bundle#SR'
This extension refers to a contained bundle.
This Bundle will be executed and the result will be available via a variable with the same name as containing resource id.
Here is a contained Bundle definition:
contained:
- id: SR
type: batch
entry:
- request:
url: /ServiceRequest
method: GET
resourceType: Bundle
As you can notice, a barcode is represented as a graphic element in the user interface.
It is not a read-only string as it was for requester and patient names, it is an actual barcode.
It is possible because we are using a custom item control for it,
defined via http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl extension.
Here you can find this control source code:
https://github.com/beda-software/fhir-emr/blob/e-requests/src/components/BaseQuestionnaireResponseForm/widgets/barcode.tsx
import { Form } from 'antd';
import { ReactBarcode } from 'react-jsbarcode';
import { QuestionItemProps } from 'sdc-qrf';
import { useFieldController } from '../hooks';
export function Barcode({ parentPath, questionItem }: QuestionItemProps) {
const { linkId } = questionItem;
const fieldName = [...parentPath, linkId, 0, 'value', 'string'];
const { value, formItem } = useFieldController(fieldName, questionItem);
return (
<Form.Item {...formItem} data-testid={linkId}>
<ReactBarcode value={value} />
</Form.Item>
);
}
As you can see it is just a simple wrapper for react-jsbarcoed library that provides an SDC control interface for it.
- There are 3 questions that are hidden.
http://hl7.org/fhir/StructureDefinition/questionnaire-hidden extension is used to mark questions as hidden.
- text: patientId
type: string
extension:
- url: http://hl7.org/fhir/StructureDefinition/questionnaire-hidden
valueBoolean: true
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression
valueExpression:
language: text/fhirpath
expression: '%Patient.id'
linkId: patient-id
readOnly: true
- text: encounterId
type: string
extension:
- url: http://hl7.org/fhir/StructureDefinition/questionnaire-hidden
valueBoolean: true
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression
valueExpression:
language: text/fhirpath
expression: '%Encounter.id'
linkId: encounter-id
readOnly: true
- text: practitinerId
type: string
extension:
- url: http://hl7.org/fhir/StructureDefinition/questionnaire-hidden
valueBoolean: true
- url: >-
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression
valueExpression:
language: text/fhirpath
expression: '%Author.id'
linkId: practitiner-id
readOnly: true
These questions use that same technique to populate their values via
http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression
However, this question contains resource IDs.
It is a compute readable information that will be needed during the extraction phase.
Extracting data into ServiceReqeust
There is an extension that defines a mapper that will be used to convert this form into an FHIR resource.
extension:
- url: http://beda.software/fhir-extensions/questionnaire-mapper
valueReference:
reference: Mapping/place-order
This mapper is a custom FHIR resource available only via the BedaEMR setup on top of Aidbox.
Mapper is defined with https://github.com/beda-software/FHIRPathMappingLanguage
Here is the mapper definition:
id: place-order
type: FHIRPath
resourceType: Mapping
body:
type: transaction
entry:
- request:
url: "ServiceRequest"
method: POST
resource:
status: 'active'
identifier:
- type:
text: "Placer Group Number"
coding:
- code: PGN
system: http://terminology.hl7.org/CodeSystem/v2-0203
system: http://diagnostic-orders-are-us.com.au/ids/pgn
value: "{{ QuestionnaireResponse.repeat(item).where(linkId='barcode').answer.valueString }}"
category:
- coding:
- code: '108252007'
display: 'Laboratory procedure'
system: 'http://snomed.info/sct'
code:
text: "{{ QuestionnaireResponse.repeat(item).where(linkId='test-code').answer.valueCoding.display }}"
coding:
- "{{ QuestionnaireResponse.repeat(item).where(linkId='test-code').answer.valueCoding }}"
intent: "{{ QuestionnaireResponse.repeat(item).where(linkId='intent').answer.valueCoding.code }}"
priority: "{{ QuestionnaireResponse.repeat(item).where(linkId='priority').answer.valueCoding.code }}"
encounter:
reference: "{{ 'Encounter/' + QuestionnaireResponse.repeat(item).where(linkId='encounter-id').answer.valueString }}"
requester:
reference: "{{ 'Practitioner/' + QuestionnaireResponse.repeat(item).where(linkId='practitiner-id').answer.valueString }}"
subject:
reference: "{{ 'Patient/' + QuestionnaireResponse.repeat(item).where(linkId='patient-id').answer.valueString }}"
performer:
- reference: "{{ QuestionnaireResponse.repeat(item).where(linkId='assigned-organization').answer.valueReference.reference }}"
As you can see it is a wrapper around Bundle definition.
The wrapper provides some useful meta information like mapper id, type, etc.
The most interesting part is located under the body key.
It is an FHIR Bundle transaction with placeholders.
Placeholders are defined with doubled curly brackets {{
and }}
.
Inside them, we define an FHIRPath expression that queries QuestionnaireResponse resource provided to extractions.
Usually this expression just queries answers by their linkIds.
You can see how references to encounter, practitioner, subject,
and performer are extracted from the questionnaire response.
Once data is extracted, a generated bundle transaction is executed on an FHIR server,
so the ServiceRequest resource will be created. You can debug this process in SDC IDE.
I hope you find interesting how this form works under the hood, please feel free to review the source code and check how this feature works: https://github.com/beda-software/fhir-emr/tree/e-requests