Undocumented Power Query pipelines become maintenance nightmares fast. Learn how to use step naming conventions, inline M comments, query descriptions, and metadata records to build queries that explain themselves — to colleagues, auditors, and future you.

You've inherited a Power BI report from a colleague who left the company six months ago. The query panel shows a dozen queries, each named "Query1" through "Query12." Inside each one, the steps are labeled with Power Query's auto-generated names: #"Changed Type", #"Changed Type1", #"Changed Type2", #"Filtered Rows1". You have a refresh failure and no idea where to start. The M code is 200 lines long, undocumented, and reading it feels like archaeology.
This scenario plays out constantly in real organizations. Power Query makes it easy to build sophisticated data pipelines quickly — but that same speed encourages habits that make queries nearly impossible to maintain. The good news is that M has a robust set of documentation tools built right into the language. You can write queries that explain themselves: named steps that describe their purpose, inline comments that capture your reasoning, query-level descriptions that orient anyone who opens the file, and structured naming conventions that make your query list scannable at a glance.
By the end of this lesson, you'll be able to audit an existing query and bring it up to professional documentation standards, build new queries with self-describing patterns from the start, and establish team conventions that scale as your Power Query work grows.
What you'll learn:
You should be comfortable navigating the Power Query editor and understand the basics of M formula syntax. If you haven't yet worked with the M formula language directly, take a look at Understanding the M Formula Language: Syntax, Data Types, and Expression Basics before continuing — we'll be editing M code directly throughout this lesson. You should also have a general familiarity with how queries load and refresh, as covered in Power Query 101: Connect, Transform, Load.
Before we get hands-on, let's establish something: documentation in Power Query is not a nicety — it's a professional requirement. Here's why it's more critical here than in most tools.
Power Query sits at the front of your data pipeline. Every downstream calculation, every report, every business decision depends on the transformations happening inside your queries. When something breaks — and it will — the person diagnosing the issue may be you, six months from now, with no memory of why you added that particular filter on line 47. Or it may be a colleague who's never seen this file before. Or it may be an auditor asking why certain records are excluded from a regulatory report.
There's a second reason: Power Query's auto-generated step names are actively misleading. When you apply a type change, Power Query calls it #"Changed Type". When you apply a second one, it calls it #"Changed Type1". These names tell you what operation was applied at a generic level, but nothing about which columns were affected, why it was necessary, or what business rule it implements. You end up with five steps that look identical in the Applied Steps pane, forcing you to click each one individually to understand the data state at that point.
Good documentation collapses that investigation time from minutes to seconds. It also forces you to think more clearly about your transformations — if you can't write a one-line description of what a step does, that's often a sign the step is doing too much.
Key insight
Documentation in Power Query serves three audiences simultaneously: your future self, your colleagues, and anyone auditing or troubleshooting the pipeline. Writing for all three forces a level of clarity that also improves the query design itself.
Power Query's M language gives you four distinct places to put documentation. Let's understand what each one is before we start applying them.
1. Step names — Every let binding in an M expression is a step name. Power Query displays these in the Applied Steps pane. When you rename a step in the GUI, you're changing the binding name in the underlying M code.
2. Inline comments — M supports both single-line (//) and block (/* */) comments. These appear in the Advanced Editor and are invisible in the GUI Applied Steps pane. They're for developers reading the code directly.
3. Query descriptions — Power Query allows you to attach a plain-text description to each query. In Excel, this appears in the Query Properties dialog. In Power BI Desktop, it appears in the Properties pane on the right side of the Advanced Editor. This is the highest-level documentation — the "what does this query do" statement.
4. Documentation records — M has a Value.Metadata function and a pattern of attaching structured metadata records to values. This is advanced, but we'll cover it because it enables genuinely powerful self-describing query architectures.
Step names are the most visible form of documentation. They appear in the Applied Steps pane without any extra navigation, so they're the first thing anyone sees when they open your query. Investing a few seconds per step in a meaningful name pays significant dividends.
Open the Advanced Editor on a freshly built query and you'll typically see something like this:
let
Source = Excel.Workbook(File.Contents("C:\Data\sales_2024.xlsx"), null, true),
sales_2024_Sheet = Source{[Item="sales_2024",Kind="Sheet"]}[Data],
#"Promoted Headers" = Table.PromoteHeaders(sales_2024_Sheet, [PromoteAllScalars=true]),
#"Changed Type" = Table.TransformColumnTypes(#"Promoted Headers",{
{"order_id", type text},
{"order_date", type date},
{"amount", type number}
}),
#"Filtered Rows" = Table.SelectRows(#"Changed Type", each [amount] > 0),
#"Changed Type1" = Table.TransformColumnTypes(#"Filtered Rows",{
{"region", type text}
}),
#"Renamed Columns" = Table.RenameColumns(#"Changed Type1",{
{"order_date", "OrderDate"},
{"order_id", "OrderID"},
{"amount", "SaleAmount"}
})
in
#"Renamed Columns"
Can you tell at a glance why there are two Changed Type steps? Or what the filter on amount is accomplishing — is it removing refunds, excluding test orders, or implementing a business rule about minimum order size? You can't. You have to click each step and read the formula bar.
Good step names follow a consistent pattern across your team. Here's a convention I've seen work well in production environments:
Format: [Verb]_[Subject]_[Detail]
Promote, Cast, Filter, Rename, Add, Remove, Merge, Pivot)Headers, OrderDate, NegativeAmounts, RegionColumn)ToDate, BusinessRule, ForReporting)Applying this to our example:
let
Source = Excel.Workbook(File.Contents("C:\Data\sales_2024.xlsx"), null, true),
RawSheet = Source{[Item="sales_2024",Kind="Sheet"]}[Data],
PromoteHeaders = Table.PromoteHeaders(RawSheet, [PromoteAllScalars=true]),
CastCoreColumns_ToCorrectTypes = Table.TransformColumnTypes(PromoteHeaders,{
{"order_id", type text},
{"order_date", type date},
{"amount", type number}
}),
FilterRows_RemoveNegativeAmounts = Table.SelectRows(CastCoreColumns_ToCorrectTypes,
each [amount] > 0
),
CastRegionColumn_ToText = Table.TransformColumnTypes(FilterRows_RemoveNegativeAmounts,{
{"region", type text}
}),
RenameColumns_ForReportingLayer = Table.RenameColumns(CastRegionColumn_ToText,{
{"order_date", "OrderDate"},
{"order_id", "OrderID"},
{"amount", "SaleAmount"}
})
in
RenameColumns_ForReportingLayer
Now the Applied Steps pane tells a coherent story. Someone can read the step list from top to bottom and understand the transformation pipeline without clicking a single step.
Note
M step names that don't contain spaces or special characters don't need the #"..." wrapper. Prefer names without spaces so your M code is cleaner. If you do need a space for readability, use the #"Step Name With Spaces" syntax — but try to avoid it.
In addition to the verb-subject-detail pattern, consider prefixing steps by their category when you're working on long, complex queries:
| Prefix | Purpose | Example |
|---|---|---|
Src_ |
Source connection steps | Src_ConnectToSalesDB |
Raw_ |
Steps that represent raw, unchanged data | Raw_AllOrders |
Cast_ |
Type casting operations | Cast_DateColumns |
Filter_ |
Row filtering | Filter_CurrentYear |
Add_ |
Added columns | Add_ProfitMarginColumn |
Rename_ |
Column renaming | Rename_ForDataWarehouse |
Final_ |
The last step before output | Final_Output |
This categorized approach is especially useful when a query has 20+ steps and you need to navigate to a specific section quickly.
Tip
You can rename steps directly in the Applied Steps pane without going into the Advanced Editor. Right-click any step and select "Rename." This is the fastest workflow for cleaning up step names during or after query construction.
Step names tell you what is happening. Comments tell you why. This distinction is critical. A step named Filter_RemoveNegativeAmounts describes the operation. A comment explains the business reason: "Negative amounts represent system-generated reversal entries from the legacy ERP. They're excluded here because the Finance team confirmed they're handled in a separate reconciliation report."
That context cannot live in a step name. It needs a comment.
Use // for short explanations following a formula:
FilterRows_RemoveNegativeAmounts = Table.SelectRows(CastCoreColumns_ToCorrectTypes,
each [amount] > 0 // Exclude ERP reversal entries (Finance confirmed: see ticket DS-2847)
),
Or above a step when the comment is longer than a trailing note warrants:
// Per the 2024-Q1 data governance decision, region codes must be cast explicitly
// to text because the source system occasionally sends numeric region codes (e.g., "10")
// which Power Query infers as integers. This causes downstream join failures.
CastRegionColumn_ToText = Table.TransformColumnTypes(FilterRows_RemoveNegativeAmounts,{
{"region", type text}
}),
Use /* */ for multi-line documentation blocks that introduce sections of a long query:
let
/* ============================================================
QUERY: SalesOrders_Cleaned
PURPOSE: Connects to the raw sales export, applies type
corrections, removes ERP reversal entries, and
renames columns to the reporting layer standard.
OWNER: Data Engineering Team
LAST MODIFIED: 2024-11-15
CHANGE LOG:
2024-11-15 - Added region cast fix (ticket DS-2921)
2024-09-03 - Initial build
============================================================ */
Source = Excel.Workbook(File.Contents("C:\Data\sales_2024.xlsx"), null, true),
/* -- SOURCE EXTRACTION -- */
RawSheet = Source{[Item="sales_2024",Kind="Sheet"]}[Data],
PromoteHeaders = Table.PromoteHeaders(RawSheet, [PromoteAllScalars=true]),
/* -- TYPE CASTING -- */
CastCoreColumns_ToCorrectTypes = Table.TransformColumnTypes(PromoteHeaders,{
{"order_id", type text},
{"order_date", type date},
{"amount", type number}
}),
/* -- BUSINESS RULE FILTERS -- */
// Exclude ERP reversal entries (Finance confirmed: see ticket DS-2847)
FilterRows_RemoveNegativeAmounts = Table.SelectRows(CastCoreColumns_ToCorrectTypes,
each [amount] > 0
),
/* -- ADDITIONAL TYPE CORRECTIONS -- */
// Must cast explicitly - source occasionally sends numeric region codes
// causing downstream join failures (see ticket DS-2921)
CastRegionColumn_ToText = Table.TransformColumnTypes(FilterRows_RemoveNegativeAmounts,{
{"region", type text}
}),
/* -- OUTPUT FORMATTING -- */
RenameColumns_ForReportingLayer = Table.RenameColumns(CastRegionColumn_ToText,{
{"order_date", "OrderDate"},
{"order_id", "OrderID"},
{"amount", "SaleAmount"}
})
in
RenameColumns_ForReportingLayer
This is what a professionally documented query looks like. The block comment at the top is the "cover page" — it tells anyone opening the Advanced Editor who owns this query, what it does, and what's changed recently. Section dividers help navigate long queries. Inline comments capture the business reasoning that can't be inferred from the code alone.
Warning
Comments in M are only visible in the Advanced Editor. They do not appear in the Applied Steps pane, the Query Properties dialog, or anywhere in the report surface. If you need documentation visible to report consumers or business users, use query descriptions (covered next) or a dedicated documentation query.
Query descriptions are the most visible, end-user-friendly form of documentation in Power Query. They're accessible without opening the Advanced Editor, making them ideal for communicating to colleagues who aren't comfortable in M code.
In Power BI Desktop:
In Excel (Power Query Editor):
The description field accepts plain text — no markdown, no formatting. Keep it clear and concise.
Think of the query description as answering these five questions:
A real example:
Cleans raw Salesforce order export for use in Sales Dashboard and Returns reconciliation.
Source: C:\Data\sales_2024.xlsx (monthly drop from Salesforce admin).
Excludes ERP reversal entries per Finance governance decision (2024-Q1).
Owner: Data Engineering Team. Last major change: 2024-11-15 (region cast fix, DS-2921).
That's 56 words. Anyone can read it in 15 seconds and know exactly what they're looking at before opening the Advanced Editor.
Query names are visible at the report level — they become table names in your data model. A consistent naming convention for queries is just as important as step naming. Consider this structure:
| Pattern | Meaning | Example |
|---|---|---|
DIM_ |
Dimension table (reference data) | DIM_Products |
FACT_ |
Fact table (transactional data) | FACT_SalesOrders |
STAGE_ |
Staging query (not loaded to model) | STAGE_RawSalesExport |
PARAM_ |
Parameter query | PARAM_ReportingYear |
FN_ |
Function | FN_ParseProductCode |
REF_ |
Reference/helper query | REF_RegionMapping |
This connects directly to the multi-stage architecture concept — if you're building multi-stage staging architectures in Power Query, your naming convention for queries should mirror your layer structure. STAGE_ queries become your raw layer; DIM_ and FACT_ queries are your conformed layer.
Tip
In Power BI, the query name becomes the table name in your data model. Using prefixes like DIM_ and FACT_ helps both in Power Query maintenance and in DAX development — RELATED(DIM_Products[Category]) is much clearer than RELATED(Sheet1[Category]).
For teams that want documentation they can query and report on — not just read — M's metadata system offers a powerful option. You can attach a structured record to any value in M using Value.Metadata, and retrieve it later with Value.Metadata().
This is admittedly an advanced pattern. But it enables something remarkable: documentation that the query itself can expose programmatically.
let
Source = Excel.Workbook(File.Contents("C:\Data\sales_2024.xlsx"), null, true),
RawSheet = Source{[Item="sales_2024",Kind="Sheet"]}[Data],
PromoteHeaders = Table.PromoteHeaders(RawSheet, [PromoteAllScalars=true]),
CastCoreColumns_ToCorrectTypes = Table.TransformColumnTypes(PromoteHeaders,{
{"order_id", type text},
{"order_date", type date},
{"amount", type number}
}),
FilterRows_RemoveNegativeAmounts = Table.SelectRows(CastCoreColumns_ToCorrectTypes,
each [amount] > 0
),
RenameColumns_ForReportingLayer = Table.RenameColumns(FilterRows_RemoveNegativeAmounts,{
{"order_date", "OrderDate"},
{"order_id", "OrderID"},
{"amount", "SaleAmount"}
}),
// Attach structured metadata for programmatic documentation
QueryMetadata = [
QueryName = "FACT_SalesOrders",
Description = "Cleaned sales orders from annual Salesforce export",
Owner = "Data Engineering Team",
Source = "C:\Data\sales_2024.xlsx",
LastModified = #date(2024, 11, 15),
BusinessRules = {
"Excludes ERP reversal entries (negative amounts)",
"Region cast explicitly to text to prevent join failures"
},
TicketReferences = {"DS-2847", "DS-2921"}
],
Final_Output = RenameColumns_ForReportingLayer
meta QueryMetadata
in
Final_Output
You can then retrieve this metadata from another query:
let
TargetQuery = FACT_SalesOrders,
Metadata = Value.Metadata(TargetQuery),
// Metadata is now a record you can navigate
Owner = Metadata[Owner], // "Data Engineering Team"
BusinessRules = Metadata[BusinessRules] // A list of rule descriptions
in
Metadata
This enables you to build a documentation catalog — a query that iterates over all your queries, extracts their metadata records, and presents a structured inventory of your entire data model. It's overkill for a five-query workbook, but for an enterprise model with 50+ queries, it's invaluable.
Note
Value.Metadata metadata is not preserved through all transformations. If you pass a table through additional M operations after attaching metadata, the metadata may be stripped. Attach it as the final step before output to ensure it persists.
One of the most practical documentation patterns is a dedicated query that reports on the structure of your other queries. This is especially useful during the audit phase — when you're inheriting an undocumented model and need to understand what you're working with.
Here's an audit query that produces a summary of all non-function, non-parameter queries in your model:
let
// Get a list of all queries in the current workbook/model
// Note: This pattern works in Excel. In Power BI, query introspection
// is more limited and typically requires manual enumeration.
// For a manual documentation inventory, structure it like this:
QueryInventory = #table(
type table [
QueryName = text,
Category = text,
Description = text,
SourceSystem = text,
Owner = text,
LoadedToModel = logical,
LastReviewed = date,
Notes = text
],
{
{"FACT_SalesOrders", "Fact",
"Cleaned sales orders from annual Salesforce export",
"Salesforce/Excel", "Data Engineering", true,
#date(2024, 11, 15), "Excludes ERP reversals"},
{"DIM_Products", "Dimension",
"Product master from ERP system",
"SAP ERP", "Data Engineering", true,
#date(2024, 10, 30), "Snapshot as of month-end"},
{"STAGE_RawSalesExport", "Staging",
"Raw Salesforce export before cleaning",
"Salesforce/Excel", "Data Engineering", false,
#date(2024, 11, 15), "Not loaded; reference only"},
{"PARAM_ReportingYear", "Parameter",
"Controls which year is loaded",
"N/A", "Data Engineering", false,
#date(2024, 11, 1), "Updated each January"}
}
)
in
QueryInventory
This query produces a proper table that you can load to your data model, use in a Power BI report page titled "Data Model Documentation," and share with stakeholders who want to understand what data is in the model and where it comes from.
It's a living document — every time you add or significantly change a query, you update this inventory. That discipline, more than any technical feature, is what separates maintainable models from the "inherited nightmare" scenario we opened with.
Now let's talk about the scenario you're most likely to face in the real world: inheriting an existing undocumented query and bringing it up to standard. Here's a systematic process.
Before renaming a single step, spend time understanding what the query actually does. Click through each step in the Applied Steps pane, reading the formula bar at each one and noting:
Take notes. A simple text file works fine. You're doing archaeology, and you need to record what you find before you start rearranging the site.
Once you understand the query, rename steps one by one using the Applied Steps pane. Right-click each step and select Rename. Follow your naming convention. Don't edit any formulas yet — just rename.
This step alone often reveals something important: when you're forced to name every step, you sometimes discover that two consecutive steps are doing the same thing, or that a step's purpose is genuinely unclear even after reading the formula. Those are red flags worth investigating.
After renaming all steps, open the Advanced Editor. The step names are now meaningful, but the code still lacks the why. Go through each non-obvious step and add a comment explaining the business reason. Reference ticket numbers, decisions, source system quirks, or data quality issues.
For steps that are straightforward (like a simple column rename), a comment may not be necessary. Focus your commenting effort on:
In the Query Properties dialog (Excel) or Properties pane (Power BI), write a clear query description using the five-question framework: what, where, who, why, and when.
If you're maintaining a documentation catalog query, update it to reflect this query's current state.
Warning
When renaming steps, Power Query automatically updates all references to that step name in subsequent steps. However, if you have other queries that reference specific steps from this query (using the QueryName{[Step="..."]} pattern), those references will break. Always check for cross-query step references before renaming, especially in complex models built around query dependencies and evaluation order.
Let's put this all together. You'll take an undocumented query and bring it to professional documentation standards.
Open Power Query in Excel or Power BI and create a new blank query. Paste the following into the Advanced Editor:
let
Source = Csv.Document(
File.Contents("C:\ExerciseData\employee_salaries.csv"),
[Delimiter=",", Columns=6, Encoding=1252, QuoteStyle=QuoteStyle.None]
),
#"Promoted Headers" = Table.PromoteHeaders(Source, [PromoteAllScalars=true]),
#"Changed Type" = Table.TransformColumnTypes(#"Promoted Headers",{
{"employee_id", type text},
{"hire_date", type date},
{"salary", type number},
{"department", type text},
{"status", type text},
{"bonus_pct", type number}
}),
#"Filtered Rows" = Table.SelectRows(#"Changed Type", each [status] = "Active"),
#"Added Custom" = Table.AddColumn(#"Filtered Rows", "Custom",
each [salary] * (1 + [bonus_pct] / 100)
),
#"Changed Type1" = Table.TransformColumnTypes(#"Added Custom",{
{"Custom", type number}
}),
#"Renamed Columns" = Table.RenameColumns(#"Changed Type1",{
{"employee_id", "EmployeeID"},
{"hire_date", "HireDate"},
{"salary", "BaseSalary"},
{"department", "Department"},
{"status", "EmploymentStatus"},
{"bonus_pct", "BonusPct"},
{"Custom", "TotalCompensation"}
}),
#"Filtered Rows1" = Table.SelectRows(#"Renamed Columns",
each [BaseSalary] > 30000
)
in
#"Filtered Rows1"
Task 1 — Rename all steps using the verb-subject-detail naming convention. Before you rename, click through each step and understand what it does. Name the query FACT_EmployeeCompensation.
Task 2 — Add a block comment at the top of the query body with:
Task 3 — Add inline comments explaining:
BaseSalary > 30000 (again, reasonable assumption)Task 4 — Add a query description in the Query Properties dialog using the five-question framework.
Task 5 (advanced) — Attach a metadata record to the final output with at minimum: QueryName, Description, Owner, BusinessRules (as a list), and LastModified.
Here's what a well-documented version of this query should look like:
let
/* ============================================================
QUERY: FACT_EmployeeCompensation
PURPOSE: Active employee salary data with calculated total
compensation (base + bonus), filtered to full-time
compensation thresholds for the HR dashboard.
SOURCE: C:\ExerciseData\employee_salaries.csv
(Monthly export from HR system)
OWNER: [Your Name]
LAST MODIFIED: 2024-11-20
============================================================ */
Src_CSVFile = Csv.Document(
File.Contents("C:\ExerciseData\employee_salaries.csv"),
[Delimiter=",", Columns=6, Encoding=1252, QuoteStyle=QuoteStyle.None]
),
/* -- SOURCE EXTRACTION -- */
Raw_AllEmployees = Table.PromoteHeaders(Src_CSVFile, [PromoteAllScalars=true]),
/* -- TYPE CASTING -- */
Cast_AllColumns_ToCorrectTypes = Table.TransformColumnTypes(Raw_AllEmployees,{
{"employee_id", type text},
{"hire_date", type date},
{"salary", type number},
{"department", type text},
{"status", type text},
{"bonus_pct", type number}
}),
/* -- BUSINESS RULE FILTERS -- */
// Exclude terminated and on-leave employees per HR policy:
// compensation reporting covers active headcount only
Filter_ActiveEmployees = Table.SelectRows(Cast_AllColumns_ToCorrectTypes,
each [status] = "Active"
),
/* -- CALCULATED COLUMNS -- */
// Total compensation = base salary + bonus percentage of base
// Formula per Total Rewards team spec (HR-SPEC-2024-07)
Add_TotalCompensation = Table.AddColumn(Filter_ActiveEmployees, "TotalCompensation",
each [salary] * (1 + [bonus_pct] / 100)
),
Cast_TotalCompensation_ToNumber = Table.TransformColumnTypes(Add_TotalCompensation,{
{"TotalCompensation", type number}
}),
/* -- OUTPUT FORMATTING -- */
Rename_AllColumns_ForReportingLayer = Table.RenameColumns(Cast_TotalCompensation_ToNumber,{
{"employee_id", "EmployeeID"},
{"hire_date", "HireDate"},
{"salary", "BaseSalary"},
{"department", "Department"},
{"status", "EmploymentStatus"},
{"bonus_pct", "BonusPct"}
}),
// Exclude interns and contractors below minimum full-time threshold
// Per Finance: compensation analytics only cover roles >= $30,000 annual
Filter_FullTimeThreshold = Table.SelectRows(Rename_AllColumns_ForReportingLayer,
each [BaseSalary] > 30000
),
Final_Output = Filter_FullTimeThreshold
meta [
QueryName = "FACT_EmployeeCompensation",
Description = "Active employee compensation with bonus, filtered to full-time threshold",
Owner = "Your Name",
BusinessRules = {
"Active employees only (HR policy)",
"Total compensation = base * (1 + bonus_pct/100)",
"Minimum salary threshold $30,000 (Finance governance)"
},
LastModified = #date(2024, 11, 20)
]
in
Final_Output
When you rename a step, Power Query updates references to that step within the same query. But if another query references an intermediate step using syntax like SalesOrders{0} or connects to the query in a way that expects specific behavior, you may introduce subtle issues. Always check your query dependencies after a major renaming exercise. The article on query dependencies and evaluation order covers how to trace these connections.
If you rename a step through the GUI and include spaces, Power Query automatically wraps the name in #"...". But if you're editing M directly and write a step name with spaces without the wrapper, the Advanced Editor will throw a parse error. Either eliminate spaces from step names or consistently use the #"..." syntax. Avoiding spaces is cleaner.
// Filter rows where status equals "Active"
Filter_ActiveEmployees = Table.SelectRows(..., each [status] = "Active"),
The step name already tells you what this does. The comment adds zero information. A useful comment:
// HR policy: terminated and on-leave employees are excluded from
// compensation reporting. See HR-POL-2024-03 for the governance decision.
Filter_ActiveEmployees = Table.SelectRows(..., each [status] = "Active"),
A query description or header comment that says "Last modified: 2023-01-10" and it's now late 2024 is worse than no documentation — it actively misleads. Either commit to updating documentation when you change the query, or leave timestamps out entirely and rely on version control (Git, SharePoint version history, or Power BI Desktop's autosave history) for change tracking.
You don't need a comment explaining Table.PromoteHeaders. The step name PromoteHeaders is sufficient. Reserve comments for non-obvious decisions, business rules, and source system quirks. Over-documenting creates noise that makes the genuinely important comments harder to find.
Key insight
Documentation that explains the what is noise; documentation that explains the why is signal. When writing a comment, ask yourself: "Could someone figure this out from the code alone?" If yes, the comment probably isn't needed. If no, it's essential.
Documentation doesn't exist in isolation from other Power Query best practices. A well-documented query is usually also a well-structured one — and if you find yourself unable to write a clear step name or comment, that's often a signal that the query design needs improvement.
For example, if you're building complex parameterized pipelines, documentation becomes critical for understanding which parameter controls which behavior. The article on parameterized queries and dynamic data sources covers how to build these systems — but without clear naming and comments, a parameterized query becomes nearly impossible to maintain.
Similarly, when you're cleaning messy data with nulls, errors, and type conversions, the business rules you're implementing — what counts as an acceptable null, what error is expected versus unexpected — are exactly the kind of reasoning that belongs in comments, not code. The code shows the implementation; the comment explains the decision.
If you're building reusable function libraries, documentation becomes even more important. A function without documentation is a black box. The article on building reusable Power Query function libraries covers a documentation pattern specific to functions: the Function.From metadata pattern, which lets you attach parameter descriptions and return type documentation directly to a function value.
And finally, for teams working at scale, good documentation is the foundation of the Power Query best practices for maintainable ETL that keep models manageable as they grow from a handful of queries to dozens.
Let's recap what you've built in this lesson.
Step naming conventions turn the Applied Steps pane into a navigable summary of your transformation pipeline. The verb-subject-detail pattern (Filter_RemoveNegativeAmounts, Cast_RegionColumn_ToText) gives every step a name that carries meaning without requiring you to open the formula bar.
Inline comments capture the why behind decisions — business rules, source system quirks, governance decisions, ticket references. Single-line // comments work for brief annotations; block /* */ comments work for section headers and multi-line explanations.
Query descriptions are the highest-visibility documentation, readable without opening the Advanced Editor. Use the five-question framework (what, where, who, why, when) to write descriptions that orient any reader in under 30 seconds.
Metadata records enable programmatic documentation — queries that describe themselves in a machine-readable format that other queries can consume to build documentation catalogs.
The audit process — understand, rename, comment, describe, catalog — gives you a repeatable workflow for bringing any inherited query up to professional standards.
Apply this immediately to one real query. Pick the most complex query in your current model and walk through the full audit process. One documented query is worth ten documented in theory.
Establish team conventions. If you work with others, share your naming convention and get agreement before everyone diverges. Consistency across a team is more valuable than any individual convention being "perfect."
Explore the advanced topics. If you're building at scale, read about understanding query dependencies and evaluation order to understand how your documented queries relate to each other, and debugging and error handling in M to see how documentation practices intersect with robust error handling patterns.
The difference between a query that causes a three-hour debugging session and one that causes a three-minute fix is almost always documentation. Start writing it now, while the context is fresh — because future you (and your colleagues) will be genuinely grateful.