diff --git a/brian_dashboard_manager/dashboards/services_mws.json b/brian_dashboard_manager/dashboards/services_mws.json
new file mode 100755
index 0000000000000000000000000000000000000000..39c9a333c0d644e486f673d7ccd14557842852c7
--- /dev/null
+++ b/brian_dashboard_manager/dashboards/services_mws.json
@@ -0,0 +1,119 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "id": 454,
+  "links": [],
+  "panels": [
+    {
+      "datasource": null,
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "folderId": null,
+      "gridPos": {
+        "h": 25,
+        "w": 24,
+        "x": 0,
+        "y": 0
+      },
+      "headings": false,
+      "id": 2,
+      "limit": 100,
+      "pluginVersion": "7.1.4",
+      "query": "",
+      "recent": false,
+      "search": true,
+      "starred": false,
+      "tags": [
+        "mws"
+      ],
+      "targets": [
+        {
+          "groupBy": [
+            {
+              "params": [
+                "$__interval"
+              ],
+              "type": "time"
+            },
+            {
+              "params": [
+                "null"
+              ],
+              "type": "fill"
+            }
+          ],
+          "orderByTime": "ASC",
+          "policy": "default",
+          "refId": "A",
+          "resultFormat": "time_series",
+          "select": [
+            [
+              {
+                "params": [
+                  "value"
+                ],
+                "type": "field"
+              },
+              {
+                "params": [],
+                "type": "mean"
+              }
+            ]
+          ],
+          "tags": []
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "",
+      "type": "dashlist"
+    }
+  ],
+  "schemaVersion": 26,
+  "style": "dark",
+   "tags": [
+    "services"
+  ],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ]
+  },
+  "timezone": "",
+  "title": "GÉANT Managed Wavelength Service",
+  "version": 1
+}
diff --git a/brian_dashboard_manager/grafana/provision.py b/brian_dashboard_manager/grafana/provision.py
index 0259d48414956946ce6b2f990af970e0a195e1ed..1eb6614792e4c51edd483f94ff09fba05a8c1262 100644
--- a/brian_dashboard_manager/grafana/provision.py
+++ b/brian_dashboard_manager/grafana/provision.py
@@ -33,7 +33,8 @@ from brian_dashboard_manager.templating.helpers import \
     get_aggregate_dashboard_data, get_interface_data, \
     get_nren_interface_data, get_dashboard_data, \
     get_nren_dashboard_data, get_aggregate_interface_data, \
-    get_nren_interface_data_old, get_re_peer_dashboard_data, get_re_peer_interface_data
+    get_nren_interface_data_old, get_re_peer_dashboard_data, get_re_peer_interface_data, get_service_data, \
+    get_service_dashboard_data
 
 from brian_dashboard_manager.templating.gws import generate_gws, generate_indirect
 from brian_dashboard_manager.templating.eumetsat import generate_eumetsat_multicast
@@ -136,6 +137,16 @@ DASHBOARDS = {
     }
 }
 
