In this introductory tutorial, we will use an example use case to illustrate the core concepts of CQLab. We will be using the CQFlow, CQDefine, CQVocabulary and CQMockData modules.
We are going to create a simple clinical pathway that creates a recommendation. It will search through a patient's medical record to determine whether a patient is eligible for a breast cancer screening. This example is purely for illustration purposes and meant to be very simple, but it will introduce key concepts that can be leveraged in more sophisticated use cases.
Our first step is to think through our solution from a high level perspective. We break down our solution into inputs, internal logic, and outputs.
Our final solution will be two separate UIs that display our results in two slightly different formats.
Let's now dive into the implementation details.
In order to be eligible for a breast cancer screening in our toy example, the following constraints must be met:
We will first use CQFlow to model our rules. CQFlow includes a visual flow design tool that enables us to create a flow diagram that represents our clinical algorithm. It is generally used by clinical domain experts to define logic that will be used as a "clinical specification" for the engineering team to later turn into a working application.
The flow diagram will auto-generate a JSON based Flow Definition that can be used by the engineering team to implement the clinical logic.
Below is our completed flow diagram. As previously mentioned, the input is a specific patient, and the possible outputs are:
A Flow Diagram is a proven tool that can be used to effectively communicate and model clinical algorithms, and can be embedded in web pages, clinical guidelines, custom applications, FHIR IGs, research papers, or any other digital medium.
This particular diagram has a total of 4 node types. A Start, End, True/False, and Emit Data node. We discuss the flow diagram and node types in more detail here. But for now, the important part is that the True/False node is a decision point that changes the direction of the flow based on the result of an evaluation, and an Emit Data node allows data to be returned as output back to the user.
Now that a flow diagram with a corresponding Flow Definition has been defined, it is time to create the Flow Implementation. The flow implementation is where the code that executes the flow is defined. Commonly, the flow implementation is exposed and executed through the CQServer, although it is possible to execute the flow implementation in any TypeScript compatible environment (including a custom server or even the browser).
This approach of building a custom server and using an SDK to implement the logic was chosen because it provides a powerful, flexible, testable, repeatable, and scalable approach to implement clinical logic. It makes it possible to have full control of data access, querying, caching, testing, debugging, and all the other vital aspects of implementing high quality and robust software. The code can be written with modular design principles and can leverage robust tooling. The server can be wrapped in Docker and easily deployed on internal infrastructure, and all authentication and data access can be controlled by the organization's existing security protocols.
After many years of experience working with different organizations, we have found the absolute necessity for this approach. It is certainly possible to model simple/toy examples using various no-code or low-code solutions. However, in real production use cases with complex needs and requirements, a mature software engineering environment with full customizability has proven to be the most efficient way to develop reliable, custom, performant, and maintainable solutions.
We will discuss the specifics of the following SDK and APIs in more detail later, but for now it's just important to understand that 1) clinical domain experts create a "clinical specification" through the Flow Definition and 2) the engineering team transforms the specification into executable code through the Flow Implementation.
Here are a few key files from the Flow Implementation, feel free to click the view source link to see the full code:
import { BreastCancerScreeningContext } from './breast-cancer-screening-interactive-context';
import {
InteractiveFlowImplementation,
ExecNode,
TernaryEnum,
} from '@cqlab/cqflow-core';
// Create an enum for the node bindings defined in the flow definition
// These bindings are used to map a node definition to node implementation
enum BreastCancerScreeningEnum {
is_female = 'is_female',
is_over_45 = 'is_over_45_years_old',
has_had_breast_cancer_screening_in_last_2_years = 'has_had_breast_cancer_screening_in_last_2_years',
}
// Create an ExecNode that uses the breastCancerScreeningLibrary library to
// make a calculation using patient data
class IsFemale extends ExecNode<BreastCancerScreeningContext> {
override async evaluate(
context: BreastCancerScreeningContext
): Promise<TernaryEnum> {
return context.breastCancerScreeningLibrary.isFemale();
}
}
class IsOver45 extends ExecNode<BreastCancerScreeningContext> {
override async evaluate(
context: BreastCancerScreeningContext
): Promise<TernaryEnum> {
return context.breastCancerScreeningLibrary.isOver45();
}
}
class HasHadBreastCancerScreeningInLast2Years extends ExecNode<BreastCancerScreeningContext> {
override async evaluate(
context: BreastCancerScreeningContext
): Promise<TernaryEnum> {
return context.breastCancerScreeningLibrary.hadBreastCancerScreeningInLast2Years();
}
}
// Instantiate the flow implementation and register the nodes
export const breastCancerScreeningImplementation =
new InteractiveFlowImplementation<BreastCancerScreeningContext>();
breastCancerScreeningImplementation.registerTrueFalse(
BreastCancerScreeningEnum.is_female,
(nodeDef) => new IsFemale(nodeDef)
);
breastCancerScreeningImplementation.registerTrueFalse(
BreastCancerScreeningEnum.is_over_45,
(nodeDef) => new IsOver45(nodeDef)
);
breastCancerScreeningImplementation.registerTrueFalse(
BreastCancerScreeningEnum.has_had_breast_cancer_screening_in_last_2_years,
(nodeDef) => new HasHadBreastCancerScreeningInLast2Years(nodeDef)
);
There are two primary types of Flow Implementations supported by the SDK: Interactive and Non-Interactive.
Interactive flow implementations are those that require user input in order to execute, such as drop down menus, text inputs, or check boxes. They are exposed in user facing applications including web and mobile apps.
Non-Interactive flow implementations do not provide the ability for user interaction. They are typically executed in back end processes that do calculations or provide recommendations that can be surfaced to the user.
In our current flow definition, there could be cases where both could make sense (NOTE: this is not always the case). We show an example of both to illustrate the similarites and differences.
The output to a CDS workflow is generally a data payload, and/or a user interface that utilizes the data payload. CQLab provides a UI component library written in React that can be used to display the output of a flow execution. It is also possible to build fully custom UIs for unique needs.
In our current example, the expected output is whether or not a patient should recieve a recommendation for a breast cancer screening.
CQFlow has built in support for Explainability. Not only is it able to execute logic to perform tasks, it is able to explain HOW it arrived at its conclusion.
The following examples should illustrate. Scroll through each patient to see the output of the non-interactive flow execution. You can also click the patient tab to see the FHIR based structured medical record data that is being used to execute the flow.
As the name implies, a non-interactive flow does not require any user interaction. The output data can be rendered in a User Interface, can be used to trigger another process, can generate a report, save data to a database, etc.
Notice first that an explanation for why the recommendation was (or was not) given is surfaced. Each step can be reviewed and confirmed by an end user. A common extension is the ability to "refute" results, or an ability to update the medical record to reflect a change in the patient's situation.
Next, the Emitted Data (in blue) is surfaced and expresses the final output of the evaluation. This contains the final structured data output that can be used by calling processes.
And finally, by clicking the expand buttons below configured node, supplemental and emitted data is surfaced directly to the user. This can be a very useful way to provide information that is not the direct output of the execution, yet can be very helpful to provide insight into what data was leveraged to make a decision.
An interactive flow, on the other hand, requires user input to complete. The two most common reasons are 1) there is missing data in the patient's record that needs to be manually entered by the user or 2) there is clinical judgement involved and it takes a physician or another medical professional to make a decision that can not be automated. These two cases are both very common, and we have found a large percentage of clinical decision support scenarios fall into the interactive flow category. Instead of full automation, this approach provides partial automation and data entry assistance which can still provide major improvements in efficiency and quality for many routine tasks.
If we can automate 7 out of 10 question in a workflow, that is a 70% boost in efficiency (assuming each question takes roughly the same amount of time to answer).
Answer the questions below to see the output of the interactive flow. Click reset to clear current answers.
Three possibilities occur in the above examples:
How exactly an answer is resolved, and what data is used to resolve it, is fully controlled by the TypeScript SDK. Every project has different needs, and the SDK provides the flexibility to customize behavior based on an individual product's goals.
In this introductory example, we introduced some core concepts of CQLab. We showed how a simple logic flow can be defined using a flow chart and Flow Definition, pointed you to example code showing how the Flow Implementation can be built using the CQLab SDK, and finally showed 2 examples of how the results can be generated using a non-interactive and interactive approach.
Next, we suggest exploring more examples to get a feel for different use cases that CQLab can be used to solve.