Skip to main content
Resource Publishing

Custom Dashboard Schema

A custom dashboard groups patient reports, custom reports, and inline count or indicator tabs into a named clinical view. Dashboards appear in a configurable section of the Medicus UI (controlled by subnavItem).

A dashboard can reference separately-published reports by identifier, or define inline tabs that embed patient queries directly.

See also the Custom Report Schema for the report types that dashboards reference, and the Patient Query Language reference for writing inline patient queries.

Examples​

Dashboard referencing published reports​

<?xml version="1.0" encoding="UTF-8"?>
<dashboard>
<name>Diabetes Register</name>
<identifier>my-org/diabetes-register</identifier>
<subnavItem>registers</subnavItem>
<patientReport>my-org/diabetes-patients</patientReport>
<indicatorReport>my-org/diabetes-indicators</indicatorReport>
</dashboard>

Dashboard with an inline count tab​

<?xml version="1.0" encoding="UTF-8"?>
<dashboard>
<name>Medication Monitoring</name>
<identifier>my-org/medication-monitoring</identifier>
<subnavItem>medication-monitoring</subnavItem>
<reportCountTab identifier="on-monitoring" label="On Monitoring" type="patient">
<row identifier="warfarin-patients" label="Warfarin">
<patientQuery>
<!-- inline patient query XML -->
</patientQuery>
</row>
</reportCountTab>
</dashboard>

Full dashboard with an inline report tab​

Putting it all together: a one-tab dashboard listing every patient with a ring pessary recorded. This example is checked automatically against the current schema (npm run check-schema-examples, wired into prebuild) - if a schema change breaks it, the build fails rather than the example silently going stale.

<?xml version="1.0" encoding="UTF-8"?>
<dashboard>
<name>Ring Pessary Review</name>
<identifier>my-org/ring-pessary-review</identifier>
<subnavItem>local-enhanced-services</subnavItem>
<section>LES</section>
<reportTab identifier="my-org/ring-pessary-patients" name="Ring Pessary Patients">
<patientReport schemaVersion="3">
<name>Ring Pessary Patients</name>
<rows>
<patientQuery>
<name>RingPessary</name>
<description>Patients with a ring pessary</description>
<population><patientQueryReference>ALL</patientQueryReference></population>
<codeLists>
<codeList>
<name>RING_PESSARY_COD</name>
<codeListDefinition>
<rules>
<inclusion>&lt;&lt; 274228008 |Insertion of pessary|</inclusion>
</rules>
</codeListDefinition>
</codeList>
</codeLists>
<variables>
<codedEntryVariable name="PESSARY_DAT" code="RING_PESSARY_COD" select="clinically_relevant_date" order="clinically_relevant_date" take="latest"/>
</variables>
<rules>
<rule>
<if>PESSARY_DAT NOTEQ NULL</if>
<then>select</then>
<else>reject</else>
</rule>
</rules>
</patientQuery>
</rows>
<columns>
<demographicInformationColumn headerName="Patient" value="patientName"/>
<demographicInformationColumn value="patientPrimaryIdentifier"/>
<demographicInformationColumn headerName="Age" value="patientAge"/>
</columns>
</patientReport>
</reportTab>
</dashboard>

Schema​

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- Reused for real, structurally-checked element types (populationType, codeListsType,
variablesType, rulesType) below, in place of the previous bare
<xs:any processContents="lax">. -->
<xs:include schemaLocation="custom-report-common.xsd"/>
<!-- ===== Dashboard subnavItem enumeration =====
these represent all of the available values users can configure for subnavitem -->
<xs:simpleType name="dashboardSubnavItemType">
<xs:restriction base="xs:string">
<xs:enumeration value="cqc"/>
<xs:enumeration value="local-enhanced-services"/>
<xs:enumeration value="medication-monitoring"/>
<xs:enumeration value="preventative-care"/>
<xs:enumeration value="registers"/>
<xs:enumeration value="screening"/>
<xs:enumeration value="vaccination-recalls"/>
</xs:restriction>
</xs:simpleType>
<!-- ===== Shared enums ===== -->
<xs:simpleType name="countTabTypeType">
<xs:restriction base="xs:string">
<xs:enumeration value="patient"/>
<xs:enumeration value="prescription"/>
<xs:enumeration value="prescription-issue"/>
<xs:enumeration value="appointment"/>
<xs:enumeration value="activity"/>
</xs:restriction>
</xs:simpleType>
<!-- ===== Opaque types =====