+SERVICE_DASHBOARDS = {
+    'MWS': {
+        'tag': ['mws'],
+        'service_type': 'GEANT MANAGED WAVELENGTH SERVICE',
+        'folder_name': 'Managed Wavelength Service',
+        'interfaces': [],
+        'services': []
+    }
+}
+
 AGG_DASHBOARDS = {
     'CLS_PEERS': {
         'tag': 'cls_peers',
@@ -181,8 +192,8 @@ AGG_DASHBOARDS = {
 }
 
 
-def provision_folder(token_request, folder_name, dash,
-                     config, ds_name, excluded_dashboards):
+def provision_folder(token_request, folder_name, dash, services,
+                     ds_name, excluded_dashboards):
     """
     Function to provision dashboards within a folder.
 
@@ -190,7 +201,7 @@ def provision_folder(token_request, folder_name, dash,
     :param folder_name: Name of the folder to provision dashboards in
     :param dash: the dashboards to provision, with interface data to generate
     the dashboards from
-    :param config: the application config
+    :param services: service data from reporting provider for service-based dashboards
     :param ds_name: the name of the datasource to query in the dashboard panels
     :param excluded_dashboards: list of dashboards to exclude from provisioning
     for the organisation
@@ -218,19 +229,24 @@ def provision_folder(token_request, folder_name, dash,
     is_nren_beta = folder_name == "NREN Access BETA"  # needed for POL1-642 BETA
     is_nren = folder_name == "NREN Access"
     is_re_peer = folder_name == "RE Peer"
+    is_service = 'service_type' in dash
+    # todo: figure out a neater way to do this?
+    is_complex = is_nren or is_nren_beta or is_re_peer or is_service
 
     if is_nren:
         data = get_nren_interface_data_old(interfaces)
         dash_data = get_nren_dashboard_data(data, ds_name, tag)
     elif is_nren_beta:
         # needed for POL1-642 BETA
-        services = fetch_services(config['reporting_provider'])
         data = get_nren_interface_data(
             services, interfaces, excluded_dashboards)
         dash_data = get_nren_dashboard_data(data, ds_name, tag)
     elif is_re_peer:
         data = get_re_peer_interface_data(interfaces)
         dash_data = get_re_peer_dashboard_data(data, ds_name, tag)
+    elif is_service:
+        data = get_service_data(dash['service_type'], services, interfaces, excluded_dashboards)
+        dash_data = get_service_dashboard_data(data, ds_name, tag)
     else:
         data = get_interface_data(interfaces)
         dash_data = get_dashboard_data(
@@ -243,7 +259,7 @@ def provision_folder(token_request, folder_name, dash,
 
     with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
         for dashboard in dash_data:
-            if is_nren or is_nren_beta or is_re_peer:
+            if is_complex:
                 rendered = render_complex_dashboard(**dashboard)
             else:
                 rendered = render_simple_dashboard(**dashboard)
@@ -346,6 +362,20 @@ def excluded_folder_dashboards(org_config, folder_name):
     return excluded if isinstance(excluded, list) else []
 
 
+def _interfaces_to_keep(interface, excluded_nrens):
+    dash_info = interface.get('dashboards_info')
+    if dash_info is None:
+        logger.info(f'No "dashboards_info" for '
+                    f'{interface["router"]}:{interface["name"]}')
+        # throw it away
+        return False
+    dashboards = {nren['name'].lower() for nren in dash_info}
+    is_lab_router = 'lab.office' in interface['router'].lower()
+    should_keep = not (is_lab_router or any(
+        nren.lower() in dashboards for nren in excluded_nrens))
+    return should_keep
+
+
 def _provision_interfaces(config, org_config, ds_name, token):
     """
     This function is used to provision most dashboards,
@@ -359,24 +389,12 @@ def _provision_interfaces(config, org_config, ds_name, token):
     """
 
     interfaces = get_interfaces(config['inventory_provider'])
+    services = fetch_services(config['reporting_provider'])
 
     excluded_nrens = org_config['excluded_nrens']
     excluded_folders = org_config.get('excluded_folders', {})
 
-    def interfaces_to_keep(interface):
-        dash_info = interface.get('dashboards_info')
-        if dash_info is None:
-            logger.info(f'No "dashboards_info" for '
-                        f'{interface["router"]}:{interface["name"]}')
-            # throw it away
-            return False
-        dashboards = {nren['name'].lower() for nren in dash_info}
-        is_lab_router = 'lab.office' in interface['router'].lower()
-        should_keep = not (is_lab_router or any(
-            nren.lower() in dashboards for nren in excluded_nrens))
-        return should_keep
-
-    relevant_interfaces = list(filter(interfaces_to_keep, interfaces))
+    relevant_interfaces = list(filter(lambda x: _interfaces_to_keep(x, excluded_nrens), interfaces))
     for interface in relevant_interfaces:
         interface['dashboards_info'] = list(filter(
             lambda x: x['name'] != '',
@@ -429,7 +447,7 @@ def _provision_interfaces(config, org_config, ds_name, token):
                 f'Provisioning {org_config["name"]}/{folder_name} dashboards')
             res = executor.submit(
                 provision_folder, token,
-                folder_name, folder, config, ds_name,
+                folder_name, folder, services, ds_name,
                 excluded_folder_dashboards(org_config, folder_name))
             provisioned.append(res)
 
@@ -588,6 +606,61 @@ def _provision_aggregates(config, org_config, ds_name, token):
             yield from provisioned
 
 
+def _provision_service_dashboards(config, org_config, ds_name, token):
+    """
+    This function is used to provision service-specific dashboards,
+    overwriting existing ones.
+
+    :param config: the application config
+    :param org_config: the organisation config
+    :param ds_name: the name of the datasource to query in the dashboards
+    :param token: a token_request object
+    :return: generator of UIDs of dashboards that were created
+    """
+    services = fetch_services(config['reporting_provider'])
+
+    excluded_folders = org_config.get('excluded_folders', {})
+
+    logger.info('Provisioning service-specific dashboards')
+
+    # loop over service dashboards and get service types we care about
+    dash_service_types = {SERVICE_DASHBOARDS[dash]['service_type']: dash for dash in SERVICE_DASHBOARDS}
+    # loop over services and append to dashboards
+    for service in services:
+        if service['service_type'] in dash_service_types:
+            dash = dash_service_types[service['service_type']]
+            svcs = SERVICE_DASHBOARDS[dash]['services']
+            svcs.append(service)
+
+    # provision dashboards and their folders
+    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
+        provisioned = []
+        for folder in SERVICE_DASHBOARDS.values():
+            folder_name = folder['folder_name']
+
+            # boolean True means entire folder excluded
+            # if list, it is specific dashboard names not to provision
+            # so is handled at provision time.
+            if is_excluded_folder(excluded_folders, folder_name):
+                executor.submit(
+                    delete_folder, token, title=folder_name)
+                continue
+
+            logger.info(
+                f'Provisioning {org_config["name"]}/{folder_name} dashboards')
+            res = executor.submit(
+                provision_folder, token,
+                folder_name, folder, services, ds_name,
+                excluded_folder_dashboards(org_config, folder_name))
+            provisioned.append(res)
+
+        for result in provisioned:
+            folder = result.result()
+            if folder is None:
+                continue
+            yield from folder
+
+
 def _provision_static_dashboards(config, org_config, ds_name, token):
     """
     This function is used to provision static dashboards from json files,
@@ -795,6 +868,8 @@ def provision(config, raise_exceptions=False):
                     config, org_config, ds_name, token_request),
                 _provision_aggregates(
                     config, org_config, ds_name, token_request),
+                _provision_service_dashboards(
+                    config, org_config, ds_name, token_request),
                 _provision_static_dashboards(
                     config, org_config, ds_name, token_request),
                 _get_ignored_dashboards(
@@ -825,6 +900,8 @@ def provision(config, raise_exceptions=False):
             }
             folders_to_keep.update({dash['folder_name']
                                     for dash in DASHBOARDS.values()})
+            folders_to_keep.update({dash['folder_name']
+                                    for dash in SERVICE_DASHBOARDS.values()})
 
             ignored_folders = config.get('ignored_folders', [])
             folders_to_keep.update(ignored_folders)
diff --git a/brian_dashboard_manager/templating/helpers.py b/brian_dashboard_manager/templating/helpers.py
index 31c47cf52313f91c65792d1aee8b030f3754e038..a5aa9e9ed6d8058af115767aaa4144e041a25b8c 100644
--- a/brian_dashboard_manager/templating/helpers.py
+++ b/brian_dashboard_manager/templating/helpers.py
@@ -340,7 +340,7 @@ def get_nren_interface_data(services, interfaces, excluded_dashboards):
                     'interface': interface_name,
                     'hostname': host,
                     'alias':
-                    f"{router} - {interface_name} - {dashboard_name} "
+                        f"{router} - {interface_name} - {dashboard_name} "
                 })
 
             if info['interface_type'] == 'AGGREGATE':
