Ravi Chandu Edru/ articles
← Back to articles

fabric-iq

Generate a Fabric IQ ontology from a Power BI semantic model

A semantic model already knows how your tables connect. Fabric IQ can read that and hand you an ontology in one click. This walkthrough builds a hospital ontology that way, then does the four things the generator cannot do for you: the keys, the relationship bindings, the time-series binding, and the meaning.

Aug 16, 2026 · 33 min read

There are two ways to build an ontology in Microsoft Fabric.

The first is by hand. You create each entity type, type in each property, define each key, draw each relationship, and bind every one of them to a column. This site has a full walkthrough of that path: eight entity types, eleven relationships, north of forty separate acts of clicking.

The second is one button.

If you already have a Power BI semantic model, Fabric IQ can read it and generate the ontology from it. Tables become entity types, columns become properties, and the relationships you drew become relationship types. It takes about ten seconds, and what it hands back is genuinely most of an ontology.

Most. Not all. The gap between "most" and "all" is the part nobody writes down, so that gap is what this article is about.

Why a semantic model can turn into an ontology

Both are the same kind of object: a declared layer over tables that already exist in OneLake, describing how those tables relate. Neither moves data.

So when you drew a line from departments.HospitalId to hospitals.HospitalId, you wrote down a business fact — a department belongs to a hospital. You wrote it for Power BI's benefit, but the fact isn't about reporting. The generator reads those already-written facts and restates them in ontology terms.

That tells you exactly what won't carry over: anything the semantic model never knew.

What lives in the semantic modelWhat the ontology gets
A tableAn entity type, named after the table
A columnA property, with its data type
A relationship, From table to To tableA relationship type, auto-named from_has_to
The To column of a many-to-one relationshipUsually a key on the To entity type
A time-series stream in an eventhouseNothing. The model never saw it
What a column means to the businessNothing. No column carries meaning
Synonyms an agent should recogniseNothing

The first four rows are the time you save. The last three are your afternoon.

If the distinction is still fuzzy: a semantic model is built for aggregation, an ontology for traversal and identity. The semantic model vs ontology concept page pulls them apart properly.

What you're building

Microsoft's Lamna Healthcare sample, so you can check your screen against a known-good result. The original lab is the click path; this article is that plus the reasoning and the traps.

Every label below is what the product showed in a build run on 17 August 2026. Several have drifted from the lab's wording — its "Bindings tab", "Add data to entity type", "Binding type: Time series", "Source entity type" and "+ Add key" are all names the UI no longer uses. Where they disagree, follow this article and trust your screen.

Six tables describing one hospital:

  • Hospitals, Departments, Rooms, Patients, VitalSignEquipment — static reference data, headed for a lakehouse
  • VitalSignsReadings — timestamped measurements from those monitors, headed for an eventhouse

That split decides where each file goes and it runs through the whole build.

The finished ontology is a five-hop spine: a hospital contains departments, a department contains rooms, a patient is admitted to a room, a monitor is assigned to a patient, and that monitor emits readings. "Whose vitals are deteriorating, and which hospital are they in" is a walk along that spine.


Part 1: land the static data in a lakehouse

An ontology binds to tables, so the tables have to exist first. That's the only reason this comes before anything interesting.

1.1 Create the workspace

Open app.fabric.microsoft.com, select Workspaces+ New workspace, and name it LamnaHealthcareIQ.

The Create a workspace dialog with the name LamnaHealthcareIQ entered and a green name is available confirmation below it
Every item in this build is LamnaHealthcare plus a suffix, so the workspace is the IQ.

Open Advanced, set Workspace type to Fabric, Fabric Trial, or Power BI Premium, pick your capacity, and Apply. The plain Power BI options won't run an ontology.

1.2 Create the lakehouse

+ New item → search lakehouse → the plain Lakehouse card, not Sample lakehouse. Name it LamnaHealthcareLH, confirm Location reads LamnaHealthcareIQ, and Create.

