Getting Started with M Code
What M code is, where to find it, and how to start reading and writing it — for Power BI users who have only ever used the UI.
Every time you click a button in the Power Query Editor — filter rows, add a column, change a type — Power Query generates M code behind the scenes. You may not have noticed, but that code is saved with your query and runs every time you refresh. M is the language underneath the UI.
This guide explains how to find that code, read it, and start modifying it directly.
Opening the Advanced Editor
In Power BI Desktop:
- Open the Power Query Editor (Home → Transform Data)
- Select any query in the Queries pane on the left
- Click Home → Advanced Editor
You'll see the full M code for that query — every Applied Step listed as a named variable in a let block.
The Applied Steps pane on the right is just a visual representation of this same code. Each step in the pane corresponds to one line in the let block. Clicking a step in the pane highlights it in the code, and vice versa.
Reading a Generated Query
When you load a CSV and apply a few transformations, the Advanced Editor shows something like this:
let
Source = Csv.Document(File.Contents("C:\data\sales.csv"), [Delimiter=","]),
#"Promoted Headers" = Table.PromoteHeaders(Source),
#"Filtered Rows" = Table.SelectRows(#"Promoted Headers", each [Region] = "East"),
#"Removed Columns" = Table.RemoveColumns(#"Filtered Rows", {"Discount"})
in
#"Removed Columns"Reading top to bottom:
Source— load the CSV file from disk#"Promoted Headers"— use the first row as column headers. The#"..."quoting syntax lets step names contain spaces — that's a Power Query convention, not required in hand-written code#"Filtered Rows"— keep only rows where the Region column equals "East"#"Removed Columns"— drop the Discount columnin #"Removed Columns"— return the last step as the query's output
Each step takes the output of the previous one as its input. The let...in structure is how every M query is organized: a set of named steps, followed by a declaration of which step to return.
Your First Edit
The most common first edit: change a hardcoded value. Instead of going back to the UI and regenerating a filter step, edit the M directly:
// Before — generated by the UI
#"Filtered Rows" = Table.SelectRows(#"Promoted Headers", each [Region] = "East")
// After — changed by hand
#"Filtered Rows" = Table.SelectRows(#"Promoted Headers", each [Region] = "West")Click Done and the query refreshes with the new filter. That's your first M code change.
Debugging with Intermediate Steps
You can temporarily change the in clause to return any step — not just the last one:
let
Source = Csv.Document(File.Contents("C:\data\sales.csv"), [Delimiter=","]),
#"Promoted Headers" = Table.PromoteHeaders(Source),
#"Filtered Rows" = Table.SelectRows(#"Promoted Headers", each [Region] = "East"),
#"Removed Columns" = Table.RemoveColumns(#"Filtered Rows", {"Discount"})
in
#"Promoted Headers" // ← return this step to inspect itThe preview pane shows the table at that stage. This is the fastest way to isolate where a transformation goes wrong.
When the UI Isn't Enough
You'll reach the limits of the UI eventually. Common scenarios where writing M directly is the better path:
- Dynamic filter values — filtering based on a value pulled from another table, rather than a hardcoded string
- Complex conditional columns — the UI's Conditional Column dialog handles simple if/else cases; M handles nested logic, multiple conditions, and computed values
- Iterating over a list — running the same transformation for each item in a list, then combining the results
- Tweaking generated code — adjusting arguments that the UI doesn't expose, like changing a merge from a Left Outer join to an Inner join
- Reusable logic — writing a custom function once and calling it across multiple queries
The next step is From the UI to M Code — a side-by-side translation guide for the ten most common UI operations.