@@ -368,6 +368,74 @@ def get_nren_interface_data(services, interfaces, excluded_dashboards):
     return result
 
 
+def get_service_data(service_type, services, interfaces, excluded_dashboards):
+    """
+    Helper for grouping interface data to be used for generating
+    dashboards for specific service types, grouped by NRENs.
+
+    Extracts information from interfaces to be used in panels.
+
+    :param services: list of services
+    :param interfaces: list of interfaces
+    :param excluded_dashboards: list of dashboards to exclude for
+    the organization we are generating dashboards for
+
+    :return: dictionary of dashboards and their service/interface data
+    """
+    result = {}
+
+    customers = defaultdict(list)
+
+    for service in services:
+        _customers = service.get('customers')
+        _service_type = service.get('service_type')
+        for cust in _customers:
+            if cust.lower() in excluded_dashboards:
+                continue
+            if _service_type != service_type:
+                continue
+            customers[cust].append(service)
+
+    for customer, services in customers.items():
+        dashboard = result.setdefault(customer, {
+            'SERVICES': []
+        })
+
+        for service in services:
+            _interfaces = service.get('endpoints')
+            name = service.get('name')
+            sid = service.get('sid')
+            scid = service.get('scid')
+
+            measurement = 'scid_rates'
+
+            if len(_interfaces) == 0:
+                continue
+
+            if 'interface' in _interfaces[0]:
+                if_name = _interfaces[0].get('interface')
+                router = _interfaces[0].get('hostname')
+            else:
+                if_name = _interfaces[0].get('port')
+                router = _interfaces[0].get('equipment')
+            router = router.replace('.geant.net', '')
+            title = f'{router} - {{}} - {if_name} - {name} ({sid})'
+
+            dashboard['SERVICES'].append({
+                'measurement': measurement,
+                'title': title,
+                'scid': scid,
+                'sort': (sid[:2], name)
+            })
+
+    for customer in list(result.keys()):
+        lengths = [len(val) for val in result[customer].values()]
+        if sum(lengths) == 0:
+            # no services/interfaces, so remove it
+            del result[customer]
+    return result
+
+
 def get_interface_data(interfaces):
     """
     Helper for grouping interface data to be used for generating
@@ -438,6 +506,7 @@ def get_aggregate_interface_data(interfaces, agg_name, group_field):
             prev[curr[field]] = groups
             prev['EVERYSINGLETARGET'] = all_agg
             return prev
+
         return reduce_func
 
     for interface in interfaces:
@@ -612,6 +681,31 @@ def default_interface_panel_generator(gridPos, use_all_traffic=True, use_ipv6=Tr
     return get_panel_definitions
 
 
+def _get_dashboard_data(data, datasource, tag, single_data_func):
+    """
+    Helper for generating dashboard definitions.
+    Uses multiprocessing to speed up generation.
+
+    :param data: the dashboard names and the panel data for each dashboard
+    :param datasource: datasource to use for the panels
+    :param tag: tag to use for the dashboard, used for dashboard dropdowns on
+    the home dashboard.
+    :param single_data_func: function that gets data for one definition
+
+    :return: generator for dashboard definitions for each dashboard
+    """
+
+    with ProcessPoolExecutor(max_workers=NUM_PROCESSES) as executor:
+        for dash in executor.map(
+                partial(
+                    single_data_func,
+                    datasource=datasource,
+                    tag=tag),
+                data.items()
+        ):
+            yield dash
+
+
 def get_nren_dashboard_data_single(data, datasource, tag):
     """
     Helper for generating dashboard definitions for a single NREN.