The New Lakehouse dialog with the name LamnaHealthcareLH, Location set to LamnaHealthcareIQ, and the Lakehouse schemas checkbox ticked
Leave Lakehouse schemas ticked, which is the default. It creates the dbo schema that Part 5.2 asks you for by name.

1.3 Upload five files, and deliberately not the sixth

Get the CSVs if you haven't: single ZIP or browse the folder. Extract them somewhere you can find them.

From the lakehouse's Get data screen, select Upload files and upload exactly these five:

Hospitals.csv, Departments.csv, Rooms.csv, Patients.csv, VitalSignEquipment.csv

Keep VitalSignsReadings.csv back for Part 2. Put it here instead and nothing errors, you just end up with a table of numbers rather than a monitor with a pulse.

Select Files in the Explorer and count five. The header reads Showing 5 items.

1.4 Load each file to a table

A CSV in Files is a file. The ontology can't bind to it. It needs a Delta table under Tables.

For each file: ellipsis (…)Load to TablesNew table. In the Load file to new table dialog, leave Schema as dbo, accept the pre-filled New table name, check Use header for column names, leave Separator as a comma, and Load.

The Load file to new table dialog over the lakehouse Files list, with Schema dbo, table name departments, Use header for column names checked, and a comma separator
Five files behind the dialog and no sixth. The Schema field is pre-filled dbo because you left Lakehouse schemas ticked in 1.2, and the table name arrives lowercased whatever the file was called.

Five tables when you're done. Expand one and check the columns carry their headers, not _c0, _c1.


Part 2: land the readings in an eventhouse

Twenty rows, so this isn't about scale. It's about shape: readings in an eventhouse are what let Part 5.3 attach them to a monitor as a pulse.

2.1 Create the eventhouse

+ New itemEventhouse, name it LamnaHealthcareEH, Create.

The New Eventhouse dialog with the name LamnaHealthcareEH entered
One field. The KQL database that arrives inside takes the same name.

Fabric creates a KQL database with the same name inside it. Open that, and it reports This database is empty, which is what you'd hope.

2.2 Ingest the readings

Select Get dataLocal file.

The empty LamnaHealthcareEH KQL database with the Get data menu open, showing Local file among options including OneLake, Eventstream, Azure Storage and Event Hubs
Local file, because the readings are a CSV on your machine. In a real build this is where Eventstream would sit instead, and nothing downstream would change.

The wizard runs in four stages: Source, Configure, Inspect, Summary.

Under Select or create a destination table, select the +, type VitalSignsReadings into the inline field, and confirm it with the tick. Then Browse for files on the right and pick VitalSignsReadings.csv.

The Get data wizard on the Configure stage, with a new destination table named VitalSignsReadings being created inline and an empty file drop zone beside it
The new table is named inline and confirmed with the tick, not through a separate dialog. Next stays greyed out until a file lands on the right.

Next takes you to Inspect, which is the stage that matters. It shows the schema Fabric inferred, and you want Timestamp typed as a datetime rather than a string — Part 5.3 asks you to nominate a timestamp column, and a string won't qualify. Fix it here rather than after. Otherwise keep the defaults and Finish.

Confirm the table shows 20 rows.


Part 3: build the semantic model, and draw the four relationships

Everything here is the generator's input. Four lines drawn now are four relationship types you don't draw later. Draw them wrong and you generate a wrong ontology, then spend Part 5 wondering why the bindings misbehave.

3.1 Create the model

Open LamnaHealthcareLH and select New semantic model from the ribbon. Name it LamnaHealthcareModel, confirm Workspace reads LamnaHealthcareIQ, expand dbo, and tick all five tables. Confirm.

The New semantic model dialog with the name LamnaHealthcareModel, Workspace LamnaHealthcareIQ, and Select all ticked so all five dbo tables are checked
All five under dbo, and note the fifth: vitalsignequipment, fully lowercased. That is the entity type name you will be living with from Part 4 onward.

Confirm creates the model, but it doesn't drop you into it. Go to your workspace item list and select LamnaHealthcareModel to open the designer.

