Reporting Workflow - mcode/medmorph-backend GitHub Wiki
One of the core functions of the backend service app is to turn patient data into reports, using a process specified in PlanDefinitions. While the details of how that data should be processed is left for content IGs to define, the base MedMorph IG specifies a few actions to serve as the basis for a reporting workflow, which can be extended further in content IGs.
References:
- https://build.fhir.org/ig/HL7/fhir-medmorph/StructureDefinition-us-ph-plandefinition.html
- https://build.fhir.org/ig/HL7/fhir-medmorph/ValueSet-us-ph-plandefinition-action.html
- https://build.fhir.org/ig/HL7/fhir-medmorph/reportsubmission.html
- https://build.fhir.org/ig/HL7/fhir-medmorph/ValueSet-us-ph-triggerdefinition-namedevent.html
Assumptions
-
There is a spectrum of conceptual approaches that MedMorph-based reporting can take:
- At one end, a highly generic framework that allows for arbitrary reports to be defined and executed completely within PlanDefinitions.
- At the other end, a non-configurable framework, where all reports are pre-coordinated within the content IGs and the implementations may be hard-coded. In this case the PlanDefinitions would simply point to one of the predefined reports.
- For this document, we assume that the goal of the IG leans more toward the generic and composable end of the spectrum, and that reports are not entirely pre-coordinated. (If things go the other way, basically no workflow or reusable architecture is required)
-
PlanDefinition.action.action is cardinality 0..*, but not marked as must-support. Therefore we assume that actions will not have nested sub-actions.
-
PlanDefinition.action.code is cardinality 0..1 but we assume it will always be present in order to indicate what action is to be taken.
-
PlanDefinition.action will be "templated" based on the specific action code. Not all possible combinations of structurally valid content will be logically valid. For example, the
create-report
action should always have an output.type ofBundle
. Structurally any resource type could be defined there, but it's not meaningful to have an output of type, say,Appointment
there. (Unless there's a pre-agreed-upon definition that an output type besides Bundle there means "do/do not/only include this resource type in the bundle") These "templates" will have to be defined beforehand. -
There should be some way to parameterize actions but it's not clear what that is yet. (This could also be the case for a "hardcoded" approach as described above, for example there could be a predefined set of reports with parameters that affect processing.)
Overview
The basic idea of the reporting workflow is that each action can be translated into a specific function, with inputs and outputs. These actions can then be chained together arbitrarily to produce a report.
Data Model
- Context:
- patient: patient whose data triggered the workflow
- records: list of records
- encounter: encounter where some data triggered the workflow
- planDefinition: the planDefinition which defines this workflow
- client: set of clients to connect to
- dest: endpoint at PHA/TTP to send report to
- db
- reportingBundle: the entire bundle to be submitted
- contentBundle: just the content section of the report bundle
- flags: various boolean or enum flags about the status of the report.
- triggered: result of check-trigger-codes
- valid: result of validate-report
- submitted: result of submit-report
- deidentified: result of deidentify-report
- anonymized: result of anonymize-report
- pseudonymized: result of pseudonymize-report
- encrypted: result of encrypt-report
- completed: result of complete-report
- currentActionSequenceStep: the current step in action sequence
- actionSequence: a list of actions in the order they should be executed
- cancelToken: indicates the job should be cancelled (from a process external to the job itself -- this tells the job to cease processing)
- exitStatus: if set, terminates the job and marks it with the given status (for example, if a given patient doesn't meet reporting criteria, don't keep reporting)
API
Action
process(Context) => Context
Processes the step that this Action represents, given the provided Context. Returns a new Context object containing changed fields. The surrounding infrastructure will ensure each Action gets a "full" Context object.
Process Flow
--> Incoming subscription
ReportWorkflow.process(subscription)
Fetch PlanDef that the subscription is linked to
Fetch Patient / Encounter given the trigger
for each step in PlanDef:
context[i+1] = plandef.steps[i].process(context[i])
Action Definitions
-
initiate-reporting-workflow
- "Request the initiation of a reporting workflow for a patient for a specific context (e.g., encounter)."
- Top-level code. Expected that all reports will start with this. Not used in this workflow, because this defines the criteria for the trigger
-
execute-reporting-workflow
-
"Execute a series of actions to accomplish reporting"
-
"This is top level action that uses other defined actions to accomplish reporting for a specific context (e.g., encounter)."
-
Top-level code with children. Expected that reports will not use this code directly, but will use child codes
-
check-trigger-codes
- "Evaluate candidate patient's data against trigger codes to determine reportability."
- Note: shouldn't this be the responsibility of the subscription? Only register for events that match?
- Possibly this is meant to support a system that has only broad subscription capabilities and not a more filtered one?
-
check-participant-registration
- "Evaluate encounter participants such as patient, practitioner, organization on whether they have been selected for reportability."
- Note: I believe the name of this one is intended to be
check-reportability
, as mentioned in the next one - check whether "reporting has been completed", as in
complete-reporting
below?
-
create-report
- "Create a Report containing Patient's data for patients who passed the check-reportability test."
- impl:
contentBundle = bundle of (records.filter(...)) ; reportingBundle = bundle of (header, contentBundle)
-
validate-report
- "Validate Report against specified profiles and terminologies."
- impl:
isValid = (...) ; if (isValid) set flags[valid] = true; else set exitStatus
-
submit-report
- "Submit the report to specified endpoint."
- impl:
client.pha.submit(reportingBundle)
-
deidentify-report
- "Deidentify the report before submitting the report."
- impl:
contentBundle = trustServices.deidentify(contentBundle)
-
anonymize-report
- "Anonymize the report before submitting the report."
- impl:
contentBundle = trustServices.anonymize(contentBundle)
-
pseudonymize-report
- "Pseudonymize the report before submitting the report."
- impl:
contentBundle = trustServices.pseudonymize(contentBundle)
-
encrypt-report
- "Encrypt the report before submitting the report."
- TODO: how is FHIR encrypted? Traditionally that would be at the interface level, not the application level. (ie, https) One possibility floated out there is do whatever encryption you like on a Bundle, then store it as a Binary resource: https://stackoverflow.com/a/33308540
-
complete-reporting
- "Complete the reporting for the patient, after which no further reports will be sent for a specific context (e.g., Encounter)."
- impl:
db.insert('completed reports', ...)
-
extract-research-data
- "Extract data from an EHR for one or more patients for research purposes."
- TODO: how is the query defined in the PlanDef?
- impl:
records.push( clients.ehr.query(...) )
-
execute-research-query
- "Execute a research query on the data mart based on the PlanDefinition and create a result to be submitted."
- TODO: again, how is the query defined in the PlanDef?
- this is probably out of scope for our implementation
- impl:
records.push( clients.datamart.query(...) )
-
-
-
terminate-reporting-workflow
-
Terminate a reporting workflow
-
Request the initiation of terminating a reporting workflow.
-
Top-level code with children. Expected that reports will not use this code directly, but will use child codes
cancel-report
- Cancel an already submitted report.
- TODO:
- How do you cancel a report?
- How do you identify the report to be cancelled?
-
Extensibility
The general approach to extensibility is to consider each IG is a class where the action codes define functions. The base MedMorph IG will define initial implementations of the various actions, then they can be extended or replaced by subclasses, ie the content IGs.
Because the Backend Service App uses NodeJS, extensibility is fairly simple.
One directory will contain the implementation of all action definitions, and all files will be require
d at runtime, so new files that implement the expected interface can be dropped in without code changes elsewhere in the system being necessary. If we want to create a "built" or "packaged" version of the app, we can allow for designating an additional directory by configuration.
Note 1: This does mean that new action definitions cannot reference new dependencies without updating the top-level package.json, unless they are bundled in directly.
Note 2: actions may require a reference to the FHIR server to fetch additional data related to a patient, or other tools like a CQL or FHIRPath evaluator, so a reference to these clients will be provided as part of the context.