Overview
The Medicus custom report builder uses a powerful and flexible XML schema to construct reports — both ad hoc reports and out-of-the-box reports such as condition registers.
Using the schema below, you can construct your own reports in XML and paste them directly into the custom report builder to view results.
Important notes:
- If you are not comfortable with XML then we recommend using the custom report builder UI instead. The XML schema is intended for developers or those who prefer to build reports programmatically
- You are responsible for ensuring the validity of reports and any clinical risks associated with their outputs.
How to Construct Reports in XML
There are two ways to construct reports in XML:
- Manually — using a text editor such as VS Code.
- Using an AI tool — such as Claude. Pass the XSD (attached at the bottom of this article) along with a plain-English description of your report. AI tools are increasingly capable of generating valid XML from a schema.
Report Structure
Every report has three top-level elements: <name>, <rows>, and <columns>, wrapped in either a <patientReport> or <activityReport> root element.
<patientReport schemaVersion="2"> <name>...</name> <rows>...</rows> <columns>...</columns> </patientReport>
Name
The display name shown in the Medicus UI.
<name>Patients with Diabetes</name>Rows
The <rows> block contains the logic for which patients or activity records should appear in the report. It always contains a single <patientQuery>.
<rows>
<patientQuery>
<population>...</population>
<codeLists>...</codeLists>
<variables>...</variables>
<rules>...</rules>
</patientQuery>
</rows>Patient Query
The patient query determines which patients are included in the report. It consists of four parts: population, code lists, variables, and rules.
1. Population
Defines the starting set of patients the query runs against. There are three options:
All Registered Patients
<population> <patientQueryReference>ALL</patientQueryReference> </population>
Another Custom Report's Results
Use the results of another custom report as the population. Get the report ID from its URL in Medicus.
<population> <patientReportReference>a1b2c3d4-e5f6-7890-abcd-ef1234567890</patientReportReference> </population>
An Inline Patient Query
Define a nested query inline — useful when you need to filter a sub-population before applying further logic.
<population>
<patientQuery>
<population>
<patientQueryReference>ALL</patientQueryReference>
</population>
<codeLists>...</codeLists>
<variables>...</variables>
<rules>...</rules>
</patientQuery>
</population>Circular and deeply nested queries are detected and blocked.
Examples of invalid structures:
• Circular: Report A → Report B → Report A
• Too deeply nested: Report A → Report B → … → Report Z (maximum depth: 10)This prevents impossible or infinitely recursive queries.
2. Code Lists
Code lists define the sets of SNOMED CT codes your query uses. There are three ways to define a code list.
Method 1: Fixed Set of Codes
Explicitly list the SNOMED CT codes to target.
<codeList>
<name>SPECIFIC_COD</name>
<fixedCodeList>
<codes>
<code>1037261000000100</code>
<code>1037271000000107</code>
</codes>
</fixedCodeList>
</codeList>Method 2: Inline ECL Definition
Use SNOMED CT Expression Constraint Language (ECL) to define the code set dynamically — for example, "all descendants of Diabetes mellitus, excluding Type 1".
<codeList>
<name>DM_COD</name>
<codeListDefinition>
<rules>
<inclusion><< 73211009 |Diabetes mellitus|</inclusion>
<exclusion><< 46635009 |Diabetes mellitus type 1|</exclusion>
</rules>
</codeListDefinition>
</codeList>Because < and > are reserved XML characters, ECL operators must be XML-escaped inside <inclusion> and <exclusion> tags. For example, << must be written as <<.
The full list of supported ECL operators:
The full list of ECL operators and their XML-escaped forms:
| Name | Symbol | XML Escaped |
|---|---|---|
| Descendant or self of | << |
<< |
| Descendant of | < |
< |
| Child of | <! |
<! |
| Child or self of | <<! |
<<! |
| Ancestor or self of | >> |
>> |
| Ancestor of | > |
> |
| Parent of | >! |
>! |
| Parent or self of | >>! |
>>! |
| Member of reference set | ^ |
^ |
Method 3: Reference an Existing Code List
Reference a centrally managed code list by its identifier.
<codeList> <name>DM_COD</name> <codeListReference>a1b2c3d4-e5f6-7890-abcd-ef1234567890</codeListReference> </codeList>
3. Variables
Variables extract and compute values from patient records based on your code lists. They are referenced later in rules and columns.
Coded Entry Variable
Retrieves a value from a matching coded entry. The take attribute controls which occurrence to return: latest, earliest, or all. The required select and order attributes control what is selected and how results are ordered — see your Medicus documentation for accepted values.
<!-- Latest date of a Dementia diagnosis --> <codedEntryVariable name="DM_DAT" code="DM_COD" select="TODO" order="TODO" take="latest"/> <!-- All occurrences (e.g. for activity reports) --> <codedEntryVariable name="BP_DAT" code="BP_COD" select="TODO" order="TODO" take="all"/>
Fixed Date
A specific date, either hardcoded or supplied at runtime.
<!-- Supplied by the user at runtime --> <fixedDate name="ACHV_DAT" runtimeValue="true"/> <!-- Hardcoded date (YYYYMMDD) --> <fixedDate name="START_DAT">20250101</fixedDate>
Derived Fixed Date
A date calculated by applying an offset to a fixed date (e.g. 12 months before an achievement date).
<derivedFixedDate name="ONE_YEAR_AGO" fixedDateName="ACHV_DAT" operation="-12 months"/>
Demographic Variables
Built-in variables for common patient fields.
<patientAge name="AGE"/> <patientClinicalSex name="SEX"/> <patientRegistrationType name="REG_TYPE"/> <patientNamedGp name="GP"/> <patientEntryDate name="REG_DAT"/>
4. Rules
Rules determine whether each patient is selected or rejected based on the variables calculated above. Each rule has an <if> condition, a <then> outcome (select or next-rule), and an <else> outcome (reject).
Supported operators: EQ, NOTEQ, GT, LT, GTEQ, LTEQ — combinable with AND, OR, and parentheses.
<!-- Single rule: select patients who have a DM code -->
<rules>
<rule>
<if>DM_DAT NOTEQ NULL</if>
<then>select</then>
<else>reject</else>
</rule>
</rules>Use next-rule to chain multiple conditions:
<rules>
<rule>
<if>DM_DAT NOTEQ NULL</if>
<then>next-rule</then> <!-- condition met — check next rule -->
<else>reject</else>
</rule>
<rule>
<if>REVIEW_DAT EQ NULL</if>
<then>select</then> <!-- both conditions met — include patient -->
<else>reject</else>
</rule>
</rules>Activity Reports
Activity reports use the same <patientQuery> structure, but with a few key differences because they track individual clinical events rather than filtering patients:
- Use
fetch="all"to retrieve every matching occurrence of a code — each occurrence becomes its own row. - Rules typically use
TRUE EQ TRUEto select all activity (patient filtering is handled by a nested population query instead). - Include
activityDateandactivitycolumns to show what happened and when.
The root element is <activityReport> instead of <patientReport>.
Columns
The <columns> section defines which data appears in each column of the report output.
Demographic Information Columns
Fixed columns drawn from patient demographic data. Use the optional headerName attribute to override the default column heading.
| Value | Default Header |
|---|---|
patientName |
Patient Name |
patientNhsNumber |
NHS Number |
patientPrimaryIdentifier |
Patient Identifier |
patientUuid |
Patient UUID |
patientDateOfBirth |
Date of Birth |
patientAge |
Age |
patientGender |
Clinical Sex |
patientRegistrationType |
Registration Type |
patientRegistrationDate |
Registration Date |
namedGP |
Named GP |
homeAddress |
Home Address |
homePhoneNumber |
Home Phone Number |
mobilePhoneNumber |
Mobile Phone Number |
emailAddress |
Email Address |
The following values are available for <codedEntryValueColumn> and are relevant to activity reports:
| Value | Default Header |
|---|---|
activityDate |
Activity Date |
activity |
Activity |
<columns> <demographicInformationColumn value="patientName"/> <demographicInformationColumn value="patientNhsNumber"/> <demographicInformationColumn value="patientDateOfBirth" headerName="DOB"/> </columns>
Value Columns
Returns a raw value from a coded entry — for example, a numeric test result. References a variable defined in the patient query.
Derived Value Columns
Runs an embedded patient query to compute a per-patient value such as Yes / No / Overdue. Contains its own codeLists, variables, and logic. Supports optional useAsFilter (default: true) and basePatientQuery attributes.
Date Columns
Like derived value columns, but returns a formatted date. Uses a <returnValueFieldName> element to specify which variable's date to display. Supports optional useAsFilter and dateFormat attributes.
Code Columns
Returns the SNOMED CT description for a coded entry.
Example Reports
Patient Report — T2DM Patients Without a Statin
Selects patients aged 40–79 with a Type 2 Diabetes diagnosis who have no record of a statin prescription.
<patientReport schemaVersion="2">
<name>T2DM Without Statin</name>
<rows>
<patientQuery>
<population>
<patientQueryReference>ALL</patientQueryReference>
</population>
<codeLists>
<codeList>
<name>T2DM_COD</name>
<codeListDefinition>
<rules>
<inclusion><< 44054006 |Diabetes mellitus type 2|</inclusion>
<exclusion><< 46635009 |Diabetes mellitus type 1|</exclusion>
</rules>
</codeListDefinition>
</codeList>
<codeList>
<name>STATIN_COD</name>
<fixedCodeList>
<codes>
<code>375864007</code>
<code>373541007</code>
<code>108600003</code>
</codes>
</fixedCodeList>
</codeList>
</codeLists>
<variables>
<codedEntryVariable name="T2DM_DAT" code="T2DM_COD" select="TODO" order="TODO" take="latest"/>
<codedEntryVariable name="STATIN_DAT" code="STATIN_COD" select="TODO" order="TODO" take="latest"/>
<patientAge name="AGE"/>
</variables>
<rules>
<rule>
<if>T2DM_DAT NOTEQ NULL AND AGE GTEQ 40 AND AGE LTEQ 79</if>
<then>next-rule</then>
<else>reject</else>
</rule>
<rule>
<if>STATIN_DAT EQ NULL</if>
<then>select</then>
<else>reject</else>
</rule>
</rules>
</patientQuery>
</rows>
<columns>
<demographicInformationColumn value="patientName" headerName="Patient"/>
<demographicInformationColumn value="patientNhsNumber" headerName="NHS Number"/>
<demographicInformationColumn value="patientAge" headerName="Age"/>
<demographicInformationColumn value="namedGP"/>
</columns>
</patientReport>Activity Report — Flu Vaccinations
Lists every flu vaccination activity across all patients.
<activityReport schemaVersion="2">
<name>Flu Vaccinations</name>
<rows>
<patientQuery>
<population>
<patientQueryReference>ALL</patientQueryReference>
</population>
<codeLists>
<codeList>
<name>CRITERION_1_COD</name>
<codeListDefinition>
<rules>
<inclusion><< 86198006 |Influenza vaccination|</inclusion>
</rules>
</codeListDefinition>
</codeList>
</codeLists>
<variables>
<codedEntryVariable name="CRITERION_1_DAT" code="CRITERION_1_COD" select="TODO" order="TODO" take="all"/>
</variables>
<rules>
<rule>
<if>TRUE EQ TRUE</if>
<then>select</then>
<else>reject</else>
</rule>
</rules>
</patientQuery>
</rows>
<columns>
<demographicInformationColumn value="patientName"/>
<codedEntryValueColumn value="activityDate"/>
<codedEntryValueColumn value="activity"/>
<codedEntryValueColumn headerName="Activity Description" value="codeDescription"/>
</columns>
</activityReport>Activity Report — Diabetes Activity (Nested Population)
Lists annual review and HbA1c activity, but only for patients already confirmed to have a diabetes diagnosis. This uses a nested inline patient query to restrict the population first.
<activityReport schemaVersion="2">
<name>Diabetes Activity</name>
<rows>
<patientQuery>
<population>
<patientQuery>
<population>
<patientQueryReference>ALL</patientQueryReference>
</population>
<codeLists>
<codeList>
<name>DM_COD</name>
<codeListDefinition>
<rules>
<inclusion><< 73211009 |Diabetes mellitus|</inclusion>
</rules>
</codeListDefinition>
</codeList>
</codeLists>
<variables>
<codedEntryVariable name="DM_DAT" code="DM_COD" select="TODO" order="TODO" take="latest"/>
</variables>
<rules>
<rule>
<if>DM_DAT NOTEQ NULL</if>
<then>select</then>
<else>reject</else>
</rule>
</rules>
</patientQuery>
</population>
<codeLists>
<codeList>
<name>CRITERION_1_COD</name>
<codeListDefinition>
<rules>
<inclusion><< 314764000 |Diabetes mellitus annual review|</inclusion>
</rules>
</codeListDefinition>
</codeList>
<codeList>
<name>CRITERION_2_COD</name>
<codeListDefinition>
<rules>
<inclusion><< 43396009 |Haemoglobin A1c measurement|</inclusion>
</rules>
</codeListDefinition>
</codeList>
</codeLists>
<variables>
<codedEntryVariable name="CRITERION_1_DAT" code="CRITERION_1_COD" select="TODO" order="TODO" take="all"/>
<codedEntryVariable name="CRITERION_2_DAT" code="CRITERION_2_COD" select="TODO" order="TODO" take="all"/>
</variables>
<rules>
<rule>
<if>TRUE EQ TRUE</if>
<then>select</then>
<else>reject</else>
</rule>
</rules>
</patientQuery>
</rows>
<columns>
<demographicInformationColumn value="patientName"/>
<demographicInformationColumn value="patientNhsNumber"/>
<demographicInformationColumn value="patientDateOfBirth"/>
<codedEntryValueColumn value="activityDate"/>
<codedEntryValueColumn value="activity"/>
<codedEntryValueColumn headerName="Activity Description" value="codeDescription"/>
</columns>
</activityReport>XML Schema
The XSD schema file is attached to the bottom of this article. You can use it to validate your XML reports before importing them into Medicus, or pass it to an AI tool to help generate reports.
FAQs
-
Q: Are there any Medicus-managed concept lists I can reference in my reports (e.g. "all asthmatic patients")?
- A: We will be introducing this functionality shortly.
-
Q: What happens if my XML is invalid?
- A: The custom report builder will surface a validation error when you attempt to import it. We recommend validating your XML against the attached XSD before importing.
-
Q: Can I use an AI tool to generate reports?
- A: Yes — tools such as Claude work well for this. Attach the XSD from the bottom of this article and describe your report in plain English. Always review the output before using it clinically.