The semantic model designer showing five table cards with their columns and no relationship lines between them, and a Generate Ontology button in the ribbon
Five tables, zero lines. Fabric doesn't guess relationships, which is right: a matching column name isn't evidence of one. Note Generate Ontology already sitting in the ribbon, under its own Ontology group.

The Σ symbols mark columns Power BI has typed as numbers and will try to aggregate. EquipmentId doesn't carry one, because its values are strings like VS-1001, which is worth remembering when you reach the keys in 5.1.

3.2 Draw the four relationships

Manage relationships+ New relationship. All four use cardinality Many to one (*:1) and cross filter Both:

From tableFrom columnTo tableTo column
departmentsHospitalIdhospitalsHospitalId
roomsDepartmentIddepartmentsDepartmentId
patientsCurrentRoomIdroomsRoomId
vitalsignequipmentPatientIdpatientsPatientId
The New relationship dialog creating rooms to departments, with Cardinality Many to one and Cross-filter direction defaulted to Single, and a note that Direct Lake tables don't show previews
Cross-filter direction arrives as Single, not Both. Change it on every one of the four.

From is always the child, the many side, the table carrying the foreign key. To is always the parent. Get it backwards and Power BI still lets you save, but your ontology will claim a hospital belongs to a department and every traversal after that runs uphill.

Cross filter Both is a Power BI setting about filter propagation, and it matters for reports built on this model. It has no ontology equivalent — relationship direction there comes from the From-to-To pair, not from this setting.

3.3 Check before you generate

Open Manage relationships and confirm exactly four active relationships. Greyed-out rows are inactive, and duplicates appear if you saved a dialog twice. Either feeds the generator something you didn't mean.

The canvas is the faster check. Four lines, each with a * at the child end and a 1 at the parent end, and every table connected into one chain.

The model designer with four relationship lines drawn, connecting vitalsignequipment to patients, patients to rooms, rooms to departments and departments to hospitals
One unbroken chain from vitalsignequipment through to hospitals. Every * sits on the child, every 1 on the parent. No line touches vitalsignequipment and rooms directly.

Read them back as sentences before you move on, because these become your ontology: a department belongs to a hospital, a room is part of a department, a patient is admitted to a room, a monitor is assigned to a patient.


Part 4: generate the ontology

Generate Ontology sits in the model editor's ribbon under its own Ontology group, so if you're still in the designer from Part 3 you're already looking at it.

The lab has you close the model first and reopen it from the workspace with ellipsis (…)Open semantic model. The button works either way, but reopening is cheap insurance that your four relationships were committed before the generator reads them.

Select Generate Ontology, pick LamnaHealthcareIQ from the workspace dropdown, name it LamnaHealthcareOntology, Create. Names take letters, numbers and underscores only.

The semantic model ribbon with the Generate Ontology button highlighted in its own Ontology group, between Model health and Explore
One button, in its own group, between Model health and Explore. Everything before this article's Part 5 happens because of it.

What arrived

Five entity types carrying every column as a typed property: hospitals, departments, rooms, patients, vitalsignequipment.

The new LamnaHealthcareOntology item open, with an Explorer listing five entity types: vitalsignequipment, rooms, departments, hospitals and patients
Five entity types, in a new LamnaHealthcareOntology tab. The toolbar is down to Add entity type, because there is nothing selected yet to relate.

Four relationship types too, each auto-named from the pair you drew. Select an entity type and the canvas shows it with the relationships it takes part in, one at a time, rather than the whole model at once.

The ontology canvas with vitalsignequipment selected, joined by an arrow to patients, the connection labelled vitalsignequipment_has_patients
The naming rule in the open: from_has_to. This is the one you will bind in 5.2, and the arrow already points the right way.

The other three read departments_has_hospitals, rooms_has_departments and patients_has_rooms.

And one thing the ribbon doesn't mention. Creating an ontology also creates a Graph in Microsoft Fabric child item, named after the ontology with _graph appended. You never edit it, but it stores the materialized graph — the actual nodes and edges built from your bindings, and the thing queries and agents really run against. Knowing it exists explains why Part 6 makes you wait.