@@ -649,7 +743,7 @@ def get_nren_dashboard_data_single(data, datasource, tag):
     def sort_key(panel):
         sort = panel.get('sort')
         if not sort:
-            return 'ZZZ'+panel.get('hostname')  # sort to end
+            return 'ZZZ' + panel.get('hostname')  # sort to end
         return sort
 
     service_panels = panel_gen(
@@ -692,28 +786,7 @@ def get_nren_dashboard_data_single(data, datasource, tag):
 
 
 def get_nren_dashboard_data(data, datasource, tag):
-    """
-    Helper for generating dashboard definitions for all NRENs.
-    Uses multiprocessing to speed up generation.
-
-    :param data: the NREN names and the panel data for each NREN
-    :param datasource: datasource to use for the panels
-    :param tag: tag to use for the dashboard, used for dashboard dropdowns on
-    the home dashboard.
-
-    :return: generator for dashboard definitions for each NREN
-    """
-
-    with ProcessPoolExecutor(max_workers=NUM_PROCESSES) as executor:
-        for dash in executor.map(
-            partial(
-                get_nren_dashboard_data_single,
-                datasource=datasource,
-                tag=tag),
-            data.items()
-        ):
-
-            yield dash
+    yield from _get_dashboard_data(data, datasource, tag, get_nren_dashboard_data_single)
 
 
 def get_re_peer_dashboard_data_single(data, datasource, tag):
@@ -752,7 +825,7 @@ def get_re_peer_dashboard_data_single(data, datasource, tag):
     def sort_key(panel):
         sort = panel.get('sort')
         if not sort:
-            return 'ZZZ'+panel.get('hostname')  # sort to end
+            return 'ZZZ' + panel.get('hostname')  # sort to end
         return sort
 
     service_panels = panel_gen(
@@ -786,28 +859,61 @@ def get_re_peer_dashboard_data_single(data, datasource, tag):
 
 
 def get_re_peer_dashboard_data(data, datasource, tag):
+    yield from _get_dashboard_data(data, datasource, tag, get_re_peer_dashboard_data_single)
+
+
+def get_service_dashboard_data_single(data, datasource, tag):
     """
-    Helper for generating dashboard definitions for all R&E Peers.
-    Uses multiprocessing to speed up generation.
+    Helper for generating dashboard definitions for a single service.
 
-    :param data: the names and the panel data for each R&E Peer
+    :param data: data for the dashboard, including the service name and
+    the panel data
     :param datasource: datasource to use for the panels
     :param tag: tag to use for the dashboard, used for dashboard dropdowns on
     the home dashboard.
 
-    :return: generator for dashboard definitions for each R&E Peer
+    :return: dashboard definition for the service dashboard
     """
 
-    with ProcessPoolExecutor(max_workers=NUM_PROCESSES) as executor:
-        for dash in executor.map(
-            partial(
-                get_re_peer_dashboard_data_single,
-                datasource=datasource,
-                tag=tag),
-            data.items()
-        ):
+    service_name, dash = data
+    id_gen = num_generator()
 
-            yield dash
+    gridPos = gridPos_generator(id_gen)
+
+    panel_gen = default_interface_panel_generator(gridPos, use_all_traffic=True, use_ipv6=False)
+
+    services_dropdown = create_dropdown_panel('Services', **next(gridPos))
+
+    def sort_key(panel):
+        sort = panel.get('sort')
+        if not sort:
+            return 'ZZZ' + panel.get('hostname')  # sort to end
+        return sort
+
+    service_panels = panel_gen(
+        sorted(dash['SERVICES'], key=sort_key), datasource)
+
+    dropdown_groups = [{
+        'dropdown': services_dropdown,
+        'panels': service_panels,
+    }]
+
+    result = {
+        'nren_name': service_name,
+        'datasource': datasource,
+        'aggregate_panels': [],
+        'dropdown_groups': dropdown_groups
+    }
+    if isinstance(tag, list):
+        result['tags'] = tag
+    else:
+        result['tag'] = tag
+
+    return result
+
+
+def get_service_dashboard_data(data, datasource, tag):
+    yield from _get_dashboard_data(data, datasource, tag, get_service_dashboard_data_single)
 
 
 def get_dashboard_data_single(
@@ -867,15 +973,14 @@ def get_dashboard_data(
 
     with ProcessPoolExecutor(max_workers=NUM_PROCESSES) as executor:
         for dash in executor.map(
-            partial(
-                get_dashboard_data_single,
-                datasource=datasource,
-                tag=tag,
-                panel_generator=panel_generator,
-                errors=errors),
-            data.items()
+                partial(
+                    get_dashboard_data_single,
+                    datasource=datasource,
+                    tag=tag,
+                    panel_generator=panel_generator,
+                    errors=errors),
+                data.items()
         ):
-
             yield dash
 
 
diff --git a/changelog.md b/changelog.md
index 8f58d66691485adf969c3df859e9cfd4fbcddb38..75c99a064a66b8284707d6b04b017fec7f7357e9 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,10 @@
 
 All notable changes to this project will be documented in this file.
 
+## [0.63] - 2024-07-18
+- POL1-700 - Managed Wavelength Service dashboard
+- Service-type based dashboard support
+
 ## [0.62] - 2024-07-11
 - POL1-579: New layout for R&E Peer dashboards
 - Migrate from jinja2 to just plain python dictionaries
diff --git a/setup.py b/setup.py
index 9b630b2b1f3658bb3cc9882a3e6887a286e526ab..598fa7359f3060337f8dbc076e69b828dcffbe17 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
 
 setup(
     name='brian-dashboard-manager',
-    version="0.62",
+    version="0.63",
     author='GEANT',
     author_email='swd@geant.org',
     description='',
diff --git a/test/test_update.py b/test/test_update.py
index e407e600de890cfc03178033a42fdb6df6e1486e..9e5dfc380a7c20d6ddc8840122309fe8fe0df12c 100644
--- a/test/test_update.py
+++ b/test/test_update.py
@@ -2,6 +2,7 @@ import pytest
 import responses
 
 from brian_dashboard_manager.grafana.provision import provision_folder, provision
+from brian_dashboard_manager.services.api import fetch_services
 
 TEST_INTERFACES = [
     {
@@ -678,11 +679,13 @@ def test_provision_nren_folder(
         }
     )
 
+    services = fetch_services(data_config['reporting_provider'])
+
     result = provision_folder(
         mock_grafana.request,
         folder_name,
         dashboards["NREN"],
-        data_config,
+        services,
         "testdatasource",
         excluded_nrens,
     )