🧠 Problem State Processor
The ProblemStateProcessor
class manages and processes state transitions for problem goals. It determines the next state, updates it, creates interventions, and triggers any associated side effects.
All it's decisions are based in the protocol definition. All state evaluations are delegated to the ProtocolDefinition
.
⚙️ Overview
Purpose
The processor is called whenever an event might trigger a transition in a problem goal state. It:
- Handles both manual and automatic state transitions (evaluates whether we need to transition to a new state or not)
- Updates the problem goal state (which also creates a new problem state transition)
- Creates interventions based on the new state
- Executes side-effect callbacks for manual transitions
- Logs the process and errors
Key Features
- Manual and Automatic Transitions: Supports both user-initiated (manual) and system-initiated (automatic) state transitions.
- Intervention Creation: Automatically creates interventions based on the new state.
- Dynamic Field Resolution: Resolves dynamic fields for due dates and custom fields.
- Error Handling: Reports errors to the logging system and gracefully handles failures.
- Dry Run Support: Allows testing transitions without making actual changes.
🔁 process
Method
Description
This is the entry point. Called to evaluate if we need to transition the problem state, and if so, execute the transition effects (e.g. create interventions)
Parameters
problem_goal
(required): the problem goal to process.new_state
(optional): manually selected state. Used from the front end state dropdown.transition_reason
(optional): reason for manual transition. Filled by the user that is changing the state manually.dry_run
(optional): if true, doesn't persist changes. This is useful to make sure the outcome is what we expect before affecting actual data. Used in backfill scripts.fetcher
(optional): data fetcher instance. Used by the transition's conditions to decide if we should move to a certain state.problem_protocol
(optional): protocol to evaluate against.skip_interventions
(optional): skips creating interventions. Useful for backfill scripts, in which we might want to set the state, but not create interventions associated with the state.trigger_source
(optional): the source of the trigger for the transition. Used for logging purposes.force_intervention_creation
(optional): creates interventions even if the state hasn't changed.
Returns
true
if processing succeeds.false
if an error occurs.
⚡ When Is It Triggered?
The process
method runs in several scenarios:
- ✅ When a PHQ-9 form is completed by the patient. Calls the process asyncronically for every active problem goal of that patient with an enabled problem type
- ✅ When an Intake form is completed by the patient. Calls the process asyncronically for every active problem goal of that patient with an enabled problem type
- ✅ On intervention completion. Calls the process synchronically for all problem goals linked to that intervention
- ✅ When a problem is created (initial state, no interventions)
- ✅ When a problem is started (creates interventions)
- ✅ When running
ProblemProtocolEvaluatorJob
- ✅ When a reading is created. Calls the process synchronically for all problem goals with a problem type configured for the reading type (
A1C Reading types
andBP Reading types
)
🧩 Intervention Creation
Interventions are automatically created when:
- The state changes and the problem is
in_progress
- A problem is started, even if the state hasn't changed yet
- There are
interventions to create
. This logic is based on:- the
skip_interventions
processor parameter - the
force_intervention_creation
processor parameters - the new state being diferent than the old state
- the state
always_create_interventions_for
configuration - the state's interventions
always_create_for
configuration
- the
Deduplication is handled using:
deduplication_key
deduplication_params
deduplication_resolver
🔁 Callbacks
Manual transitions can trigger callbacks.
Example: In the housing protocol, changing the state manually updates the patient's housing_status
.
🎯 Operations
The processor currently supports different operations for each intervention inside a state. The supported operations are:
- Create: This is the default operation. When the processor processes a problem goal and there are interventions to create, it attempts to create every intervention with this operator if the deduplication checks don't find an existing match. If there is a match, no action is taken.
- Update: When the processor processes a problem goal and there are interventions to create, it checks with the deduplication logic whether a matching intervention exists. If one is found, it updates the intervention with the fields defined in the intervention. If no match is found, no action is taken.
- Upsert: This is a combination of the previous two operations. The processor first checks for matches based on deduplication logic. If an intervention is found, it updates it. If no match is found, it creates a new one.
⏳ Dynamic Values
Some values can't be hardcoded in the protocol definition and must be resolved at runtime.
- Dates: Expressed in
unit
.unit_type
. Examples:1.week
,3.months
- Dynamic values: Use the
dynamic#
prefix and are resolved by theDynamicFieldResolver
. In that file you can find the currently supported dynamic resolvers - Deduplication resolvers: Use the
deduplication_resolver
key. Supported resolvers and their implementation can be foundhere
🚨 Error Handling
If processing fails:
- Error is reported to
Rails.error.report
- The
Collector
logs and finalizes the trace - The method returns
false