By hand that would have been five entity creations, twenty-five property definitions, and four relationship dialogs. Then it stops, because four things remain that it structurally cannot do.


Part 5: finish the ontology by hand

The documentation compresses this into a sentence. It's the part that decides whether your ontology works.

Only 5.1 comes first is a real constraint, because every binding resolves against a key. After that 5.2 and 5.3 are independent, and doing relationships first is just a debugging convenience: it proves the static spine before streaming can confuse the result.

5.1 The keys, and the rule that predicts which are missing

A key is the property whose value picks out exactly one instance. Without it the graph can't tell two instances apart.

The official guidance says only that some keys are inferred and others need adding. The pattern behind it: a many-to-one relationship declares the To column unique. Many departments point at one hospital, so hospitals.HospitalId identifies a single hospital and the generator can promote it. Nothing makes that promise about a From column.

So look at which tables ever appear on the To side:

Entity typeEver a To table?Key
hospitalsYes, from departmentsHospitalId
departmentsYes, from roomsDepartmentId
roomsYes, from patientsRoomId
patientsYes, from vitalsignequipmentPatientId
vitalsignequipmentNeverEquipmentId

vitalsignequipment sits at the end of the chain with nothing pointing at it, so it's the one most likely to arrive keyless. Check all five anyway.

Select an entity type to land on its Bind data to properties page. Under Entity type key, either the key is listed or you get Define entity type key — select that, pick the property from the Property list dropdown, and Save.

The rooms entity type's Bind data to properties page showing an Entity type key missing warning, with the Add or edit key dialog open and RoomId chosen from the Property list
rooms with no key, and the binding refusing to save until it has one. The dialog warns that the key applies to every binding this entity type will ever have.

The dialog warns the key "will apply to all your entity type bindings", which is why keys come first, and the binding card refuses to save until one exists.

A relationship type says departments connect to hospitals. A binding proves which department connects to which, by pointing at a table holding both identifiers in the same row.

Bind to the child table. It's the only one that carries both columns.

departments carries a DepartmentId and a HospitalId on every row. hospitals has no column pointing back, because one hospital has many departments, so it physically cannot express the link. The foreign key lives on the many side, so the many side is the binding table — in any model, not just this one.

Select a relationship and you get three panels: Origin entity type and Target entity type read-only on the outside, and the wiring in the middle.

The vitalsignequipment_has_patients relationship page, with Origin entity type vitalsignequipment, Target entity type patients, Mapping table vitalsignequipment, and two matched key columns
The outer panels are read-only, showing each entity type's key and its own binding. Everything you set lives in the middle, under Mapping table.

Pick the Mapping table, then answer its two questions: Matched <origin>: <key> and Matched <target>: <key>. Each asks which column in the mapping table carries that entity type's key.

RelationshipMapping tableMatched originMatched target
departments_has_hospitalsdepartmentsDepartmentIdHospitalId
rooms_has_departmentsroomsRoomIdDepartmentId
patients_has_roomspatientsPatientIdCurrentRoomId
vitalsignequipment_has_patientsvitalsignequipmentEquipmentIdPatientId

A Switch direction button sits top right. If Part 4 handed you a reversed relationship, try that before adding a replacement and deleting the original — check the name afterwards, since it may keep the old one.

5.3 The time-series binding, or how one entity type gets two data sources

This is the thing an ontology does that a semantic model cannot, and the reason the build bothered with an eventhouse. vitalsignequipment knows only static facts so far: which monitor, on which patient, since when. A second binding points the same entity type at a different Fabric item carrying a different shape of data, so afterwards it reads both at once — the lakehouse for what a monitor is, the eventhouse for what it's measuring.

On its Bind data to properties page, Binding selection already holds the lakehouse card generation made for you. Expand Add data binding, take the second option, Eventhouse table or materialized view, and choose LamnaHealthcareEHVitalSignsReadings.

The Add data binding menu open on the vitalsignequipment entity type, offering Lakehouse table and Eventhouse table or materialized view
Bindings are cards, not a single slot. The lakehouse card stays exactly where it is; the eventhouse one is about to sit beside it.

