The Decision Engine Is Data
A clinical routing engine built once on FHIR primitives, then parameterised and driven entirely from FHIR resources in the database. New conditions and protocols ship as artifacts, not as code changes.
Decisions that end up hardcoded
Every clinical product reaches the same moment. A result comes in, and the system has to decide what happens next. A questionnaire is completed and a follow-up program should start. A symptom score crosses a threshold and a clinician should be paged. A medication-adherence answer reports a treatment change and a different protocol should take over. The decision is the clinical core of the product, and most teams write it as code.
Writing it as code is the path of least resistance. A handler reads the response, an if statement checks the score, a branch enrolls the next plan or sends the alert. It works for the first condition and the first protocol. Then the product adds a second condition, a third, a regional variation, a new escalation rule, and the decision logic spreads across handlers the way scattered authorization checks spread across controllers. The shape of the failure is the one we described in The FHIR Authorization Tax: a rule that is correct in nine places and subtly wrong in the tenth.
Clinical routing carries a second cost that authorization does not. Clinical routing logic changes for clinical reasons, not engineering ones. The threshold moves because a guideline was updated. A new indication is added because the product expanded. The team that knows why the rule should change (the clinical team) cannot change it, because the rule is in a deploy, behind an engineering ticket, behind a release, behind a regulatory re-review of the changed code.
Observe, extract, evaluate, act
Underneath every one of these decisions is the same sequence. Observe a result. Extract the structured facts from it. Evaluate a set of conditions against those facts. Act on the conditions that hold.
A questionnaire produces a QuestionnaireResponse. The structured facts are the answers, turned into coded clinical resources: a Condition with a SNOMED code, an Observation with a score, a MedicationStatement. The conditions are clinical predicates over those facts: this patient has atopic dermatitis and no other indication, this score is above the escalation threshold. The actions are bounded: enroll a CarePlan, supersede the current one, raise a flag for a human to review.
The sequence is the same across patient questionnaires, chronic disease management, remote patient monitoring, and ePRO. The facts differ, the predicates differ, the actions differ. The sequence does not. A decision engine that implements the sequence once, and reads the facts, the predicates, and the actions as data, does not have to be rewritten for the next product or the next condition.
Moving the decision into the data layer
FHIR already carries the vocabulary for every part of the sequence, which means the decision can live where the data lives instead of in application code.
The extraction is the Structured Data Capture $extract operation. A Questionnaire is annotated with the FHIR resources its answers should produce; QuestionnaireResponse/$extract turns a completed response into those resources. The mapping from answer to clinical fact is data on the Questionnaire, not code in a handler.
The predicates are a Library of Clinical Quality Language (CQL). The library is a FHIR resource; the CQL is its content. Each named definition is a clinical predicate (has atopic dermatitis and not the other two indications) evaluated against the extracted resources.
The decision table is a PlanDefinition. Each action carries an applicability condition that references a CQL definition by name, a code naming the bounded action to take, and a definitionCanonical pointing at the plan to enroll. Evaluating the PlanDefinition with $apply runs the conditions and returns the actions whose conditions held.
The actions produce ordinary clinical resources: a CarePlan and its scheduled Task occurrences, a Flag for human review, a Provenance recording why the decision was made. None of these is bespoke. They are the resources a clinical record already contains.
The engine is generic code that runs the sequence: extract, evaluate, act. The behaviour is the FHIR resources it reads. A Library, a PlanDefinition, and an annotated Questionnaire in the database are the entire definition of how a given product routes a patient.
A worked example
A dermatology service collects patient-reported outcomes over WhatsApp. A new patient completes an onboarding questionnaire in a chat thread; established patients answer recurring instruments (POEM, DLQI) on a schedule. The product has to read each onboarding response and start the correct disease-specific follow-up program: atopic dermatitis, psoriasis, or hidradenitis suppurativa.
When the onboarding response is complete, $extract turns the triage answers into a coded Condition whose SNOMED code is the patient’s selected indication. A router PlanDefinition is then evaluated with $apply. Its three actions each carry an applicability condition (ShouldEnrollAtopic, ShouldEnrollPsoriasis, ShouldEnrollHs) defined in a CQL Library, and each names the follow-up PlanDefinition to enroll. The condition that holds determines the action that fires. The matching follow-up plan is enrolled, the intake plan is superseded, and the first questionnaire of the new program goes out on the next scheduled occurrence.
Adding a fourth indication is a new follow-up PlanDefinition, a new CQL definition, a new action on the router, and a binding entry on the customer’s Organization. It is four FHIR resources. It is not a code change, a deploy, or a regulatory re-review of changed software.
The CQL also scopes its facts to the response that triggered the run, so a patient’s historical conditions do not pollute a fresh decision. If no condition holds, or more than one does, no plan is enrolled and the patient is routed to human review instead. No response is silently dropped: the outcome is always either an enrolled plan or a flagged review.
Router proposes, application disposes
Logic as data raises an obvious question: if a PlanDefinition in the database can decide what the system does, what stops a misconfigured artifact from doing something the code never anticipated?
The answer is a bounded-action allow-list. The router does not execute anything. $apply evaluates the conditions and proposes actions, with nothing persisted. The application then reads each proposed action’s code and executes it only if the code is in the set of actions the application has implemented and validated: enroll a plan, supersede a plan, route to review. A proposed action with an unrecognised code is rejected and logged, not run.
The allow-list keeps two properties at once. The clinical decision is data, so it changes without a code change. The set of things that decision can cause is code, so a config artifact cannot make the system take an action that was never built and never reviewed. The router proposes from a fixed vocabulary; the application disposes within a boundary it controls.
What is built, and what the architecture leaves open
The engine today implements three bounded actions: enroll a follow-up plan, supersede the active one, and route a patient to human review by raising a Flag. It fires on the completion of an onboarding plan. That is enough to take a patient from a first questionnaire to the correct disease-specific monitoring program without anyone writing code per condition.
The rest of the action vocabulary is specified and not yet implemented: notify a practitioner, send a message, create a task, assign a care team, and escalate mid-treatment when a recurring score crosses a threshold. Each is a slot. Implementing one means writing the bounded action behind the code and adding it to the allow-list, once. After that, every customer’s artifacts can propose it, and which patients it fires for is a CQL definition and a router action, in data.
The expensive part is the engine itself: the code that observes a result, extracts the facts, evaluates the predicates, and disposes the proposals safely. It is built once against FHIR’s primitives. From there, extending the routing engine is a matter of filling the action slots in code and writing the rules in data. The clinical work lives in those rules, and the rules live in the database.
Backend primitives the engine rests on
The engine is generic, but it rests on backend primitives that have to exist for any of it to work. QuestionnaireResponse/$extract and PlanDefinition/$apply with CQL evaluation are clinical-reasoning operations the backend runs. CarePlan scheduling materialises the enrolled plan’s Task occurrences and transitions them as their due time arrives. FHIR Subscriptions deliver the completion event that wakes the engine. The audit log and Provenance record why each patient was routed where, by name. The access boundary at the data layer governs every read and write the engine performs.
These are the primitives Fire Arrow ships. The decision engine is an application above them, and it stays small because the backend carries the extraction, the evaluation, the scheduling, the eventing, and the audit. The delivery channel, WhatsApp in the example above, sits to the side: it collects the response and signals completion, and it is swappable for email or SMS because the engine only ever reads the FHIR resources the channel produces.
A clinical decision engine that lives in code grows a year of routing logic that the team has to defend on every audit and edit on every release. A clinical decision engine that reads its decisions from FHIR resources is built once, and the decisions change after that as data. The first is a project. The second is a property of the data.
What to read next
For the scheduling primitive the enrolled plans depend on, see the CarePlan scheduling feature and the CarePlan glossary entry. For the surveillance side of questionnaire workflows, the Questionnaire Surveillance whitepaper covers authoring, monitoring protocols, and response-based alerting. For the access boundary every part of the engine runs behind, see The FHIR Authorization Tax and the FHIR authorization (RBAC) feature. For applied workflows built on the same primitives, see ePRO on FHIR, chronic disease management, and remote patient monitoring.
References
- HL7 FHIR Structured Data Capture, Extraction
- HL7 FHIR Clinical Reasoning module, PlanDefinition
$apply - Clinical Quality Language (CQL) specification
- HL7 FHIR R4, PlanDefinition, Library, and Provenance