I’m working on the Procedures app for O3 (requirements: Procedure History) and would like feedback on the requirements and the data model before implementation.
Planned Fields
The app will handle both historical procedures (patient intake) and current procedures (documented at facility):
Core fields:
Procedure name (coded)
Custom procedure name (text) - fallback for unlisted procedures
Procedure start datetime
Procedure end datetime (null for historical procedures)
Actual duration (number)
Duration unit (coded: minutes/hours)
Outcome (coded)
Outcome comment (text, 255 chars)
Form Design
Planning to use a single workspace form with a toggle to switch between “Historical” and “Current” modes. This will adjust field requirements and validation:
You’re right that we don’t have any pattern for approximate dates. Probably the most straight-forward thing to do is to actually store dates as ISO 8601 text strings. This allows representations like “2018”, “2018-01”, “2018-01-01T134123-0530.” This, of course, makes parsing and handling these fields a bit of an issue.
With birthdates, we store a full datetime and a boolean indicating if it’s estimated or not. This is obviously problematic because it’s unclear if it’s estimated “20 years” or “3 years, 8 months” or “sometime in April 2000”. I’m not sure we how we handle this in Conditions, the other domain where this is quite relevant.
As a general rule, they should be FormRecordable (an interface in core) which implies they are tieable to an encounter (just like Conditions). However, this field should be optional.
At PIH, we often (especially for surgical procedures) collect pre and post surgery diagnosis related to the procedure. (These diagnoses aren’t recorded as conditions.)
I don’t know if this would be standard for clinicians or data-use, but perhaps this is useful to the discussion.
I like the idea of making this optional because trying to tie a historical procedure to an encounter might be a tall order. But for the current procedures, it’s more likely happening during an encounter and thus linking makes sense.
Good point @ball. KenyaEMR also captures order reason (free text), but I can also see they have a diagnosis field, which I am not quite sure how it’s captured … @dkibet@kmuiruri please chime in
@jayasanka i am very sure that you must have discussed this. But just for the sake of completeness and documentation, what were the considered reasons to create a new dedicated table for procedures instead of using an obs group?
Thanks again for pointing this out @dkayiwa. The implementation can be done either using Obs Groups or a dedicated table. Both approaches come with balanced pros and cons, let me summarize the conversation I had with @ibacher and my observations here:
Option A: Obs Groups
Pros:
Leverages existing Obs querying infrastructure
Follows established codebase patterns
Cons:
More validation code due to the metadata layer
Complex querying (must traverse Obs tree with joins)
Performance overhead vs flat table
Requires ~15 concepts to be created
More complex data model (parent Obs + child Obs members)
Harder to write direct SQL queries
Option B: Dedicated Table
Pros:
Much simpler querying (flat structure, single SELECT)
Better performance for large datasets (no joins)
Explicit schema, easier to understand, columns clearly defined
Direct SQL access for reporting
Easy to add indexes on any field
Easier to add constraints (NOT NULL, foreign keys)
Cons:
Need liquibase migration to create table
need to maintain DAO layer, CRUD operations
Schema changes require migrations
Obs Groups add about ~30–40% more validation code because of the extra metadata layer needed to build and parse Obs tree structures. On the other hand, validation for a dedicated table is more straightforward, with many rules handled directly at the database level.
This entity is expected to have around 15 fields, and the validation logic also depends on whether the linked procedure is historical or not.
Both options support audit trails and form integration, so those factors don’t really sway the decision. OpenMRS already uses both patterns: for example, conditions and allergies use dedicated tables, while diagnoses rely on Obs Groups.
I lean slightly toward Option B due to the simplicity and performance benefits. However, after evaluating both approaches the difference feels marginal. Either option would be reasonable depending on team preference and long-term maintenance considerations.
another approach would be adding a field called estimatedDateText (String)
It can hold free text like “around 2020 Feb” or “earlier 2019”. We’d still store and treat these as proper dates for sorting and logic, but show this text next to startDateTime in the UI so it’s clearly indicated to the end user that the date is relative.
But I like your approach more, and this pattern is already encouraged by FHIR with partial dates. Datatypes - FHIR v5.0.0
Updated fields:
? = pending decisions
Field Name
Type
Required (Historical)
Required (Current)
Description
uuid
UUID
Yes
Yes
Unique identifier
patientId
UUID
Yes
Yes
Reference to the patient
procedure
CodedOrFreeTextAnswer
Yes
Yes
Coded or free-text
bodySite
Concept
Yes
Yes
Coded body site
startDateTime
Date or Partial Date?
Yes
Yes
When the procedure started; used for sorting
estimatedDateText?
String
No
No
Free-text date like “around 2020 Feb” or “earlier 2019”; presence indicates a historical procedure and is displayed next to startDateTime in the UI
endDateTime
Date
No
No
When the procedure ended
duration
Integer
No
Yes?
Duration of the procedure
durationUnit
Enum or Concept or leverage unit column if going with obs
No
Yes?
Unit for duration
encounter
Encounter reference
No
Yes
Associated encounter
outcome
CodedOrFreeTextAnswer
Yes?
Yes
Coded or free-text outcome
notes
String
No
No
Free-text notes
The following fields will be inherited by FormRecordable interface:
form_namespace
form_field_path
The main weight of this requirement is collecting historical procedures. So to keep the first iteration simple, I suggest we defer decisions on the following fields (even though they’re important) to a next iteration. That way we can get some real usage insights after an initial cycle:
I see… Thanks for pointing that out, Daniel. I was under the wrong impression. Now that I went through the codebase, I see that it was actually moved to a dedicated table from the obs groups a while ago. Any idea what drove that decision? It’s definitely going to be a key input.
@mseaton@mogoodrich I’m guessing PIH stores procedures using obs groups. Can you confirm? If so, could you point me to the specific repo and let me know which of the two options you’d prefer?
These guys usually started their life in the emrapi module as obs groups and then eventually moved to core when they became stable. Jira
One other thing that i had not mentioned is that when captured as obs, these guys took advantage of the existing form entry technologies which deal with observations. With a custom table structure, you would need to put some effort into how to collect data for them.
Another point to note is that with obs, you do not require any particular version of openmrs to take advantage of this functionality because all that you are dealing with is specific obs groups that get configured. If you iterate on this new table structure from a module instead of core, then you only require that module.
@jayasanka i said all the above not to sway you in any particular direction, but just for the sake of completeness.
At PIH we use obs group (or maybe just obs), mainly because that is the primary, default way to collect clinical data. All the points that @dkayiwa makes are very valid as well–obs are well-supported in form technologies and historically in OpenMRS. If we add procedures as a first-class citizen, we will at some point need to build support in the form technologies, etc, to capture procedures
It’s worth noting that at PIH we still collect diagnoses as obs because we haven’t carved out the time to migrate our existing obs group based diagnoses to the first-class diagnoses dedicated table
That being said, I do think there is a merit in making Procedures a first-class citizen, as we have done for Conditions and Diagnoses. I think we should lean heavily on what FHIR does… first, the fact that Procedure is a first-class citizen in FHIR is an argument for supporting it in OpenMRS. I would also lean heavily on modelling it the way FHIR does, see ( Procedure - FHIR v5.0.0) My general pattern when implementing other domain objects (specifically “Medication Dispense”) was to model as closely to FHIR as possible but don’t worry about modelling every FHIR field, just the ones we know we need in the short-to-mid-term to meet our requirements. (flagging @ibacher in case he disagrees with any of this)
We definitely want to link to encounter, optionally (which is what FHIR does)
Based on what you both shared, my takeaway so far is that both approaches are valid depending on maturity and scope. Another angle is how much the data is expected to change over time: if something is likely to go through revisions, a first-class model tends to be a better fit. If it’s more of a point-in-time capture that rarely gets edited, obs groups make a lot of sense.
That said, if Procedures are something we expect to be queried, reasoned about, and integrated externally, a first class model feels like the right direction to explore. The fact that Procedure is modeled explicitly in FHIR is a meaningful signal there.
Happy to keep iterating on this. the discussion itself is already shaping the design nicely…
@jayasanka is this a feature that you are going to implement in openmrs-core 3.0 and hence make it the required platform version required to access this functionality?
So, I would recommend including status (± status_reason) and patient so we’re aligned with FHIR.
Your procedure name (presumably a concept id) aligns with FHIR’s Procedure.code. Ideally, custom name would be required iff the procedure is set to a specific procedure concept like “Procedure, non-coded” … but I couldn’t fiend a CIEL concept for this.
Recording procedure start and optional end time makes sense, but…
I don’t understand the end datetime comment “null for historical procedures”. I would expect end datetime to be missing if the procedure is still ongoing or if it wasn’t recorded.
Do we need duration? Couldn’t this be determined from start & end times?
Other properties I would include:
performer(s) – who performed the procedure. Ideally, would support multiple performers and a function/role for each.
note (a text field to document details of the procedure).
I shared a proposal for better date/time support a couple of years ago that would help with partial dates, including allowing capturing a procedure date like “2018”. Like @ibacher suggested, it uses ISO 8601 dates for the actual data and then allows for a date (or datetime) to hold an interpretation (like 2018-01-01) to be used when an actual date/time is needed. I would love to see us start adopting a convention like this (as long as we are consistent as we adopt it instead of re-inventing a new approach in each case).