A second card appears, and with it a section the lakehouse binding never showed: Timeseries data. Set Timestamp column to Timestamp — the red asterisk means required, and it's the switch that makes this a time series rather than another flat table.

Check Entity type key mapping reads EquipmentIdEquipmentId, confirm the Properties rows for HeartRate, OxygenSaturation, ReadingId, RespiratoryRate and Timestamp, and Save.

Two binding cards on vitalsignequipment, with Entity type key mapping on EquipmentId, a Timeseries data section with Timestamp column selected, and five mapped properties
Two bindings, one entity type. Entity type key mapping is what files each reading against a machine; the Properties list below it is everything else the stream carries.

5.4 The meaning, which the generator could never have given you

The lab ends without this step. Every ontology worth building includes it.

Your ontology is now structurally complete and semantically mute. It knows a monitor connects to a patient. It doesn't know that clinicians call it "the monitor", or that OxygenSaturation is what everyone in the building calls SpO2. None of that was in a column, so none of it survived generation.

Names. Generated entity types read vitalsignequipment. Rename where the product allows, because anyone writing a graph query types these by hand.

Display names. Without a display name property, instances label themselves by key — and this sample shows why that hurts. RoomId holds 1 through 10; RoomNumber holds ICU-301, ER-102. The key is the meaningless one, which is correct, keys exist to be stable rather than readable. So set RoomNumber on rooms, a full name on patients, HospitalName on hospitals. One dropdown each, and it decides how legible every later screen is, including every agent answer.

Descriptions and synonyms. On entity types, properties, relationships. None required, all of it read by an agent trying to map a plain-language question onto your graph. "Which patients are in the ICU" only finds DepartmentName = 'ICU' if something connects the word to the column.

This is the City Cart build's lesson exactly: an agent read the graph structure perfectly and still answered wrong, because the meaning of one word was never written down. Generation makes the structural half free, which makes the other half more tempting to skip. The other half was always the hard one.


Part 6: prove it actually works

An unverified ontology is a diagram.

Select rooms and use View Entity Type details. The entity type page has three tabs: Configure, where you have spent all of Part 5, Instances, and Overview.

Open Overview. You'll get "Updating your ontology" while the _graph item builds the nodes and edges. Give it a minute or two, then refresh the browser, because the page doesn't always refresh itself.

The rooms entity type Overview tab, with a Relationship graph tile showing patients to rooms to departments, a Resource links tile, and bar charts for RoomNumber, DepartmentId and RoomType
The Overview tab builds itself from the bindings. Read the relationship graph first: patients_has_rooms and rooms_has_departments, pointing the right way.

The Relationship graph tile is the fastest proof you have: rooms joined to patients on one side and departments on the other, relationship names on the arrows. A reversed one is still visible here.

Below it Fabric draws one bar chart per property, and those are a free data check. RoomNumber should give ten bars of one, since room numbers are unique. DepartmentId should give 3, 3 and 4 — ICU has three rooms, Emergency three, Surgical four. Wrong numbers mean the binding is reading the wrong column.

Then switch to Instances and check the counts, small enough to verify by eye:

Entity typeInstances
hospitals1
departments3
rooms10
patients5
vitalsignequipment5, each with a time series behind it

Then open ICU-302, the room whose RoomId is 2. You get its department, Intensive Care Unit, and its patient, Casey Morgan. Follow Casey to monitor VS-1002 and the readings are there.

That's instance-level identity, not just a schema. A semantic model gives you average heart rate by department. Only this tells you the person in ICU-302 is Casey Morgan and hands you her trace.

Read the failures, because they're specific

What you seeWhat it means
Instances tab emptyBinding wrong or key missing. Back to 5.1 and 5.2
Instances fine, Relationship graph tile bareThe relationship binding was never configured. Back to 5.2
A property's bar chart has the wrong countsThe binding is reading the wrong column. Back to 5.2
Every instance labelled by an IDNo display name property. Cosmetic, 5.4 fixes it
Still "Updating your ontology" after five minutesRefresh. If it persists, a binding points at a table that no longer exists
vitalsignequipment has instances but no readingsTime-series binding saved without Entity type key mapping. Back to 5.3

