The extension system
Introduced by RFC-27 (opens in a new tab), the extension system enables frontend modules to insert UI elements into each other, and for these interactions to be configurable by system administrators.
Those familiar with the OpenMRS RefApp 2.x extension system (opens in a new tab) will be glad to know that the basic concepts here are similar, but simpler. “Extensions” are roughly the same thing as before, “points” are now called “slots,” and there is no longer anything like “apps,” which no one really understood anyway.
Key Concepts
The extension system posits two concepts: extensions and slots. An extension is a component. A slot is a place in the UI.
Extensions get rendered into slots. An extension gets associated with a slot in one of the following ways:
- The extension names the slot in its definition, using the
slotorslotsproperty. - A call to the attach (opens in a new tab) function.
- A system administrator adds the extension to the slot using the slot’s add array.
When to use extensions and slots
The extension system should be thought of as a system for making behavior configurable by administrators. It should not be thought of as a way to reuse components across modules.
This key question is: Am I creating a collection of similar things, such as buttons or tiles, which an administrator might want to re-order or otherwise change?
If so, this may be a good place to use extensions.
What if I just want to mount something from one framework into something in another framework?
Just use the Single SPA mountParcel (opens in a new tab) function.
What if I just want to use a component from one module in a different module, and I can change both?
Consider exporting the component and using it the normal way.
Usage
Extensions are defined in a module's routes.json file. This is the required approach for Core v5+ modules. The setupOpenMRS function was used in Core v4 and earlier but is no longer supported in Core v5+. Modules must migrate to routes.json to work with Core v5+. Each extension definition includes a name and a component reference. It may also specify the names of slots to attach the extension to by default, required privileges for role-based access control, connectivity requirements, and a number of other things (opens in a new tab), some of which will be covered below.
Slots are components. There is an ExtensionSlot (opens in a new tab) React component. If you are working in a different framework and would like to create an extension slot, please get in touch with the OpenMRS Frontend 3.0 team on Slack.
Principles
Nomenclature
Naming extensions
An extension will have a name which identifies it. That name should describe what the extension does. It should not have anything to do with where the extension will appear in the application. It has no innate sense of place.
✅ Good extension names:
- Vitals table
- User avatar
- Biometrics tile
❌ Bad extension names:
- Top bar (“top” indicates a place)
- Home page reports link (“home page” indicates a place)
- Steve (names should be descriptive)
Note: You will likely see a lot of extension and slot names which are all lowercase with dashes. This is not necessary; it is better to give extensions names that are pleasant to read. Similarly, you will see many slots suffixed with “slot.” This is also not necessary.
Naming slots
A slot will also have a name which identifies it. That name should describe the location in the app that it represents. If it describes the things that can go in it, it should only use the most general terms imaginable—things like “button” or “tile” or “widget”.
✅ Good slot names:
- Primary nav right menu
- Patient header detail box
- Form header buttons
❌ Bad slot names:
- Patient address (too prescriptive about contents)
- homepage-widgets-slot (should be
Homepage widgets) - Extra buttons (too vague)
Styling
An extension should be as agnostic as possible to the context in which it appears. This means that you should avoid defining the size of an extension. Extensions should be responsive (within reason), such that the contents will adapt to a variety of different extension dimensions.
Slots should be responsible for as much styling as generically applies to all of their contents. If all of the extensions in a slot should have a border, the slot should apply the border. The slot should also be responsible for setting the dimensions into which the extensions will render.
A slot can apply styles to an extension with the following CSS selector:
.slot > * > * {
...;
}Extension configurability
The beautiful thing about configurability in the extension system is that you don't need to think about it. Extensions and slots have a standard configuration interface that allows administrators to add, remove, and re-order extensions, as well as configure extension-specific settings within a particular slot.
You can use useConfig as usual within an extension.
The schema for an extension can be specified using defineExtensionConfigSchema. If no schema is defined specifically for your extension, the extension will inherit the configuration of the module that contains it.
Role-based access control (RBAC)
Extensions support role-based access control (RBAC) through privileges. You can declare required privileges directly in the extension definition in routes.json, and administrators can override or refine these requirements via configuration.
Declaring privileges in routes.json:
{
"extensions": [
{
"name": "vitals-widget",
"component": "vitals",
"slot": "patient-header-slot",
"privileges": ["View Vitals"]
}
]
}The privileges property can be a single string or an array of strings. If an array is provided, the user must have all of the specified privileges to see the extension. If a single string is provided, the user must have that specific privilege. If the user doesn't have the required privileges, the extension will not be rendered. Note: Users with the "System Developer" role bypass privilege checks.
Overriding privileges via configuration:
Administrators can override the extension's default privilege requirements using the Display conditions configuration object (described in the next section). This allows fine-tuning access control without modifying code.
Display conditions
Every extension automatically has a Display conditions configuration object available, which allows administrators to control when extensions are displayed without modifying code. Display conditions are evaluated during the filtering phase before extensions are rendered. This is particularly useful for fine-tuning the role-based access control requirements set in routes.json or implementing implementation-specific access policies.
The Display conditions object supports the following properties:
-
privileges(array of strings): Overrides the extension's default privileges requirement declared inroutes.json. If specified, the user must have all of these privileges to see the extension. If not specified, the extension's defaultprivilegesproperty from its definition is used. This allows administrators to customize access control per implementation without code changes. Note: Users with the "System Developer" role bypass privilege checks. -
expression(string): A boolean JavaScript expression that must evaluate totruefor the extension to display. The expression has access to asessionobject containing:session.authenticated- whether the user is authenticatedsession.user- the logged-in user object (with properties likeuuid,username,systemId,privileges,roles, etc.)session.sessionLocation- the current session locationsession.currentProvider- the current providersession.locale- the current locale
-
online(boolean): Overrides whether the extension displays when the browser is online. If not specified, the extension's defaultonlineproperty is used. -
offline(boolean): Overrides whether the extension displays when the browser is offline. If not specified, the extension's defaultofflineproperty is used.
Example configuration:
{
"@openmrs/esm-patient-chart": {
"extensionSlots": {
"patient-header-slot": {
"configure": {
"vitals-widget": {
"Display conditions": {
"privileges": ["View Vitals", "Manage Vitals"],
"expression": "session.user.systemId !== 'admin' && session.sessionLocation?.uuid === 'some-location-uuid'",
"online": true,
"offline": false
}
}
}
}
}
}
}This configuration would:
- Require the user to have both "View Vitals" and "Manage Vitals" privileges
- Hide the extension for users with systemId "admin"
- Only show the extension when the session location matches a specific UUID
- Only display when online
UI Editor
The Implementer Tools app includes a UI Editor feature that provides a visual interface for configuring extensions. When enabled, it displays overlays on extension slots and extensions throughout the application, allowing administrators to:
- Click on slots to configure which extensions appear in them
- Click on extensions to configure their settings within a specific slot
- Visually identify where extensions are rendered
- See extension counts and metadata at a glance
The UI Editor uses the data-extension-slot-name and data-extension-id attributes that are automatically added to slot and extension DOM elements. The position: relative styling on extension containers enables the UI Editor to position its overlay elements correctly.
To enable the UI Editor, open the Implementer Tools (usually via a keyboard shortcut or menu) and toggle the "UI editor" switch in the configuration panel.
State
Sometimes, extensions are not as independent as we might wish they were, and have to expect some state from the slot in which they are mounted. Most commonly, extensions that pertain to a specific patient will accept a patientUuid parameter which can be used to fetch relevant patient information.
State is provided as a parameter to the ExtensionSlot or Extension components, and received as a prop by the extension.
Using the Extension component:
When you provide custom children to ExtensionSlot (either as a React node or a function), you must use the <Extension /> component to mark where each extension should be rendered. The Extension component is a React helper that renders a single extension instance within a slot.
import { ExtensionSlot, Extension } from "@openmrs/esm-react-utils";
// With custom wrapper
<ExtensionSlot name="patient-header-slot">
<div className="custom-wrapper">
<Extension />
</div>
</ExtensionSlot>
// With function children
<ExtensionSlot name="dashboard-slot">
{(extension) => (
<div className={`widget-${extension.meta?.size}`}>
<Extension state={{ patientUuid: "123" }} />
</div>
)}
</ExtensionSlot>If you don't provide children to ExtensionSlot, the Extension component is automatically rendered for each extension.
See the ExtensionSlot API docs (opens in a new tab) for more.
Meta
Sometimes, extensions might want to pass information to the slot that receives them. This is used, for example, by patient chart widgets. Dashboards render these widgets into a grid format. When a dashboard receives a widget, the widget informs the dashboard (which is a slot) how many grid columns it would like to take up. This happens using meta.
Meta is provided by extensions in their definition in routes.json. (In Core v4 and earlier, this was done in the setupOpenMRS function, but that approach is no longer supported in Core v5+.)
Slots can access meta through the extension system API, such as by using useExtensionSlotMeta (opens in a new tab).
Order
By default, extensions render in the order they are declared or attached. The final order is determined by a priority system with three tiers:
- Configured order (highest priority): Extensions listed in the slot's
orderconfiguration array appear first, in the order specified in that array. - Registered order: Extensions with an
orderproperty in theirroutes.jsondefinition appear next, sorted by their order value (offset by 1000 to ensure they come after configured extensions). - Attached order (lowest priority): Extensions without any order specification appear last, in the order they were attached (offset by 2000 to ensure they come after all ordered extensions).
Extensions added by administrators via the add array follow the same ordering rules.
Extensions can provide an order index in their definition to influence the order in which they are rendered. This works like z-index (opens in a new tab) in CSS—similarly, it is a way of setting relative order among elements that don't officially know about each other.
Administrators can also override the sort order using the order array alongside add/remove arrays in the slot configuration. The runtime automatically processes these configuration changes to determine the final order of extensions.
Feature flags
Extensions can be conditionally rendered based on feature flags. This allows you to gradually roll out new features or enable experimental functionality for specific implementations.
Declaring feature flags in routes.json:
{
"extensions": [
{
"name": "experimental-widget",
"component": "experimental",
"slot": "dashboard-slot",
"featureFlag": "experimental-feature"
}
]
}If the featureFlag property is specified, the extension will only render when that feature flag is enabled. Feature flags can be toggled directly from the Implementer Tools app, which provides a visual interface for enabling and disabling feature flags. Once a feature flag is registered (via registerFeatureFlag), it automatically appears in the Implementer Tools with a toggle switch, allowing administrators to enable or disable it without code changes.
Advanced slot rendering
The ExtensionSlot component supports advanced rendering patterns for customizing how extensions are displayed.
Filtering extensions with the select prop:
You can filter which extensions are rendered using the select prop, which accepts a function that receives the array of assigned extensions and returns a filtered array:
<ExtensionSlot
name="patient-header-slot"
select={(extensions) => extensions.filter(ext => ext.meta?.priority === 'high')}
/>Custom rendering with function children:
You can customize how each extension is rendered by passing a function as children. The function receives the extension object and any state passed to the slot:
<ExtensionSlot name="dashboard-slot">
{(extension, state) => (
<div className={`widget widget-${extension.meta?.size || 'medium'}`}>
<Extension state={state} />
</div>
)}
</ExtensionSlot>The extension object (of type AssignedExtension) includes properties like name, id, meta, moduleName, config, online, offline, and featureFlag, allowing you to create sophisticated layouts based on extension metadata.
Accessing extension context:
Each extension receives an _extensionContext prop containing:
extensionId: The unique identifier for this extension instanceextensionSlotName: The name of the slot this extension is rendered inextensionSlotModuleName: The module that owns the slotextensionModuleName: The module that provides the extension
This context is useful for debugging, logging, or conditional logic that depends on where an extension is rendered.
Dynamic slot management
While most extensions are attached to slots declaratively via routes.json or administrator configuration, you can also manage slot attachments programmatically at runtime.
Attaching extensions dynamically:
import { attach } from "@openmrs/esm-framework";
// Attach an extension to a slot at runtime
attach("patient-header-slot", "vitals-widget");
// Attach the same extension multiple times with different IDs
attach("patient-header-slot", "vitals-widget#instance1");
attach("patient-header-slot", "vitals-widget#instance2");Extension IDs can include an optional # suffix to distinguish multiple instances of the same extension within a slot. For example, "vitals-widget#primary" and "vitals-widget#secondary" are two distinct instances of the "vitals-widget" extension.
This is useful for:
- Dynamic slots that are created at runtime (e.g., workspace slots)
- Implementation-specific logic that conditionally shows extensions
- Temporary extensions that should only appear under certain conditions
- Rendering the same extension multiple times in the same slot with different configurations
Detaching extensions:
Deprecated: The detach and detachAll functions are deprecated. Extension attachments should be considered declarative. Use configuration (remove array) or avoid attaching extensions in the first place rather than detaching them at runtime.
import { detach, detachAll } from "@openmrs/esm-framework";
// Detach a specific extension from a slot (deprecated)
detach("patient-header-slot", "vitals-widget");
// Detach all extensions from a slot (deprecated)
detachAll("patient-header-slot");Querying assigned extensions:
You can programmatically get the list of extensions assigned to a slot:
import { getAssignedExtensions } from "@openmrs/esm-framework";
const extensions = getAssignedExtensions("patient-header-slot");
// Returns an array of AssignedExtension objects with properties:
// - id: unique extension instance ID (may include # suffix)
// - name: extension name
// - moduleName: the module that provides the extension
// - meta: extension metadata object
// - config: extension configuration (null until slot is mounted)
// - online, offline: connectivity flags
// - featureFlag: optional feature flag nameThis is useful for building custom slot implementations or debugging extension assignments.
How slots and extensions register themselves
Behind the scenes, slots call registerExtensionSlot(moduleName, slotName) when they mount (the React-friendly <ExtensionSlot> helper does this for you). This tells the runtime which module owns the slot and seeds the internal store (registerExtensionSlot lives in @openmrs/esm-extensions).
Extensions declared in routes.json are registered via registerExtension. (The setupOpenMRS() approach from Core v4 and earlier is no longer supported in Core v5+.) Attaching slot/slots fields in the same definition is the declarative way to populate attachedIds; calling attach(slotName, extensionId) gives you imperative control (useful for dynamic slots or implementation-specific tweaks). Once registered/attached, the runtime merges administrator config, feature flags, privileges, and online/offline constraints before rendering the assigned extensions.
Meta and slot APIs
Slots can read meta through useExtensionSlotMeta() and use it to adjust layouts—dashboards read column requests, patient headers read badge sizes, etc. Because meta is just another part of the definition, it’s merged with slot config overrides through the same getAssignedExtensionsFromSlotData logic.
For hooks into slot state if the default helpers are not enough, consult registerExtensionSlot, renderExtension, and getAssignedExtensions in @openmrs/esm-extensions, and useExtensionSlotMeta in @openmrs/esm-react-utils, so you understand how the stores, configs, and connectivity checks interact.
Troubleshooting
If your extension isn’t showing up:
- Double-check
routes.json— thecomponentname must match the named export fromsrc/index.ts. - Confirm the target slot exists and is rendered (look for
<ExtensionSlot name="..." />in the host module). - Unless you explicitly need runtime control, bind the slot in
routes.jsonrather than usingattach. - Slot configuration can override order: administrators can reorder entries via the slot’s
orderarray.
Worked example
-
Export the extension component in
src/index.ts:import { getSyncLifecycle } from "@openmrs/esm-react-utils"; import { createLeftPanelLink } from "./left-panel-link"; export const patientListLink = getSyncLifecycle( createLeftPanelLink({ name: "patient-lists" }), { featureName: "patient-lists-link", moduleName: "@openmrs/esm-patient-lists", } );Note: The
getSyncLifecyclewrapper is required to convert your React component into an extension-compatible lifecycle. The options object must includefeatureNameandmoduleName. -
Add an entry to
routes.json:{ "extensions": [ { "component": "patientListLink", "slot": "patient-lists-dashboard-slot", "name": "patient-lists-home-link", "order": 4 } ] } -
Ensure a host renders the slot, e.g.,
<ExtensionSlot name="patient-lists-dashboard-slot" />. -
Admins can later tweak the slot's
orderarray or useadd/removearrays via the configuration system to control which extensions appear and in what order. -
To reuse the same extension in another slot, either call
attach("another-slot", "patient-lists-home-link")at runtime, or add the extension to the other slot'saddarray in the configuration.
Additional Resources
Short introductory videos:
- OpenMRS Frontend 3 Extension System 1 - Basics (opens in a new tab)
- OpenMRS Frontend 3 Extension System 2 - State and Meta (opens in a new tab)
- Introductory presentation: Quick Guide to Slots (opens in a new tab)
For a terse technical description of the extension system, see the Extensions RFC (opens in a new tab).
See the App shell deep dive (opens in a new tab) for the runtime wiring that powers slot registration, extension rendering, and the parcel lifecycle.
Workshop
A live workshop was hosted on Zoom, providing a comprehensive introduction to the extension system, as well as practical problems. Recordings and materials are available below.
- Part 1: About our Frontend Module Architecture & How to Use Extensions (opens in a new tab)
- Part 2: Practical Session on our MFE Architecture & How to Use Extensions (opens in a new tab)
How the extension system works
For the extension system to work four things exist:
- A generic component model with a defined lifecycle and loading mechanism
- A way to define where extensions should be placed (so called “slot”)
- A way to define an extension coupling it to (1)
- A configuration for assigning available extensions from (3) to slots (2)
Let’s explore these four things in depth.
Behind the Scenes
For (1), extensions are implemented using single-spa parcels (opens in a new tab).
For (2) you can use the registerExtensionSlot() function together with renderExtension(). For frameworks such as React, helper components may exist (e.g., ExtensionSlot).
For (3), you define extensions in your application's routes.json file (recommended for Core v5+). An example:
{
"extensions": [
{
"name": "foo",
"component": "fooComponent"
}
]
}Note: fooComponent is the name of the export defined in src/index.ts.
As a shorthand for (4), you can specify a target slot via the slot property in the extension definition. Alternatively, you can attach extensions programmatically using attach:
// attaches an extension "foo" to a slot "foo-slot"
attach("foo-slot", "foo");Generally, slot assignment is done at initialization time as a default (via the slot property in routes.json), or explicitly via administrator configuration. The exception is "dynamic" slots that are created at runtime, such as workspace slots in the patient chart module.
Extensions and Slots
An extension can be in any of the following four states with respect to an extension slot:
- attached: Set via code using
attach()(or declaratively via theslot/slotsproperty inroutes.json). Note:detach()is deprecated. - configured: Set via administrator configuration using the
addandremovearrays - assigned: Computed by merging attached and configured extensions, then filtering based on privileges, feature flags, and connectivity (
online/offlineflags) - connected: (Deprecated term) This is the same as "assigned" after filtering—extensions that are actually rendered
The runtime uses the "assigned" state to determine which extensions to render. The filtering happens automatically based on user privileges, enabled feature flags, and connectivity state.
Rendering
Extensions are rendered by following their exported lifecycle functions. The getAsyncLifecycle and getSyncLifecycle functions from @openmrs/esm-react-utils are convenience layers that export these lifecycle functions wired together with single-spa-react. Use getAsyncLifecycle for components that need to be lazy-loaded, and getSyncLifecycle for components that are already available synchronously.
In a nutshell:
- When the component should be rendered, the
loadfunction is evaluated. If it returns aPromise(via an asynchronously loadedimport()), the system waits for the component to be available. - The component is wrapped with lifecycle functions provided by
single-spa-react. - The lifecycle functions (
bootstrap,mount,unmount, andupdate) are exported and managed by Single-SPA.
These lifecycle functions are not magic - theoretically you could write them on your own, however, since the single-spa ecosystem already provides convenience wrappers such as single-spa-react for many frameworks we don’t recommend it.
Before rendering, two additional factors are evaluated:
- Connectivity mode: Whether the extension should render based on the current online/offline state
- Component props: What data and services should be passed to the rendered component
Connectivity mode is determined by checking the browser's connectivity status via isOnline() from @openmrs/esm-utils. This function first checks if window.offlineEnabled is true; if not, it assumes the app is always online. If offline mode is enabled, it then checks navigator.onLine to determine the actual connectivity state.
The extension's online and offline flags control whether it renders in each mode:
- If
online: false, the extension will not render when the browser is online - If
offline: true, the extension will render when the browser is offline - By default, extensions render online (
online: true) and do not render offline (offline: false)
Note: While the type system allows online and offline to be objects, this feature is not currently implemented in the rendering logic. Use boolean values.
Component props include:
_meta: The extension's metadata object_extensionContext: Information about the extension and its slot (extension ID, slot name, module names)- Any additional props passed to the
ExtensionSlotcomponent (via thestateprop)