Module 5 · Live Dashboard

Live Dashboard

Now that live data is flowing, write KQL queries across all three streams and pin them as live tiles to a Real-Time Intelligence Dashboard. By the end you'll watch the city pulse update on screen, second by second.

60 minutes
🎯 Goal: live RTI dashboard
🛠 KQL Queryset · RTI Dashboard

Learning Objectives

Step 1 - Create the Real-Time Dashboard RTI DASHBOARD

  1. Create the dashboard

    From the eh_urbanpulse_rti Eventhouse, click the Database tab in the top header. On the toolbar, click Real-Time Dashboard+ New dashboard. Name it rtd_urbanpulse_city.

    Launching the dashboard from the Eventhouse pre-wires it to eh_urbanpulse_rti as a data source, so you can skip to adding tiles.

    📷
    SCREENSHOT NEEDED
    module-5/00-rtd-from-eventhouse.png
    Eventhouse Database tab with the Real-Time Dashboard button highlighted on the top toolbar
  2. Switch the dashboard into edit mode

    The new dashboard opens in View mode. Click the toggle in the top-right and switch to Editing. The toolbar now shows + Add visual, Add Markdown, Add alert, Add data source, and Add parameter.

  3. Tile 1 - Critical patients (Stat)

    1. On the toolbar, click + Add visualStat. A query editor opens on the left and a tile preview on the right.
    2. Confirm the data source dropdown shows eh_urbanpulse_rti (auto-selected because you launched from the Eventhouse).
    3. Paste the KQL below and click Run. The result pane should show a single integer value.
    4. In the right-hand Visual formatting pane, set Tile name to Critical patients. Optionally add a Conditional formatting rule that turns the number red when the value is greater than 0.
    5. Click Apply changes in the top-right.
    KQL · Tile 1
    HospitalVitals
    | where timestamp > ago(15m)
    | summarize arg_max(timestamp, *) by patient_id
    | where condition == "critical"
        or spo2 < 90
        or heart_rate > 120
        or temperature_f > 101.5
    | count
  4. Tile 2 - Top 5 highest heart rate (Table)

    1. Click + Add visualTable.
    2. Paste the KQL below and click Run.
    3. In Visual formatting, set Tile name to Top 5 heart rate. Optionally enable Conditional formatting on the condition column to colour critical rows red.
    4. Click Apply changes.
    KQL · Tile 2
    HospitalVitals
    | where timestamp > ago(15m)
    | summarize arg_max(timestamp, *) by patient_id
    | project patient_id, condition, heart_rate, spo2, temperature_f, timestamp
    | order by heart_rate desc
    | take 5
  5. Tile 3 - Live room occupancy (Column chart)

    1. Click + Add visualColumn chart.
    2. Paste the KQL below and click Run. The renderer uses to_location as the X axis and patients as the Y axis automatically.
    3. In Visual formatting, set Tile name to Live room occupancy.
    4. Click Apply changes.
    KQL · Tile 3
    HospitalMovement
    | summarize arg_max(timestamp, *) by patient_id
    | where event_type != "Discharged"
    | summarize patients = count() by to_location
    | order by patients desc
    | render columnchart with (title="Live room occupancy")
  6. Tile 4 - Train fleet status (Pie chart)

    1. Click + Add visualPie chart.
    2. Paste the KQL below and click Run.
    3. In the visual config pane, confirm Category = line and Value = trains.
    4. In Visual formatting, set Tile name to Train fleet status (last 10m).
    5. Click Apply changes.
    KQL · Tile 4
    TrainTelemetry
    | where timestamp > ago(10m)
    | summarize arg_max(timestamp, *) by trainId
    | summarize trains = count() by line, status
    | render piechart with (title="Train fleet status · last 10m")
  7. Arrange the layout

    Back on the dashboard canvas, drag the tiles to match the recommended grid below. Tiles 1 and 2 are KPI / table on the top row; Tiles 3 and 4 are the two charts on the bottom row.

    TILE 1 Critical Patients 3 TILE 2 · Top 5 highest heart rate PAT-10003 · critical · HR 142 · SpO2 88 PAT-10007 · elevated · HR 128 · SpO2 95 PAT-10001 · stable · HR 92 · SpO2 98 TILE 3 · Room occupancy TILE 4 · Train fleet status
    Suggested layout (Tiles 1-4)
  8. Confirm the dashboard matches the reference

    Your dashboard should now look like the reference screenshot below: Critical patient Stat top-left, Top 5 heart rate table top-right, Live room occupancy column chart bottom-left, and Train fleet status pie chart bottom-right.

    📷
    SCREENSHOT NEEDED
    module-5/01-dashboard-reference.png
    Live RTI dashboard reference: Critical patient Stat, Top 5 heart rate table, Live room occupancy column chart, Train fleet status pie chart
  9. Set auto-refresh and save

    Top-right of the dashboard → Auto refresh → set 15 seconds. Click Save on the toolbar, then toggle the top-right switch back to View. Tiles will continue to update without the editor chrome.

