From Template to Working Form: The Gap Nobody Explains
openEHR promises that clinical data models drive everything — including the user interface. Define your archetypes, compose them into templates, and forms should "just work." In practice, the journey from an Operational Template (OPT) to a working clinical form that clinicians actually want to use involves tooling choices, framework trade-offs, and integration work that the specification does not cover.
This guide walks through the three main approaches to building clinical forms from openEHR templates: Medblocks web components, the Better Platform Forms SDK, and custom implementations using the web template format. We cover what works, what does not, and where each approach fits.
Understanding the Template-to-Form Pipeline
From Archetypes to Operational Templates
An Operational Template (OPT) is the compiled, deployment-ready artifact that combines multiple archetypes with additional constraints specific to your use case. For example, you might have archetypes for blood pressure, body temperature, and pulse rate. An OPT for "Vital Signs Encounter" combines all three, constrains which fields are visible, sets default values, and defines the clinical context.
OPTs are created using template design tools like the openEHR Template Designer or Ocean's tools. The output is an XML file (.opt) that your CDR consumes.
Web Template Format: The Bridge to UI
When you upload an OPT to a CDR like EHRbase or Better Platform, the CDR generates a web template — a JSON representation of the template that is designed for UI consumption. The web template contains everything a form builder needs:
- Tree structure — The hierarchy of sections, observations, and individual data points.
- Data types — Whether each field is a quantity, coded text, date, free text, etc.
- Constraints — Valid ranges, cardinality (required vs. optional, single vs. repeating), allowed coded values.
- AQL paths — The path to each field used for data binding and querying.
- Terminology — Human-readable labels, descriptions, and coded value options.
Fetch a web template from EHRbase:
# Get web template from EHRbase
curl -s "https://ehrbase.example.com/ehrbase/rest/openehr/v1/definition/template/adl1.4/vital_signs_encounter" \
-H "Accept: application/openehr.wt+json" | python3 -m json.tool
# Response structure (simplified):
{
"templateId": "vital_signs_encounter",
"version": "1.0.0",
"defaultLanguage": "en",
"tree": {
"id": "encounter",
"rmType": "COMPOSITION",
"children": [
{
"id": "blood_pressure",
"rmType": "OBSERVATION",
"children": [
{
"id": "systolic",
"rmType": "DV_QUANTITY",
"aqlPath": "/content[at0001]/data[at0002]/events[at0003]/data[at0004]/items[at0005]",
"inputs": [{"type": "DECIMAL", "validation": {"range": {"min": 0, "max": 1000}}}],
"annotations": {"unit": "mm[Hg]"}
}
]
}
]
}
} Approach 1: Medblocks Web Components
What Medblocks Provides
Medblocks UI is an open-source library of web components designed specifically for openEHR data entry. Each component maps to an openEHR data type and handles serialization to and from the flat JSON composition format.
Key components include:
<mb-form>— Container that manages form state, serialization, and submission.<mb-input>— Free text input, maps to DV_TEXT.<mb-quantity>— Numeric input with unit, maps to DV_QUANTITY.<mb-select>— Dropdown for coded values, maps to DV_CODED_TEXT.<mb-date>— Date picker, maps to DV_DATE_TIME.<mb-boolean>— Checkbox, maps to DV_BOOLEAN.<mb-repeat>— Repeating section for 0..* cardinality fields.
Building a Form with Medblocks
Here is a complete blood pressure form using Medblocks components:
<!-- Blood Pressure Form with Medblocks -->
<mb-form id="bp-form">
<h3>Blood Pressure Recording</h3>
<mb-context path="encounter/context/start_time"></mb-context>
<mb-context path="encounter/context/setting" value="238"></mb-context>
<label>Systolic (mmHg)</label>
<mb-quantity
path="encounter/blood_pressure/any_event/systolic"
unit="mm[Hg]"
min="0" max="1000"
required
></mb-quantity>
<label>Diastolic (mmHg)</label>
<mb-quantity
path="encounter/blood_pressure/any_event/diastolic"
unit="mm[Hg]"
min="0" max="1000"
required
></mb-quantity>
<label>Body Position</label>
<mb-select path="encounter/blood_pressure/any_event/position">
<mb-option value="at1000" label="Standing"></mb-option>
<mb-option value="at1001" label="Sitting"></mb-option>
<mb-option value="at1003" label="Lying"></mb-option>
</mb-select>
<button type="submit">Save</button>
</mb-form>
<script type="module">
import '@nicknisi/medblocks-ui';
const form = document.getElementById('bp-form');
form.addEventListener('mb-submit', async (e) => {
const composition = e.detail; // Flat JSON composition
const response = await fetch(
`${CDR_URL}/ehr/${ehrId}/composition?templateId=vital_signs_encounter`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(composition)
}
);
});
</script> Strengths and Limitations
Strengths: Open source, CDR-agnostic, low learning curve, good developer experience. Components are framework-agnostic (works with React, Vue, Angular, or plain HTML). Data binding to flat JSON paths is intuitive.
Limitations: Limited to data entry forms — no built-in support for complex clinical workflows (order entry, multi-step wizards). Styling customization requires CSS overrides. The component library covers common data types but may lack specialized clinical components (body site selectors, pain scales).
Approach 2: Better Platform Forms SDK
The Better Ecosystem
Better Platform provides a commercial openEHR CDR with an integrated forms toolkit. The Better Forms SDK includes a drag-and-drop form designer and a rendering engine that generates forms from templates.
The workflow is:
- Upload OPT to Better Platform.
- Open the Form Designer — a web-based visual editor that shows the template structure.
- Drag fields onto the canvas — arrange fields, set labels, configure validation, add conditional visibility rules.
- Preview and test — the designer generates a live preview of the form.
- Deploy — the form configuration is stored alongside the template and rendered at runtime by the Better Forms engine.
Form Configuration Example
// Better Forms SDK - form configuration (simplified)
{
"templateId": "vital_signs_encounter",
"formId": "vitals-entry-v1",
"layout": {
"type": "columns",
"columns": [
{
"width": "50%",
"fields": [
{
"path": "encounter/blood_pressure/any_event/systolic",
"label": "Systolic BP",
"widget": "number-input",
"validation": { "required": true, "min": 0, "max": 300 },
"style": { "highlight-if": "value > 140", "highlight-color": "#FF6B6B" }
},
{
"path": "encounter/blood_pressure/any_event/diastolic",
"label": "Diastolic BP",
"widget": "number-input",
"validation": { "required": true, "min": 0, "max": 200 }
}
]
},
{
"width": "50%",
"fields": [
{
"path": "encounter/body_temperature/any_event/temperature",
"label": "Temperature",
"widget": "number-input",
"unit-selector": true
}
]
}
]
}
} Strengths and Limitations
Strengths: Visual form designer reduces development time significantly. Integrated with Better CDR — forms auto-sync with template changes. Support for conditional logic, calculated fields, and multi-step workflows. Production-tested in large health systems.
Limitations: Vendor lock-in to Better Platform. The Forms SDK is not open source. Customization beyond the provided widgets requires Better professional services. Pricing is per-deployment, not per-developer.
Approach 3: Custom Form Building
When Custom Makes Sense
Custom form building is the right choice when: your UX requirements exceed what Medblocks or Better provides, you need deep integration with your existing frontend framework, you have specialized clinical workflows (multi-step order entry, complex conditional logic), or you want full control over the rendering pipeline.
Working with the Web Template Format
The web template JSON is your starting point. Parse it to generate form fields programmatically:
// TypeScript: Parse web template to generate form fields
interface WebTemplateNode {
id: string;
rmType: string;
name: string;
localizedName?: string;
aqlPath: string;
min: number;
max: number;
inputs?: Array<{ type: string; validation?: any; list?: any[] }>;
children?: WebTemplateNode[];
}
function generateFormFields(node: WebTemplateNode, parentPath: string = ""): FormField[] {
const currentPath = parentPath ? `${parentPath}/${node.id}` : node.id;
const fields: FormField[] = [];
// Leaf node = actual form field
if (!node.children || node.children.length === 0) {
fields.push({
path: currentPath,
label: node.localizedName || node.name,
type: mapRmTypeToInputType(node.rmType),
required: node.min > 0,
repeatable: node.max === -1 || node.max > 1,
validation: extractValidation(node),
options: extractOptions(node)
});
}
// Recurse into children
if (node.children) {
for (const child of node.children) {
fields.push(...generateFormFields(child, currentPath));
}
}
return fields;
}
function mapRmTypeToInputType(rmType: string): string {
const mapping: Record<string, string> = {
"DV_TEXT": "text",
"DV_CODED_TEXT": "select",
"DV_QUANTITY": "number",
"DV_DATE_TIME": "datetime",
"DV_BOOLEAN": "checkbox",
"DV_COUNT": "integer",
"DV_ORDINAL": "radio",
"DV_PROPORTION": "percentage",
"DV_MULTIMEDIA": "file-upload"
};
return mapping[rmType] || "text";
} Binding Forms to the CDR
Flat JSON Composition Format
Regardless of which form approach you use, data is sent to the CDR in flat JSON composition format. Each form field value is mapped to a flat key using the web template path:
// Form field values to flat composition
function serializeFormToFlatComposition(fields: FormField[], values: Record<string, any>): Record<string, any> {
const composition: Record<string, any> = {
"ctx/language": "en",
"ctx/territory": "US",
"ctx/composer_name": getCurrentUserName()
};
for (const field of fields) {
const value = values[field.path];
if (value === undefined || value === null) continue;
switch (field.type) {
case "number": // DV_QUANTITY
composition[`${field.path}|magnitude`] = value;
composition[`${field.path}|unit`] = field.unit;
break;
case "select": // DV_CODED_TEXT
composition[`${field.path}|code`] = value;
break;
case "text": // DV_TEXT
composition[`${field.path}|value`] = value;
break;
case "datetime": // DV_DATE_TIME
composition[`${field.path}|value`] = value;
break;
}
}
return composition;
} CDR Submission and Error Handling
POST the flat composition to the CDR. Handle validation errors by mapping them back to form fields:
// Submit composition to CDR with field-level error mapping
async function submitComposition(ehrId: string, templateId: string, composition: Record<string, any>) {
const response = await fetch(
`${CDR_URL}/ehr/${ehrId}/composition?format=FLAT&templateId=${templateId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(composition)
}
);
if (!response.ok) {
const error = await response.json();
// CDR returns validation errors with AQL paths
// Map them back to form field paths
if (error.validationErrors) {
return error.validationErrors.map((ve: any) => ({
field: ve.path, // e.g., "encounter/blood_pressure/any_event/systolic"
message: ve.message // e.g., "Value 999 exceeds maximum 300"
}));
}
throw new Error(`CDR error: ${response.status}`);
}
return await response.json(); // { compositionUid: "..." }
} Validation Rules from Archetypes
Client-Side vs. Server-Side Validation
The web template contains enough information to implement client-side validation that mirrors the CDR server-side validation. This gives users immediate feedback without a round-trip to the server.
| Archetype Constraint | Client-Side Validation | Server-Side (CDR) |
|---|---|---|
occurrences 1..1 | Required field indicator + prevent submit | Reject composition if missing |
DV_QUANTITY range 0..300 | Input min/max attributes + inline error | Reject if out of range |
DV_CODED_TEXT with value set | Dropdown with only valid options | Reject if code not in value set |
cardinality 0..* | Repeatable section with Add/Remove buttons | Accepts any number of entries |
DV_DATE_TIME pattern | Date picker with format enforcement | Reject if format invalid |
DV_PROPORTION type ratio | Two number inputs with ratio display | Validate numerator/denominator |
Implementing Constraint Extraction
// Extract validation rules from web template node
function extractValidation(node: WebTemplateNode): ValidationRules {
const rules: ValidationRules = {
required: node.min > 0
};
if (node.inputs) {
for (const input of node.inputs) {
if (input.validation?.range) {
rules.min = input.validation.range.min;
rules.max = input.validation.range.max;
}
if (input.validation?.pattern) {
rules.pattern = input.validation.pattern;
}
if (input.list) {
rules.allowedValues = input.list.map((item: any) => ({
value: item.value,
label: item.label || item.localizedLabels?.en || item.value
}));
}
}
}
return rules;
} Choosing the Right Approach
The right form-building approach depends on your constraints:
- Medblocks — Best for: teams building CDR-agnostic applications, rapid prototyping, organizations that value open source. Invest in Medblocks when you need to ship quickly and your form requirements are standard data entry.
- Better Forms SDK — Best for: organizations already on Better Platform, teams that want visual form design without coding, clinical departments that need to create forms without developer involvement. Invest in Better Forms when form creation velocity matters more than framework independence.
- Custom Build — Best for: complex clinical workflows, deep framework integration requirements, teams with strong frontend engineering. Invest in custom when your form UX is a differentiator and you need full control over every interaction.
Form Version Management
When a template version changes, forms need to update. The strategy depends on your form building approach:
- Auto-generated forms (from web template parsing) — Minor version changes are handled automatically. The form regenerates from the new web template, including new optional fields. Major version changes may require manual review of the generated form layout.
- Medblocks forms — You need to update the HTML/JSX to include new field components. The component paths must match the updated web template paths.
- Better Forms — The visual designer shows new fields when the template updates. A form administrator reviews and positions the new fields.
- Custom forms — You own the full update cycle. Every template change requires a code change in the form layer.
Regardless of approach, implement form version tracking. Store the template version alongside the form configuration so you know which template version each form targets.
Need expert help with healthcare data integration? Explore our Healthcare Interoperability Solutions to see how we connect systems seamlessly. We also offer specialized Healthcare Software Product Development services. Talk to our team to get started.
Frequently Asked QuestionsCan I use React or Vue components instead of web components?
Yes. Medblocks web components work inside React and Vue applications. For a fully native experience, you can build your own component library that wraps the web template parsing logic in framework-native components. Several organizations maintain internal React component libraries for openEHR forms.
How do I handle forms that span multiple templates?
Each form submission creates one composition from one template. If a clinical workflow requires data from multiple templates (e.g., vitals + medications + assessment), create separate forms and submit separate compositions. Link them via shared context (encounter ID, time window).
What about form pre-population from existing data?
Query the CDR for the patient's most recent composition of the same template type. Deserialize the flat JSON into form field values. Medblocks supports this via the mb-form element's data property. Custom implementations need to build the deserialization logic.
Can clinicians create forms without developer involvement?
Better Forms SDK is the closest to a no-code form builder. The visual designer allows clinical informaticists to arrange fields, set labels, and configure validation without writing code. Medblocks and custom approaches require developer involvement for form creation.
Conclusion
Building clinical forms from openEHR templates is a solved problem at the technical level — the web template format provides everything needed to generate functional forms. The real decisions are about trade-offs: development speed vs. customization depth, vendor independence vs. integrated tooling, auto-generation vs. hand-crafted UX.
Start with Medblocks for rapid validation. Move to custom components when your UX requirements demand it. Use Better Forms if you are on the Better Platform and want the fastest path to production. Whichever approach you choose, invest in the web template parsing layer early — it is the foundation that all three approaches share.
Need help building clinical forms on openEHR? Our engineering team has implemented all three approaches across production health systems.



