Dataflow Gen2 lets you attach multiple output destinations to a single query — run your transformation once and write results to both a Fabric Lakehouse table and a Warehouse table simultaneously. This lesson teaches you the complete pattern: from Power Query transformation to dual-destination configuration, data type gotchas, and production-ready pipeline orchestration.

Here's a scenario that comes up constantly in real-world Fabric implementations: your analytics team wants to query transformed sales data from a Fabric Warehouse using clean T-SQL and stored procedures, while your data science team wants that same cleaned dataset landing in a Lakehouse where they can run Spark notebooks against it. You've already written the transformation logic once in Dataflow Gen2 — the last thing you want to do is maintain two separate dataflows with identical Power Query steps just to feed two different destinations.
The good news is that Dataflow Gen2 has a feature purpose-built for exactly this problem: output destinations. Unlike the original Dataflow Gen1 where a query had exactly one place to go, Gen2 lets you attach multiple output destinations to the same query. That means your transformation runs once, and the results get written simultaneously to as many Fabric items as you need. One cleaned dataset, multiple consumers, zero duplication of logic.
By the end of this lesson, you'll know how to configure a single Dataflow Gen2 query to write its output to both a Lakehouse managed table and a Warehouse table at the same time. You'll understand how Fabric physically executes that dual write, where the gotchas are, and how to make this pattern production-ready inside a data pipeline.
What you'll learn:
This lesson assumes you're already comfortable with the basics of Dataflow Gen2 and Power Query transformations. If you're newer to the tool, spend some time with Ingesting Data with Dataflow Gen2: Power Query Skills in Microsoft Fabric before continuing — we'll be moving fast through the Power Query side and spending our energy on the output configuration.
You should also have:
Before you click a single button, let's get the mental model right — because if you misunderstand what Fabric is doing under the hood, you'll make configuration mistakes that are hard to debug.
When you add an output destination to a Dataflow Gen2 query, you are not adding a second copy of the dataflow. You are telling the Fabric backend that after the query finishes evaluating, it should write the result set to an additional location. Internally, Dataflow Gen2 uses a staging area in OneLake — a temporary location where your transformed data lands first before being pushed to each configured destination.
This staging step is what makes multi-destination possible. The query evaluates once, the result set materializes in staging, and then separate write operations fan out from that staging area to each destination. Think of it like a mail sorting facility: one inbound truck (your query result), multiple outbound routes (your destinations).
Key insight
The staging area in OneLake is what decouples your transformation logic from your output destinations. It's also why you need a Fabric capacity — the staging storage is provisioned automatically and doesn't require any configuration from you, but it does require the Fabric compute layer.
There's an important implication here for data consistency. Because each destination write is a separate operation, they don't happen in a single atomic transaction. In the vast majority of cases this is fine — both writes will succeed or the dataflow will fail and retry — but it's worth knowing that if the dataflow fails midway through writing to the second destination, you could briefly have fresh data in one location and stale data in the other. We'll discuss how to handle this in the pipeline orchestration section.
Let's work with something concrete. Imagine you're pulling daily sales order data from an Azure SQL Database. The raw data has messy column names, some inconsistent casing in product category names, and a calculated margin column that doesn't exist in the source. After transformation, the cleaned result needs to:
gold_sales_orders, used by the Direct Lake semantic model powering executive dashboardsdbo.fact_sales_orders, used by the BI team for T-SQL-based reporting and ad hoc queriesSame data, two consumers, one transformation. Let's build it.
In your Fabric workspace, select New and then Dataflow Gen2. You'll land in the Power Query Online editor — a familiar interface if you've used Power Query in Excel or Power BI Desktop.
Connect to your source. For this example, we're connecting to Azure SQL Database using the SQL Server connector. Enter your server name, database name, and credentials. Navigate to the SalesOrders table and select it.
Your raw query will look something like this in Power Query M (you can view it by going to Home > Advanced Editor):
let
Source = Sql.Database("myserver.database.windows.net", "SalesDB"),
SalesOrders = Source{[Schema="dbo", Item="SalesOrders"]}[Data]
in
SalesOrders
Now apply the transformation steps that produce the clean, analysis-ready dataset. In the Power Query editor, you'd do this through the UI, but here's the full M script so you can see exactly what's happening:
let
Source = Sql.Database("myserver.database.windows.net", "SalesDB"),
SalesOrders = Source{[Schema="dbo", Item="SalesOrders"]}[Data],
// Rename columns to clean snake_case names
RenamedColumns = Table.RenameColumns(SalesOrders, {
{"OrderID", "order_id"},
{"CustID", "customer_id"},
{"OrderDate", "order_date"},
{"ShipDate", "ship_date"},
{"ProdCategory", "product_category"},
{"ProdName", "product_name"},
{"UnitPrice", "unit_price"},
{"Qty", "quantity"},
{"Discount", "discount_pct"},
{"Revenue", "revenue"},
{"COGS", "cost_of_goods_sold"}
}),
// Normalize product category casing
NormalizedCategory = Table.TransformColumns(
RenamedColumns,
{{"product_category", Text.Proper, type text}}
),
// Add calculated margin column
AddedMargin = Table.AddColumn(
NormalizedCategory,
"gross_margin",
each [revenue] - [cost_of_goods_sold],
type number
),
// Filter out test orders (order_id starts with "TEST-")
FilteredOrders = Table.SelectRows(
AddedMargin,
each not Text.StartsWith([order_id], "TEST-")
),
// Set correct data types explicitly
TypedTable = Table.TransformColumnTypes(FilteredOrders, {
{"order_id", type text},
{"customer_id", type text},
{"order_date", type date},
{"ship_date", type date},
{"product_category", type text},
{"product_name", type text},
{"unit_price", type number},
{"quantity", type Int64.Type},
{"discount_pct", type number},
{"revenue", type number},
{"cost_of_goods_sold", type number},
{"gross_margin", type number}
})
in
TypedTable
Notice that the last step explicitly sets data types. This is not optional when you're writing to multiple destinations — it's critical. Each destination enforces its own schema, and letting Power Query infer types is an invitation for silent mismatches.
Warning
If you don't explicitly define column types in your Power Query steps, Dataflow Gen2 will infer them from the first batch of data it sees. This works until a batch comes through with a null in a column it thought was Int64, or a value that looks like a date but is actually a string. Always set your types explicitly before configuring output destinations.
With your query producing clean, correctly-typed data, it's time to add destinations. In the Dataflow Gen2 editor, look at the bottom of the screen — you'll see a panel called Query settings on the right, and at the bottom of the canvas you'll find the Add data destination button. In some layouts, you'll see it as a "+" icon in the lower right of the query card itself.
Click Add data destination and select Lakehouse from the list of options.
A connection dialog will appear. If you haven't connected to this Lakehouse from a dataflow before, you'll need to establish the connection. Select your workspace from the dropdown, then select your Lakehouse item. You'll then specify:
gold_sales_ordersIf the table doesn't exist yet, Fabric will create it. If it does exist, you'll get to choose how to handle the existing data.
This is one of the most consequential decisions in the destination configuration, and the one most people get wrong the first time.
You have two options:
Append: New rows are added to the existing table each time the dataflow runs. Use this when your query returns only new records (i.e., you've already filtered to today's data or new records since last run).
Replace: The table is truncated and all rows from the current query result are written fresh. Use this when your query always returns the full current state of the data.
For our sales orders scenario, let's assume we're running an incremental pattern where the dataflow pulls all orders from the past 30 days and we want the table to always reflect that rolling window. That means Replace is the right choice — we don't want to accumulate 30-day snapshots on top of each other.
Tip
If you need a true incremental merge (upsert based on a key), Dataflow Gen2's built-in update methods won't do it natively at the destination level. For that pattern, consider landing to the Lakehouse with Replace and then running a separate Spark notebook to handle the merge, or use a pipeline with a Copy Activity into a staging table followed by a T-SQL MERGE.
After selecting the update method, you'll see the Column mapping screen. This shows the columns coming from your query on the left and the destination columns on the right.
If the table is new, Fabric proposes to auto-create the schema based on your query's column types — accept this and verify each type. If you're mapping to an existing table, carefully check that each source column aligns to the correct destination column. A common mistake here is leaving columns unmapped because of a name mismatch between your query output and an existing table.
For gold_sales_orders, our new table, the auto-mapping should be clean since we explicitly typed everything in the query. Confirm and save.
Now for the second destination. With your query still selected in the canvas, click Add data destination again — yes, the same button, on the same query. This time select Warehouse.
The connection flow is similar to the Lakehouse. Select your workspace, then your Warehouse item. You'll then specify the schema and table name:
dbofact_sales_ordersNote
Unlike Lakehouse destinations where you're working with Delta tables managed in OneLake, a Warehouse destination writes to tables managed by the Warehouse's SQL engine. If you want to understand the architectural differences between these two storage types and when each one makes sense, Fabric Lakehouse vs Warehouse: Choosing the Right Store for Your Workload covers that decision in depth.
The same Append vs. Replace choice applies here, and you should make the same decision you made for the Lakehouse — in our case, Replace. The key thing to understand is that each destination can have a different update method.
This is actually quite powerful. You could, for example:
The same query result fans out to both, but each destination handles the data according to its own write mode.
Here's where practitioners get caught. Fabric Warehouse tables use T-SQL data types, and the mapping from Power Query types to T-SQL types isn't always intuitive:
| Power Query Type | T-SQL Type in Warehouse |
|---|---|
type text |
varchar(8000) |
type number (decimal) |
decimal(18,0) — watch the precision! |
Int64.Type |
bigint |
type date |
date |
type datetime |
datetime2(7) |
type logical |
bit |
The most common pain point is type number mapping to decimal(18,0), which silently truncates your decimal places. If unit_price is 24.99 and lands as 25 in your Warehouse table, this is why.
The fix: pre-create the Warehouse table with the exact types you want before configuring the destination. When Dataflow Gen2 finds an existing table, it maps to the existing column types rather than trying to create them. Open your Warehouse and run:
CREATE TABLE dbo.fact_sales_orders (
order_id VARCHAR(50) NOT NULL,
customer_id VARCHAR(50) NOT NULL,
order_date DATE NOT NULL,
ship_date DATE NULL,
product_category VARCHAR(100) NOT NULL,
product_name VARCHAR(200) NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
quantity INT NOT NULL,
discount_pct DECIMAL(5, 4) NULL,
revenue DECIMAL(18, 2) NOT NULL,
cost_of_goods_sold DECIMAL(18, 2) NOT NULL,
gross_margin DECIMAL(18, 2) NOT NULL
);
Now when you configure the Warehouse destination and Fabric sees this existing table, it will respect your DECIMAL(10,2) definition rather than defaulting to DECIMAL(18,0). This is the most reliable way to get precise type control for Warehouse destinations.
Warning
If you let Dataflow Gen2 auto-create the Warehouse table and your type number columns get created as decimal(18,0), you won't see an error — the data will just silently lose its decimal precision. Always pre-create Warehouse tables with explicit types when precision matters, which for financial data is essentially always.
Before you publish and run, take a moment to verify both destinations are correctly attached. In the Power Query canvas, your query card should show two destination indicators — typically rendered as small icons or badges at the bottom of the query card, one for the Lakehouse and one for the Warehouse.
You can click on each destination indicator to open the configuration and double-check:
If a column appears unmapped (greyed out or flagged), stop and resolve it before publishing. An unmapped column means that column will be silently dropped during the write — no error, just missing data.
Click Publish in the top right of the Dataflow Gen2 editor. Publishing saves and triggers the dataflow to run immediately. You'll see a notification that the dataflow is refreshing.
To monitor the run, navigate to the Monitoring Hub in your workspace. You'll see the dataflow appear with its current status. A successful run for a multi-destination dataflow shows one overall status — Fabric handles both writes as part of the same dataflow execution.
Once complete:
gold_sales_orders with datadbo.fact_sales_orders:SELECT TOP 10
order_id,
order_date,
product_category,
revenue,
gross_margin
FROM dbo.fact_sales_orders
ORDER BY order_date DESC;
Both should show identical row counts and data.
Running a multi-destination dataflow on a manual publish schedule is fine for development, but in production you want orchestrated, scheduled execution with retry logic and alerting. That means wrapping it in a Data Pipeline.
Remember the data consistency concern we flagged earlier — the two destination writes are not atomic. If your dataflow succeeds in writing to the Lakehouse but fails midway through writing to the Warehouse, you'll have an inconsistency until the next successful run.
A pipeline gives you:
Create a new Data Pipeline in your workspace. Add a Dataflow activity — this is the activity type for invoking a Dataflow Gen2. Configure it to point to your newly published dataflow.
After the Dataflow activity, add two Script activities (or Lookup activities against the SQL Analytics Endpoint / Warehouse) to run validation queries. Connect them in parallel off the success path of the Dataflow activity:
Validation Query for Lakehouse (via the SQL Analytics Endpoint):
SELECT COUNT(*) AS row_count
FROM gold_sales_orders
WHERE order_date >= CAST(DATEADD(day, -30, GETDATE()) AS DATE);
Validation Query for Warehouse:
SELECT COUNT(*) AS row_count
FROM dbo.fact_sales_orders
WHERE order_date >= CAST(DATEADD(day, -30, GETDATE()) AS DATE);
Add a final Script or Set Variable activity that compares the two counts. If they differ by more than a small tolerance (accounting for any timing differences), trigger a pipeline failure or send an alert.
Tip
For the Lakehouse validation query, use the SQL Analytics Endpoint connection string, which gives you T-SQL access to your Lakehouse Delta tables without needing a Warehouse. This pattern is covered in detail in Querying Lakehouse Data with the SQL Analytics Endpoint: Writing T-SQL Against Delta Tables Without a Warehouse.
Set your pipeline schedule based on how frequently your source data updates. For daily sales data, a morning run (say 6:00 AM) gives the overnight batch time to complete before the pipeline kicks off. Configure the schedule on the pipeline, not the dataflow itself — when a pipeline owns the execution, you get centralized monitoring and control.
Let's put everything together in a structured exercise you can complete in your own Fabric environment.
You work for a retail company that processes e-commerce orders. Your source is a SQL table called raw_orders in an Azure SQL Database (or you can simulate this with any table you have access to). The business needs:
gold_orders_summary for Direct Lake reportingdbo.dim_orders_reporting for the BI team's T-SQL reportsStep 1: Prepare your Warehouse table
Open your Fabric Warehouse and run the following DDL to pre-create the destination table with correct types:
CREATE TABLE dbo.dim_orders_reporting (
order_id VARCHAR(50) NOT NULL,
customer_id VARCHAR(50) NOT NULL,
order_date DATE NOT NULL,
product_category VARCHAR(100) NOT NULL,
revenue DECIMAL(18, 2) NOT NULL,
gross_margin DECIMAL(18, 2) NOT NULL,
margin_pct DECIMAL(7, 4) NULL,
is_discounted BIT NOT NULL,
load_timestamp DATETIME2(0) NOT NULL
);
Step 2: Build the Dataflow Gen2
Create a new Dataflow Gen2 and connect to your source table. Apply these transformations:
let
Source = /* your source connection here */,
// Add margin percentage
AddedMarginPct = Table.AddColumn(
Source,
"margin_pct",
each if [revenue] = 0 then null else ([revenue] - [cost_of_goods_sold]) / [revenue],
type number
),
// Add boolean discount flag
AddedDiscountFlag = Table.AddColumn(
AddedMarginPct,
"is_discounted",
each [discount_pct] > 0,
type logical
),
// Add load timestamp
AddedTimestamp = Table.AddColumn(
AddedDiscountFlag,
"load_timestamp",
each DateTimeZone.RemoveZone(DateTimeZone.LocalNow()),
type datetime
),
// Select and reorder only the columns we need
FinalColumns = Table.SelectColumns(
AddedTimestamp,
{"order_id", "customer_id", "order_date", "product_category",
"revenue", "gross_margin", "margin_pct", "is_discounted", "load_timestamp"}
)
in
FinalColumns
Step 3: Configure both destinations
gold_orders_summary, set update method to Replacedbo.dim_orders_reporting, set update method to ReplaceStep 4: Publish and verify
Publish the dataflow and wait for it to complete. Then verify:
-- In Warehouse
SELECT
COUNT(*) AS warehouse_count,
MIN(order_date) AS earliest_order,
MAX(order_date) AS latest_order,
SUM(revenue) AS total_revenue
FROM dbo.dim_orders_reporting;
Run the equivalent query against your Lakehouse via the SQL Analytics Endpoint and confirm the numbers match.
Step 5: Wrap in a pipeline
Create a Data Pipeline, add a Dataflow activity pointing to your dataflow, and set a daily schedule.
This is the most common frustration. The dataflow runs, transformations complete, and then the destination write fails cryptically. Nine times out of ten, it's a data type mismatch. Go to the Monitoring Hub, open the dataflow run details, and look at the activity logs — they usually contain the specific column and type that caused the issue. Fix by either adjusting your Power Query types or altering the destination table's column definition.
Since the two writes are separate operations, this can happen. The most common cause is a permissions issue — the dataflow's service principal or user identity has access to one item but not the other. Verify that your Fabric identity has at minimum Write access to both the Lakehouse and the Warehouse. Check item permissions from the workspace, not just workspace-level roles.
This is the silent unmapped column problem. Go back into the dataflow editor, click on the destination indicator for the affected destination, and open the column mapping. Any column showing as unmapped will be dropped. Either map it manually or, for Warehouse destinations, add the column to the table definition first.
By default, Dataflow Gen2 queries have a timeout. For large source tables, the query evaluation plus two writes can exceed this. Solutions:
This happens when the Delta table metadata isn't updated properly. After a Replace write, Direct Lake semantic models need the table's metadata to be current. In most cases, this resolves automatically, but if you're seeing stale data in reports, open the Lakehouse, navigate to the table, and use the Refresh option. You can also trigger a Delta table optimization from a notebook. The details of keeping Delta tables healthy for Direct Lake are covered in Connecting a Power BI Semantic Model to a Fabric Lakehouse in Direct Lake Mode.
Key insight
Query folding is your best friend for multi-destination dataflows. If your source supports it (SQL databases, SharePoint lists, OData feeds all do), Power Query will push as much of your transformation as possible back to the source — filtering, column selection, aggregations. This means less data travels to the Fabric staging area, transformations run faster, and your destination writes start sooner. You can verify folding is happening by right-clicking a query step and checking whether "View Native Query" is available.
Multi-destination dataflows are powerful but not always the right tool. Here's an honest assessment of when to reach for this pattern versus alternatives.
Use multi-destination Dataflow Gen2 when:
Consider alternatives when:
The architecture this supports best is the Gold layer of a Medallion Architecture where you have clean, business-ready data that needs to be accessible to both SQL consumers (Warehouse) and file/Spark consumers (Lakehouse). Instead of two separate ETL paths from Bronze to Gold, you have one transformation and two output paths.
You now know how to configure a Dataflow Gen2 query to write its results to a Lakehouse table and a Warehouse table simultaneously — and more importantly, you understand why it works the way it does (staging area, separate write operations, independent update methods) and what can go wrong (type mismatches, unmapped columns, permissions gaps, non-atomic writes).
The pattern you've built here — one transformation, multiple destinations, wrapped in a pipeline — is a production-grade pattern used in real Fabric implementations. It eliminates duplicated transformation logic, keeps your data in sync across consumer types, and integrates cleanly with the orchestration and monitoring tools Fabric provides.
Where to go next:
Microsoft Fabric Fundamentals
Creating and Managing Fabric Lakehouses with Notebooks: Reading External Files from OneLake, Writing Delta Tables, and Browsing Results in the Lakehouse Explorer
Handling Schema Evolution in Fabric Lakehouse Delta Tables: Adding Columns, Merging Incompatible Schemas, and Enforcing Constraints Across Medallion Layers