A dashboard row's <patientQuery> is a fragment, not a complete standalone query:
an enterprise dashboard is authored with nothing but a <description> (population/codeLists/
variables/rules filled in per-organisation when it runs elsewhere), so only
<description> is required here. Whatever else IS present still validates for real,
against the same types (populationType/codeListsType/variablesType/rulesType) a
standalone report's <patientQuery> uses, so an invalid codedEntryVariable select
value is still a save-time error rather than only failing at run time. See
EnterpriseDashboardFactory::countTabXml()/indicatorTabXml() and EnterpriseDashboardAuthoringTest
for the minimal shape this has to keep accepting. -->
<xs:complexType name="patientQueryType">
<xs:all>
<xs:element name="name" type="xs:string" minOccurs="0"/>
<xs:element name="description" type="xs:string"/>
<xs:element name="population" type="populationType" minOccurs="0"/>
<xs:element name="codeLists" type="codeListsType" minOccurs="0"/>
<xs:element name="variables" type="dashboardVariablesType" minOccurs="0"/>
<xs:element name="rules" type="rulesType" minOccurs="0"/>
</xs:all>
</xs:complexType>
<!-- Every variable type except codedEntryVariable stays lax (anyAttribute/any content):
an enterprise dashboard can author a placeholder variable with only one attribute set (e.g.
appointmentType="enterprise-label:..."), resolved per-organisation when it runs elsewhere,
so the full required-attribute strictness custom-report-common.xsd's variablesType
demands for a real, standalone report does not hold here. codedEntryVariable is the
one confirmed customer complaint (an invalid select value passing save-time
validation), so it alone gets the real, strict type. -->
<xs:complexType name="dashboardVariablesType">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="codedEntryVariable" type="codedEntryDateVariableType"/>
<xs:element name="fixedDate" type="lenientVariableType"/>
<xs:element name="derivedFixedDate" type="lenientVariableType"/>
<xs:element name="patientAge" type="lenientVariableType"/>
<xs:element name="patientEntryDate" type="lenientVariableType"/>
<xs:element name="patientClinicalSex" type="lenientVariableType"/>
<xs:element name="patientRegistrationVariable" type="lenientVariableType"/>
<xs:element name="clinicalCaseVariable" type="lenientVariableType"/>
<xs:element name="patientNamedGp" type="lenientVariableType"/>
<xs:element name="patientRegisteredGpPractice" type="lenientVariableType"/>
<xs:element name="patientRegisteredGpPracticeName" type="lenientVariableType"/>
<xs:element name="patientQuery" type="lenientVariableType"/>
<xs:element name="prescriptionVariable" type="lenientVariableType"/>
<xs:element name="prescriptionIssueVariable" type="lenientVariableType"/>
<xs:element name="appointmentVariable" type="lenientVariableType"/>
<xs:element name="codedEntryDate" type="lenientVariableType"/>
<xs:element name="codedEntryValue" type="lenientVariableType"/>
<xs:element name="codedEntryFromListVariable" type="lenientVariableType"/>
<xs:element name="codedEntryValueFromList" type="lenientVariableType"/>
<xs:element name="entryDateFromList" type="lenientVariableType"/>
<xs:element name="aggregatedEntryDate" type="lenientVariableType"/>
<xs:element name="conditionalCodedEntryVariable" type="lenientVariableType"/>
<xs:element name="derivedCodedEntryDate" type="lenientVariableType"/>
<xs:element name="derivedCodedEntryPractitioner" type="lenientVariableType"/>
<xs:element name="derivedEntryDateFromList" type="lenientVariableType"/>
<xs:element name="derivedPatientEntryDate" type="lenientVariableType"/>
<xs:element name="derivedPrescriptionIssueDate" type="lenientVariableType"/>
<xs:element name="derivedPrescriptionIssueDescription" type="lenientVariableType"/>
<xs:element name="derivedPrescriptionDate" type="lenientVariableType"/>
<xs:element name="derivedBooleanValue" type="lenientVariableType"/>
<xs:element name="patientCareHome" type="lenientVariableType"/>
<xs:element name="patientEthnic" type="lenientVariableType"/>
<xs:element name="patientRegistrationType" type="lenientVariableType"/>
<xs:element name="specialCase" type="lenientVariableType"/>
</xs:choice>
</xs:complexType>
<!-- ===== Runtime value structures ===== -->
<xs:complexType name="tabRuntimeValueEntryType">
<xs:sequence>
<xs:element name="value" maxOccurs="unbounded">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="name" type="xs:string" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="tabRuntimeValuesType">
<xs:sequence>
<xs:element name="runtimeValue" type="tabRuntimeValueEntryType" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<!-- ===== Tab types ===== -->
<xs:complexType name="reportTabType">
<xs:sequence>
<!-- exportColumns follows the report rather than preceding it: the wildcard would
otherwise match it too, which is a unique particle attribution violation. -->
<xs:any processContents="lax" minOccurs="1" maxOccurs="1"/>
<xs:element name="exportColumns" type="exportColumnsType" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="identifier" type="xs:string" use="required"/>
<xs:attribute name="name" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="countRowType">
<xs:sequence>
<xs:element name="patientQuery" type="patientQueryType"/>
</xs:sequence>
<xs:attribute name="identifier" type="xs:string" use="required"/>
<xs:attribute name="label" type="xs:string" use="optional"/>
</xs:complexType>
<!--
Author-defined columns for a count/indicator tab's drill-down patient list. Present, these
REPLACE the per-type defaults outright, so the author owns the order and can leave columns
out. It reuses columnsType, so it accepts exactly the columns a dashboard list report can:
the direct columns read off a retained record, plus the self-contained date/value/derived
columns that carry their own lookup. The drill-down runs through the same report engine, so
they render as they do in a list report. DrilldownColumnsValidator still rejects a cross-type
record column (one reading a record the tab never retained).
-->
<xs:complexType name="reportCountTabType">
<xs:sequence>
<xs:element name="runtimeValues" type="tabRuntimeValuesType" minOccurs="0"/>
<xs:element name="drilldownColumns" type="columnsType" minOccurs="0"/>
<xs:element name="exportColumns" type="exportColumnsType" minOccurs="0"/>
<xs:element name="row" type="countRowType" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="identifier" type="xs:string" use="required"/>
<xs:attribute name="label" type="xs:string" use="required"/>
<xs:attribute name="type" type="countTabTypeType" use="required"/>
</xs:complexType>
<xs:complexType name="indicatorRowType">
<xs:sequence>
<xs:element name="numerator">
<xs:complexType>
<xs:sequence>
<xs:element name="patientQuery" type="patientQueryType"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="denominator">
<xs:complexType>
<xs:sequence>
<xs:element name="patientQuery" type="patientQueryType"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="identifier" type="xs:string" use="required"/>
<!-- The customer-facing code ("AST001"), unlike identifier= which is this row's internal key. -->
<xs:attribute name="indicatorIdentifier" type="xs:string" use="optional"/>
<xs:attribute name="label" type="xs:string" use="optional"/>
<xs:attribute name="target" type="xs:decimal" use="optional"/>
<xs:attribute name="matchLabel" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="reportIndicatorTabType">
<xs:sequence>
<xs:element name="runtimeValues" type="tabRuntimeValuesType" minOccurs="0"/>
<xs:element name="drilldownColumns" type="columnsType" minOccurs="0"/>
<xs:element name="exportColumns" type="exportColumnsType" minOccurs="0"/>
<xs:element name="row" type="indicatorRowType" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="identifier" type="xs:string" use="required"/>
<xs:attribute name="label" type="xs:string" use="required"/>
<xs:attribute name="type" type="countTabTypeType" use="optional"/>
</xs:complexType>
<!-- ===== Root element ===== -->
<xs:element name="dashboard">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="identifier" type="xs:string"/>
<xs:choice>
<xs:element name="subnavItem" type="dashboardSubnavItemType"/>
<xs:element name="type" type="dashboardSubnavItemType"/>
</xs:choice>
<xs:element name="section" type="xs:string" minOccurs="0"/>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="patientReport" type="xs:string"/>
<xs:element name="indicatorReport" type="xs:string"/>
<xs:element name="patientCountReport" type="xs:string"/>
<xs:element name="activityCountReport" type="xs:string"/>
<xs:element name="reportTab" type="reportTabType"/>
<xs:element name="reportCountTab" type="reportCountTabType"/>
<xs:element name="reportIndicatorTab" type="reportIndicatorTabType"/>
</xs:choice>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

Key elements and attributes​

Element / AttributeRequiredDescription
nameYesDisplay name of the dashboard.
identifierYesUnique identifier, e.g. my-org/diabetes-register.
subnavItem / typeYes (one of)Controls which section of the Medicus UI the dashboard appears under. Allowed values: cqc, local-enhanced-services, medication-monitoring, preventative-care, registers, screening, vaccination-recalls.
sectionNoOptional sub-section label within the chosen nav area.
patientReportNoIdentifier string of a separately-published patient report.
indicatorReportNoIdentifier string of a separately-published indicator report.
patientCountReportNoIdentifier string of a separately-published patient count report.
activityCountReportNoIdentifier string of a separately-published activity count report.
reportTabNoInline tab embedding a report. Requires identifier and name attributes.
reportCountTabNoInline count tab. Requires identifier, label, and type attributes. type is one of patient, prescription, prescription-issue, appointment, activity.
reportIndicatorTabNoInline indicator tab. Each row has a numerator and denominator patient query. Optional target percentage (decimal).