Run all five entity types, not just rooms. An empty Instances tab is invisible until an agent gives you a confidently wrong answer three weeks later.


What generation actually bought you

JobGeneratedStill yours
Five entity types
Twenty-five properties, typed
Four relationship types, named and directed
Four of five keys, probablyVerify all five
Four relationship bindingsSometimesVerify all four
The time-series bindingAll yours
Names, display names, descriptions, synonymsAll yours

The structural work collapsed from an afternoon into ten seconds. The semantic work didn't move at all, because it was never in the semantic model to begin with.

Generate Ontology is a transcription tool, not an authoring tool. It copies facts you already wrote down. It cannot write down facts you never did.

When you should not use this path

Your semantic model is a star schema built for reporting. Most production models are, carrying date dimensions nobody would call a business entity, bridge tables that exist only to resolve many-to-many joins, and denormalisation done for speed. Generate from that and you get an ontology shaped like a report, wearing entity types as a costume.

The test: read your relationship lines out loud as sentences. "A department belongs to a hospital" is a business fact. "A fact table joins a date dimension" is a modelling technique. Mostly the second kind? Build by hand, and let the ontology take the shape of the business rather than the warehouse.

You don't already have a semantic model. Building one purely to generate an ontology means declaring every relationship in the Power BI designer, then verifying every one again in the ontology editor. Go straight to the manual build.

Where this goes next

There's a question waiting in this data, worth knowing about even if you stop building today.

Follow monitor VS-1001 across its eight readings, 08:00 to 08:35. Heart rate 78, 82, 85, 88, 90, 92, 95, 98. Oxygen falling 98 to 93. Respiratory rate climbing 16 to 22. Somebody is getting worse — and no single reading trips an alarm, because the highest number anywhere in the file is 98. The other four monitors drift mildly the same way over three readings each, so a threshold can't separate them either. Only the sustained trend does, and seeing it means reading the whole trace.

Then the harder half. VS-1001 is a serial number. To be useful the answer has to say Kerry Allen, room ICU-301, Intensive Care Unit, Lamna Healthcare Regional Medical Center, Seattle, and each of those facts lives in a different table. That sentence is the whole ontology read aloud: the time-series binding gets you the trace, the four relationship bindings get you the name, the room, the ward and the building. Remove any one and the answer degrades back to a serial number.

A data agent pointed at LamnaHealthcareOntology turns the question into that sentence. Two articles here build one, and both apply unchanged:

  • The City Cart e-commerce build creates an agent, watches it answer confidently and wrongly, then fixes it with instructions. The shortest demonstration of why 5.4 matters.
  • The pharma cold-chain build goes further into time-series questions and adds an operations agent that watches without being asked.

Clean up

To remove everything: select the workspace, Workspace settings, General, Remove this workspace. On a paid capacity, pause it in the Azure portal too — removing the workspace stops the items, not the capacity.

Fresher-to-pro glossary

Plain words in this articleOfficial name
A model of how tables connect, for reportssemantic model; Direct Lake when it reads OneLake tables in place
A model of how tables connect, for traversal and agentsontology, the Ontology (preview) item
One kind of thing the business hasentity type; each real row is an instance
Reading a semantic model and restating it as an ontologyGenerate Ontology
The property whose value picks out exactly one instanceentity type key
The property whose value labels an instance on screendisplay name property
Wiring an entity type to the table holding its rowsbinding
The table that proves which instance connects to whichthe child table, the one carrying the foreign key
Data that stays true until someone changes itstatic data, in a lakehouse
Data where every value carries the moment it was measuredtime-series data, in an eventhouse
A property whose values each carry a timestamptime-series property, created by the binding, never declared before it
The saved network of nodes and edges built from the bindingsthe materialized graph, in the Graph in Microsoft Fabric item
The language the graph speaksGQL (Graph Query Language), the ISO standard; not GraphQL
The agent you ask questions in plain languagedata agent

Discuss this post