Every undocumented Power BI model I've inherited told the same story: whoever built it understood it perfectly, and that understanding left the organization the day they did. Documentation gets skipped for a predictable reason — it doesn't show up in a demo, there's always a more visible task competing for the same hour, and the person writing it already knows everything it would contain, so it feels redundant right up until someone else has to open the file.

The excuse that doesn't hold

"The model is self-explanatory" is true for exactly one person: whoever built it, while they still remember why. A relationship set to single-direction filtering instead of bidirectional was a deliberate decision to avoid ambiguous filter propagation somewhere — six months later, with no note attached, it just looks like an oversight, and the next developer either leaves it alone out of caution or "fixes" it and reintroduces the exact bug it was preventing. Documentation isn't about explaining what the model does — Power BI's own UI mostly shows you that. It's about capturing why, which nothing in the tool captures automatically.

Relationships: cardinality and filter direction, not just the diagram

The model view shows what's connected to what. It does not explain why a relationship is one-to-many instead of many-to-many, or why cross-filter direction is set to single instead of both. Those decisions usually exist to prevent a specific problem — ambiguous filter paths, unintended double-counting, a bridge table introduced deliberately to break a many-to-many relationship into two clean one-to-many relationships. Document the reasoning next to the decision, not just the resulting diagram:

  • Which relationships are intentionally single-direction, and what would break if someone flipped them to bidirectional.
  • Where bridge tables exist and which many-to-many relationship they're resolving.
  • Any relationship deliberately left inactive, and which measures activate it via USERELATIONSHIP — this one especially, because an inactive relationship with no explanation looks like a mistake waiting to be "corrected."

Measure logic: the DAX itself is necessary, not sufficient

Reading a measure's DAX tells you what it computes. It rarely tells you what business rule it's encoding. A measure that excludes a specific transaction type, or filters out intercompany rows, or applies a currency conversion using a specific rate table, is enforcing a decision that came from a conversation with the business, not from the data itself. Whoever encoded that rule five months ago knows exactly why it's there. The next person sees a filter condition with no context and has to reverse-engineer the business logic from the code, or worse, guess.

I use the measure's description property for exactly this — not a restatement of the DAX, but the business rule behind it:

Net Revenue :=
CALCULATE(
    SUM('Sales'[Amount]),
    'Sales'[TransactionType] <> "Intercompany",
    'Sales'[IsVoided] = FALSE()
)

The description on that measure isn't "sums the Amount column" — that's obvious from reading it. It's "Excludes intercompany transactions and voided sales per finance's reporting standard; matches the number in the monthly close package, not the raw transactional total." That sentence is the part that took a meeting to establish and takes five seconds to lose forever if it's never written down.

Data lineage: where a number actually comes from

"Where does this column come from" is one of the most common questions from a new developer, and one of the hardest to answer by inspection once a model has gone through a few iterations of Power Query transformations. At minimum, document:

  • Source system and table or view for each fact and dimension — not just "SQL Server," but which database, which schema, which object.
  • Any non-obvious transformation applied in Power Query — a filter that drops certain rows, a merge that could silently produce duplicates if the join cardinality assumption is ever wrong, a column split or conversion that encodes a business rule rather than a pure data-type fix.
  • Refresh dependencies — if this model depends on a dataflow, another dataset via a composite model, or a specific upstream ETL job completing first, that dependency needs to be written down somewhere other than in the head of whoever set up the refresh schedule.

RLS rules: document these like security configuration, because they are

RLS logic buried only in a role's filter expression is a liability, not just an inconvenience. If a role's DAX filter is wrong — too permissive or too restrictive — nobody notices from looking at the model unless they know exactly what access that role is supposed to grant in the first place. Document, per role: which users or groups get assigned to it, what data they're intended to see (in plain business terms, not DAX), and what the actual filter expression is. Keep this current every time a role changes, and treat a discrepancy between the documented intent and the actual filter as a bug, because from a security standpoint, that's exactly what it is.

Lightweight approaches that survive contact with reality

Heavyweight documentation processes fail because they're the first thing skipped under deadline pressure. What actually survives:

  • Description properties on tables, columns, and measures — they live inside the model itself, so they can't go stale in a separate document nobody opens, and they surface directly in the field list as tooltips.
  • Tabular Editor's scripting and its model documentation export — a short script run against the model produces a reasonably complete inventory of tables, measures, and relationships without anyone hand-typing a spec.
  • A short README at the workspace level — not a full design document, just: what this model is for, who owns it, where the source data lives, and any known quirks. Two paragraphs beats a thirty-page document nobody maintains.
  • Comments inside complex DAX itself, using // for short notes or /* */ for longer ones, directly at the step of the calculation they explain — useful the moment someone opens the formula bar, which is exactly when they need it.
The documentation that survives is the kind that lives where people are already looking — in the model's own metadata, in the formula itself — not in a wiki page that's accurate the day it's written and wrong a year later.

None of this needs to be exhaustive to be worth doing. A model with description properties filled in, RLS intent written down next to the filter logic, and a two-paragraph README will save the next developer hours the first time they touch it — and spare you the message asking you to explain a decision you made eight months ago and can no longer fully remember either.