Step 1b - Drive an Activator alert from a tile ACTIVATOR

Real-Time Dashboards visualise live data, but operators often need a push when something crosses a threshold - not a tile they have to be looking at. Fabric Activator (Reflex) lets you bind a rule to any KQL stream or dashboard tile and route alerts to Teams, email, or Power Automate.

You'll wire one rule that fires every time the Critical patients tile crosses zero.

  1. Open the Add alert dialog

    On the dashboard toolbar (Editing mode), click Add alert. The Activator pane slides in from the right and pre-fills the underlying KQL from the tile that's currently selected.

    If nothing is selected, click Tile 1 - Critical patients first so the alert is bound to that tile's query.

    📷
    SCREENSHOT NEEDED
    module-5/04-add-alert-toolbar.png
    Dashboard toolbar in Editing mode with the Add alert button highlighted, and the Activator pane open on the right pre-filled with the Critical patients KQL
  2. Pick or create an Activator item

    In the Activator pane:

    • Workspace: your lab workspace.
    • Activator: + Create new → name it act_urbanpulse_alerts.
    • Rule name: CriticalPatientsDetected.

    Click Next.

  3. Define the condition

    • When: value (the integer the Stat tile produces)
    • Becomes: greater than
    • Threshold: 0
    • Frequency: Every 1 minute

    Activator evaluates the underlying KQL on that cadence and fires the rule the first time the count crosses the threshold (it won't re-fire on every poll while the count stays high).

  4. Choose an action

    Pick the simplest channel for the lab:

    • Email: enter your own address. Subject: UrbanPulse - critical patients detected. Body: include the value and timestamp tokens.

    For production, swap email for a Teams channel post or a Power Automate flow that pages an on-call rotation.

  5. Save and start the rule

    Click Save and start. Activator provisions the rule and begins polling. The first fire arrives within ~1 minute (the producers always have at least one critical patient in the last 15-minute window).

    📷
    SCREENSHOT NEEDED
    module-5/03-activator-rule.png
    Activator rule editor showing the CriticalPatientsDetected rule with value > 0 condition and email action configured
💡
Why pair Activator with the dashboard? The dashboard is the human-facing surface; Activator is the machine-facing one. Same KQL, two delivery modes - one rule covers both the live tile and the on-call alert without you maintaining a second pipeline.

Step 2 - Medallion Inside KQL with Update Policies ★ OPTIONAL

This step is optional. The dashboard you built in Step 1 is a complete deliverable. Add this section if you want to see how to build a Bronze → Silver → Gold medallion entirely inside KQL - no Spark notebooks, no scheduled pipelines. Allow ~15 extra minutes.

So far HospitalVitals, HospitalMovement, and TrainTelemetry are all Bronze - raw, schema-on-write copies of what landed via Eventstream. A KQL update policy is a stored expression that runs on every ingest into a source table and writes the transformed rows into a target table. Chain them and you get medallion tiers that stay in sync as new events stream in - no orchestrator needed.

BRONZE HospitalVitals SILVER HospitalVitals_clean GOLD PatientStatus_now update policy cleanse + cast materialized view arg_max per patient Every Bronze ingest auto-fires the policy → Silver gets new rows in milliseconds.
Figure 4.2 - The KQL medallion: an update policy chains Bronze → Silver, a materialized view rolls Silver → Gold.
  1. 5a - Create the Silver target table

    In your KQL query window (Eventhouse → KQL Database → Query), run:

    KQL
    // SILVER target - one clean, typed row per vitals event
    .create table HospitalVitals_clean (
        patient_id: string,
        age: int,
        condition: string,
        heart_rate: int,
        bp_systolic: int,
        bp_diastolic: int,
        temperature_f: real,
        spo2: int,
        is_critical: bool,
        timestamp: datetime
    )
    
    .alter table HospitalVitals_clean policy streamingingestion enable
  2. 5b - Define the Bronze → Silver update policy

    An update policy needs two pieces: a function that transforms rows, and a policy declaration that wires it to a source table.

    KQL
    // 1. The transform function (called by KQL on every batch ingest into Bronze)
    .create-or-alter function HospitalVitals_to_clean() {
        HospitalVitals
        | where isnotempty(patient_id) and heart_rate > 0
        | extend is_critical = (
            heart_rate > 130 or heart_rate < 45
            or spo2 < 90
            or bp_systolic > 180 or bp_systolic < 80
          )
        | project patient_id, age, condition,
                  heart_rate, bp_systolic, bp_diastolic,
                  temperature_f, spo2, is_critical, timestamp
    }
    
    // 2. The policy: every ingest into HospitalVitals fires the function and writes to HospitalVitals_clean
    .alter table HospitalVitals_clean policy update
    ```
    [
      {
        "IsEnabled": true,
        "Source": "HospitalVitals",
        "Query": "HospitalVitals_to_clean()",
        "IsTransactional": true,
        "PropagateIngestionProperties": true
      }
    ]
    ```
    IsTransactional: true means a Silver write only commits if the function succeeded - bad source rows don't break the chain. Combined with PropagateIngestionProperties, retention and caching policies inherit too.
  3. 5c - Define the Silver → Gold materialized view

    For Gold we don't need another physical table - a materialized view keeps an always-current "latest row per patient" projection that answers dashboard queries in milliseconds.

    KQL
    .create materialized-view with (backfill = true) PatientStatus_now on table HospitalVitals_clean
    {
        HospitalVitals_clean
        | summarize arg_max(timestamp, *) by patient_id
    }

    backfill = true seeds the view from existing Silver rows (so it works immediately on the data already mirrored from Bronze). New Silver rows update the view incrementally.

  4. 5d - Verify the chain

    Run all three to see the tiers light up:

    KQL
    HospitalVitals             | count    // BRONZE: total raw rows
    HospitalVitals_clean       | count    // SILVER: typed + filtered, should match (no bad rows)
    PatientStatus_now          | count    // GOLD: should equal active patient count (~15)

    Point a dashboard tile at PatientStatus_now and you've got a sub-second "current critical patients" KPI without a single ETL job.

  5. 5e (optional within optional) - Repeat for Trains

    The recipe is identical. For practice, try:

    • TrainTelemetryTrainTelemetry_clean (drop GPS-zero rows; is_delayed = status == "DELAYED") → TrainStatus_now mat-view (arg_max by trainId)
💡
Why this matters for the rest of the lab. Module 7's Fabric Ontology can bind directly to HospitalVitals_clean instead of raw HospitalVitals - which means Module 8's Hospital Data Agent gets cleaned data with the is_critical flag pre-computed. Less prompt-shaping, more accurate answers. And because the OneLake-availability toggle from Module 3 cascades to every table, your Silver/Gold tiers are also available as Delta in OneLake automatically - so the Direct Lake semantic model can read them too.

What You Just Built

💡
Take a beat to appreciate this. No data engineer authored a single ETL job. You went from raw Event Hub messages to a live operational dashboard in under three hours. That's the Real-Time Intelligence story Microsoft is pitching, and you just built it.

Module 6 turns the warm-path Lakehouse data into a Power BI report using Direct Lake - no import refresh, no DirectQuery latency.

References