Parameters, Query Dependencies & Intro to M
The step up from clicking buttons to building something maintainable. Parameters make queries portable, references and groups keep a growing file navigable, the Advanced Editor shows you what has been happening all along, and custom functions let you write a transformation once and apply it everywhere.
Learning Objective
Everything so far has been achievable through the interface. This lesson is about the structure that makes a twenty-query file comprehensible rather than a maze.
- Create and use parameters for paths, environments and thresholds
- Organise queries with references, duplicates and groups, and know the difference
- Read the Advanced Editor confidently and understand M's let/in structure
- Write a custom function and invoke it across a table
- Structure a query file so someone else can maintain it
Why This Matters
A file with three queries needs no organisation. A file with twenty-five does, and every real project ends up with twenty-five.
Without structure you get: hard-coded paths that break on anyone else's machine, the same cleaning logic copy-pasted into six queries that then drift apart, a flat list where staging queries and output tables are indistinguishable, and no way to tell which query feeds which.
If not, the file is yours alone — and that is a liability the moment you are on leave.
Concept
Parameters
Home › Manage Parameters › New Parameter. A named value you reference in queries instead of hard-coding it.
| Use | Example | Benefit |
|---|---|---|
| File and folder paths | DataFolder | The file works on any machine; a .pbit prompts for it on open |
| Server and database names | SqlServer, Database | Switch between dev, test and production without editing queries |
| Date ranges | StartDate | Limit the data loaded during development, widen it for production |
| Thresholds | MinOrderValue | Business rules become visible and adjustable, not buried in a filter step |
| Incremental refresh | RangeStart, RangeEnd | Reserved names required by Power BI — Module 11 Lesson 4 |
Parameter types
- Any value — free text or number.
- List of values — a dropdown, which prevents typos and documents the valid options.
- Query — the list comes from another query, so it stays current.
DEV / TEST / PROD cannot be got wrong and tells the next person what the valid environments are.It is the same principle as rule-based filters and Choose Columns: make the correct thing the easy thing.
Reference, duplicate and group
| Action | Creates | Linked to the original? |
|---|---|---|
| Reference | A new query starting from the original's output | Yes — changes to the original flow through |
| Duplicate | An independent copy of all the steps | No — the two diverge from that moment |
| Group | A folder in the Queries pane | n/a — purely organisational |
Reference is what you usually want. One source of truth; branches that build on it.
The staging pattern
The standard structure for a maintainable file:
- Source queries — connect and do minimal shaping. Load disabled.
- Staging queries — reference the source, do the cleaning. Load disabled.
- Output queries — reference staging, do any final shaping. These load to the model.
Right-click any query → Enable load to toggle whether it becomes a table in the model. Turning it off for helpers keeps the Data pane clean and the model small.
Groups
Right-click in the Queries pane → New Group. A conventional set:
0 Parameters1 Sources2 Staging3 Model— the queries that actually load4 Data Quality— the check queries from Lesson 39 Functions
The numeric prefixes force a sensible order, since groups sort alphabetically.
Reading the Advanced Editor
Home › Advanced Editor shows the M behind your Applied Steps. It has always been there; this is the point at which reading it becomes useful.
let
StepName1 = <expression>,
StepName2 = <expression using StepName1>,
StepName3 = <expression using StepName2>
in
StepName3- let introduces a list of named steps.
- Each step is
Name = expression, separated by commas — no comma after the last one. - in names the step that is the query's result. Usually the last, and it does not have to be.
- Step names with spaces are written
#"Step Name". That is where the hash marks come from. - M is case-sensitive.
Table.SelectRowsworks;table.selectrowsdoes not.
Custom functions
A function is a query that takes arguments. Two ways to make one:
1 · From a parameter (the easy route)
- Build a query that does the transformation for one case, using a parameter for the thing that varies.
- Right-click the query → Create Function.
- Power Query generates a function taking that parameter as its argument.
This is exactly what Combine Files does behind the scenes (Module 2 Lesson 2) — the same pattern, made explicit.
2 · By hand
// CleanText.pq - one definition of "cleaned text" for the whole file
(input as nullable text) as nullable text =>
let
Trimmed = Text.Trim(input ?? ""),
Cleaned = Text.Clean(Trimmed),
Collapsed = Text.Replace(Cleaned, " ", " "),
Result = if Collapsed = "" then null else Collapsed
in
ResultInvoke it in a Custom Column: CleanText([FullName]). Or across several columns at once:
Table.TransformColumns(Source, {
{"FullName", CleanText, type nullable text},
{"Email", CleanText, type nullable text},
{"Country", CleanText, type nullable text}
})This is the same argument as a database view versus duplicated Power Query logic (Module 2 Lesson 3), one level down.
Visual Explanation
A well-organised query file, as it appears in the Queries pane.
📁 0 Parameters
⚙ DataFolder "C:\bwb\data\" (text)
⚙ Environment PROD (list: DEV / TEST / PROD)
⚙ StartDate 2024-01-01 (date)
📁 1 Sources [load disabled]
▤ src_Sales raw CSV, headers promoted, nothing else
▤ src_Products raw CSV
▤ src_Customers raw CSV
📁 2 Staging [load disabled]
▤ stg_Sales references src_Sales -> reduce, clean, type
▤ stg_Products references src_Products -> dedupe, clean
▤ stg_Customers references src_Customers -> standardise country
▤ map_Country Enter Data: raw -> standard
📁 3 Model [LOADS TO MODEL]
▦ Sales references stg_Sales
▦ Product references stg_Products
▦ Customer references stg_Customers
▦ Date generated calendar
📁 4 Data Quality [LOADS TO MODEL]
▦ DQ_Issues unmapped values, duplicate keys, bad conversions
📁 9 Functions
ƒ CleanText (text) => text
ƒ StandardiseCountry (text) => textOne place to change cleaning. The staging query.
Obvious what loads. Only groups 3 and 4.
Obvious what feeds what. The naming prefix plus Query Dependencies.
One definition of shared logic. The functions group.
Query Dependencies
View › Query Dependencies draws a diagram of which query feeds which. On a file like the one above it is immediately readable; on a flat unorganised file it is the only way to work out what is going on.
Power BI Demonstration
DataFolder, type Text, current value your data folder path including the trailing backslash.DataFolder & "bwb-retail-sales.csv".Environment, Suggested Values: List of values, enter DEV, TEST, PROD. Note that it now presents as a dropdown.let, the steps, and the in. Find a step name with spaces and note the #"…" syntax.CleanText function from section 03. Name it CleanText.Table.TransformColumns(PreviousStep, {{"FullName", CleanText, type nullable text}}).Example Dataset
Any of the course files will do for this lesson — the subject is structure rather than content. Using several at once makes the organisational benefit more obvious.
bwb-retail-sales.csv1.3 MB · 15067 rows · 13 columns · CSV
Columns & data types
| Column | Data type | Meaning |
|---|---|---|
OrderID | Text | Order identifier. One order can have several rows — one per product line. |
OrderDate | Date | Date the order was placed. Use this for time intelligence. |
ShipDate | Date | Date the order shipped. A second date column, for the role-playing dimension lesson. |
CustomerKey | Text | Foreign key to bwb-customers.csv. |
ProductKey | Text | Foreign key to bwb-products.csv. |
StoreKey | Text | Foreign key to bwb-stores.csv. Blank for non-store channels. |
Channel | Text | Online, Retail Store or Partner. |
Quantity | Whole number | Units sold on this line. |
UnitPrice | Decimal | List price per unit before discount. |
Discount | Decimal | Discount rate applied, 0 to 0.30. |
NetSales | Decimal | Revenue after discount. The measure you will sum most often. |
COGS | Decimal | Cost of goods sold for this line. |
Profit | Decimal | NetSales minus COGS. Pre-computed so you can validate your own DAX. |
Questions this dataset can answer
- Which category generates the most revenue, and is it also the most profitable?
- How did monthly revenue trend across the two years, and where is the seasonal peak?
- Which region has the highest revenue per customer?
- What share of revenue comes from discounted orders, and does discounting improve profit?
- Which products account for the top 20% of revenue?
Step-by-Step Exercise
DataFolder parameter, referenced by every source query. Test it by moving your data folder and changing only the parameter.CleanText from section 03, and one of your own — perhaps a StandardiseCountry that wraps the mapping merge from Lesson 3.Table.TransformColumns.DataFolder. This is the payoff for parameterising.Expected Result
| Check | Expected |
|---|---|
| Queries pane | Six numbered groups, nothing loose at the top level |
| Italic query names | All sources and staging — load disabled |
| Data pane in Power BI | Only the model tables and DQ_Issues |
| Hard-coded paths | None — all via DataFolder |
| Duplicated queries | None — references only |
| Query Dependencies diagram | A readable left-to-right flow, no crossing tangles |
| .pbit on open | Prompts for DataFolder before refreshing |
If it did not match, that gap is the problem. A file you cannot draw from memory is a file you will get lost in, and so will everyone after you.
Common Mistakes
Fix: Use Reference. Duplicate only when you genuinely want an independent copy that will evolve differently — which is rare.
Fix: Right-click → untick Enable load on everything except your model tables and check queries.
Fix: Parameterise. Paths and servers always; thresholds whenever the value is a business decision rather than a technical one.
Fix: Open it on a query you built yourself and read it. The structure is simpler than it looks, and it is the same every time.
Fix: Write a function. One definition, invoked everywhere, improved in one place.
Professional Tip
src_ for sources, stg_ for staging, map_ for mapping tables, DQ_ for checks, and plain names for the tables that load to the model.Combined with numbered groups this means the Queries pane is self-explanatory without anyone having to open anything. It also makes the Query Dependencies diagram readable, because the prefix tells you which layer each box belongs to.
Model tables get plain names deliberately — those are the ones that appear in the Data pane, and
stg_Sales would look wrong there.Two more:
- Put a description on every query. Right-click → Properties → Description. It shows as a tooltip in the pane and is the only documentation anyone will actually read.
- Keep source queries genuinely minimal. Connect and promote headers, nothing more. When a source changes shape, having exactly one thin query to fix is worth a great deal.
Knowledge Check
Knowledge Check
5 questions. Answer each one to see why the right answer is right.
What is the difference between Reference and Duplicate?
In M, what do let and in do?
Why disable load on staging queries?
What is the main argument for writing a custom function rather than repeating a transformation?
You want a parameter for the target environment. What type should it be?
Practical Challenge
Restructure a messy query file for handover
You inherit a .pbix with 18 queries in a flat list. Paths are hard-coded, three queries are near-identical duplicates, everything loads to the model, and nobody can tell what feeds what. Restructure it so a new analyst could pick it up, and document what you changed.
- Apply the six-group staging structure
- Parameterise every path and any hard-coded business threshold you find
- Replace the three duplicates with references or a function — and justify which you chose
- Disable load on everything that is not a model table or a DQ check
- Adopt a naming convention and apply it throughout
- Produce a handover note explaining the structure and what changed
- State one thing you deliberately did not change, and why
Sales2024 CSV from C:\Users\jane\Desktop\data\ [loads]
Sales2025 CSV from C:\Users\jane\Desktop\data\ [loads]
Sales2026 CSV from C:\Users\jane\Desktop\data\ [loads]
Products CSV, deduped, trimmed [loads]
Customers CSV, country mapping merged inline [loads]
CountryMap Enter Data [loads]
Query1 filters Sales2024 to NetSales > 100 [loads]
Query1 (2) filters Sales2025 to NetSales > 100 [loads]
Query1 (3) filters Sales2026 to NetSales > 100 [loads]
Calendar generated [loads]
Sheet1 leftover, unused [loads]
... 7 more [all load]The three Query1 variants are the interesting problem. They do the same thing to three different tables.
Two possible answers: append the three sales queries first and filter once, or write a function taking a table and returning the filtered version. Which is better depends on whether the three sales files should be one table in the model — and they almost certainly should.
Also notice the year-per-file pattern. Is three queries even the right shape?
Solution
Try the challenge properly before opening this. Reading a solution you have not attempted feels like learning, but it is not.
The restructure
1 · The biggest change: the sales files should not be three queries
Sales2024, Sales2025 and Sales2026 are the same data in three files. Three queries means three places to fix anything, and a fourth query has to be written by hand every January.Use the Folder connector (Module 2 Lesson 2) pointed at the data folder, filtered to
sales*.csv. One query, and 2027 appears automatically.This also dissolves the
Query1 problem entirely — there is now one table to filter, so the duplicated filter logic has nothing to duplicate.2 · The resulting structure
📁 0 Parameters
⚙ DataFolder text "\\fileserver\finance\data\"
⚙ MinOrderValue decimal 100 <- was hard-coded in Query1 x3
📁 1 Sources [load disabled]
▤ src_SalesFolder Folder connector, filtered to sales*.csv
▤ src_Products CSV via DataFolder
▤ src_Customers CSV via DataFolder
📁 2 Staging [load disabled]
▤ stg_Sales references src_SalesFolder -> combine, reduce, clean, type
▤ stg_Products references src_Products -> dedupe, clean
▤ stg_Customers references src_Customers -> merge map_Country
▤ map_Country Enter Data: raw -> standard
📁 3 Model [LOADS]
▦ Sales references stg_Sales, filtered to >= MinOrderValue
▦ Product references stg_Products
▦ Customer references stg_Customers
▦ Date generated calendar
📁 4 Data Quality [LOADS]
▦ DQ_Issues unmapped countries, duplicate keys, orphaned product keys
📁 9 Functions
ƒ CleanText (nullable text) => nullable text3 · References or a function for the duplicates?
The three
Query1 queries existed only because the sales data was in three queries. Consolidating the source removes the need for any of them: one stg_Sales, one filter step in Sales, referencing MinOrderValue.The general lesson: when you find duplicated logic, ask why it exists before deciding how to share it. Sometimes the duplication is telling you the structure above it is wrong.
4 · The handover note
0 Parameters holds the two values you may need to change. 1 Sources connects to files and does nothing else. 2 Staging holds all the cleaning logic — this is where you go to change how data is treated. 3 Model is what loads into Power BI. 4 Data Quality returns rows only when something is wrong; its count is on the hidden _Validation page.To move the data folder: change the
DataFolder parameter. Nothing else.To change the minimum order value: change
MinOrderValue. It was previously hard-coded in three places, which is why the reports had disagreed.New sales files: drop them in the folder named
sales*.csv. They are picked up automatically — no query change needed.What changed: three sales queries became one Folder connection; three duplicate filter queries were removed; an unused
Sheet1 query was deleted; load was disabled on 7 helper queries that were appearing as tables in the model; all paths were parameterised; a DQ check query was added.5 · What I deliberately did not change
map_Country is a hard-coded table inside the file. The better design is an Excel file on SharePoint that a business user can maintain without opening Power Query, or a database reference table.I left it alone deliberately, for two reasons.
First, it is not currently broken. Changing it introduces a new external dependency and a new failure mode, in a handover where the priority is that the new analyst can understand and run what exists.
Second, and more importantly, it is not my decision to make. Moving the mapping out of the file means someone has to own it, maintain it, and be told when to update it. That is an organisational commitment, not a technical change. I have flagged it as a recommendation with the reasoning, and left the decision to whoever will own the process.
Restructuring work has a strong pull toward changing everything you can see. Knowing where to stop — and being explicit about what you left and why — is part of doing it well.
The three judgements this challenge tests
Recognising that duplication was a symptom. The obvious fix shares the logic; the right fix removes the need for it. Asking "why does this duplication exist?" before "how do I share it?" is the habit worth building.
Writing the handover note for the reader, not the author. It leads with "to change X, do Y" rather than with a list of what was refactored. The new analyst needs to operate the thing before they need its history.
Deliberately not fixing something. The mapping table is a genuine weakness and changing it would have been defensible. Leaving it, flagging it, and explaining that the decision belongs to someone else demonstrates a sense of scope that matters more in real work than technical thoroughness does.
Lesson Summary
- Parameters make queries portable and business rules visible. Use a list of values wherever the options are known.
- Reference, do not duplicate. References stay linked; duplicates drift apart.
- Use the staging pattern: Parameters → Sources → Staging → Model → Data Quality → Functions, in numbered groups.
- Disable load on everything that is not a model table or a check query.
- Every M query is
letstepsinresult. Step names with spaces use#"…". M is case-sensitive. - Custom functions give shared logic one definition, so improvements apply everywhere at once.
- View › Query Dependencies shows what feeds what. If you cannot sketch it from memory, the structure is not clear enough.
- When you find duplicated logic, ask why it exists before deciding how to share it.
Syllabus topics delivered
Progress
Marking a lesson complete stores your progress in this browser and updates your BitWithBite learner dashboard. Nothing is uploaded anywhere.