Power BI is turning 10, and we’re marking the occasion with a special community challenge. Use your creativity to tell a story, uncover trends, or highlight something unexpected.
Get startedJoin us at FabCon Vienna from September 15-18, 2025, for the ultimate Fabric, Power BI, SQL, and AI community-led learning event. Save €200 with code FABCOMM. Get registered
Hi all,
We are using Fabric deployment pipelines to transfer all items (notebooks, lakehouses, pipelines, semantic models, and reports) from a development workspace to test to production.
One of the problems we run into at the moment is that when we create Notebook A in development, deploy that to test, create Notebook B and delete Notebook A in development and deploy those changes, that Notebook A stays as an unpaired, orphaned item in the test workspace.
In neither the API documentation (we use the deployment APIs) nor the GUI can I find a way to clean up these items from the test workspace (and subsequently in the future the production workspace).
I understand that we don't want to deploy items to test that we don't need but there are scenarios in which, after for instance a refactor, we will have removed notebooks and possibly other items from dev and want to have those then removed from test and production as well.
I have tried comparing item names between test and development to delete items with a name that are in test but not development but since its possible to have multiple reports for instance with the same name in one workspace, this does not fully solve our issue (when we for instance want to remove the unpaired, orphaned report1 from the main directory but want to keep the paired report1 from folderA).
Is anyone aware of a way to deal with this issue? (of course preferably something that can be included in the automation process)
Solved! Go to Solution.
Since this issue can only occur with reports, I have solved it by looking at the pipeline stage artifacts endpoint of the Power BI REST API (link here).
The Powershell code to delete orphaned reports is this:
$subscriptionId = "<YOUR DEFAULT SUBSCRIPTION>"
$testWorkspaceStage = "<YOUR STAGE NUMBER HERE (0 is the first stage (usually dev) etc)>"
$pipelineId = "<YOUR DEPLOYMENT PIPELINE ID>"
$resourceUrl = "https://api.fabric.microsoft.com"
$fabricHeaders = @{}
function SetFabricHeaders() {
# Set default subscription
Update-AzConfig -DefaultSubscriptionForLogin "$subscriptionId"
# Login to Azure
Connect-AzAccount | Out-Null
# Get authentication
$fabricToken = (Get-AzAccessToken -ResourceUrl $resourceUrl).Token
$fabricHeaders = @{
'Content-Type' = "application/json"
'Authorization' = "Bearer {0}" -f $fabricToken
}
}
# Set the headers
SetFabricHeaders
# Build url for retrieving artifacts API
$artifactUrl = "https://api.powerbi.com/v1.0/myorg/pipelines/$pipelineId/stages/$testWorkspaceStage/artifacts"
# Retrieve all artifacts
$artifacts = Invoke-RestMethod -Headers $fabricHeaders -Uri $artifactUrl -Method GET
# Define orphaned reports for deletion
$orphanedReports = (
$artifacts.reports |
Where-Object { [string]::IsNullOrEmpty($_.sourceArtifactId) } |
Select-Object -ExpandProperty artifactId
)
# Build url for item deletion API
$itemUrl = "https://api.fabric.microsoft.com/v1/workspaces/$testWorkspaceId/items/"
# Iterate through all orphaned reports and delete them
foreach ($report in $orphanedReports) {
# Construct API call for each orphaned item
$url = "$itemUrl/$report"
Write-Host "Deleting report with ID: $report"
# Make the web request
try {
Invoke-RestMethod -Headers $fabricHeaders -Uri $url -Method DELETE
Write-Host "Report with ID $report deleted"
}
catch {
Write-Host "Error making request for ID $report : $_"
}
}
The other possible orphaned items (notebooks, data pipelines, lakehouses etc) can be deleted using the DELETE Items endpoint based on the displayName (as those have to be unique for these items in a workspace).
Hope this helps someone out in the future as well 👍
PS: Since this deletes only reports (Power BI items), it means that the script can also be called by a service principal.
*edit: I have no idea how I didn't see the "List Deployment Pipeline Stage Items" Fabric REST API before (link here) but with that you can solve the entire problem of orphaned items (not just reports, not just based on displayName (since those can differ even when the items are still paired), but everything that is no longer paired).
This is also the solution to the problem that you get when you have an item, you deploy that to test, remove it from dev, and create it back in dev with the same name. Those actions cause your deploy to fail because it is trying to deploy an item to test eventhough there is already an item (unpaired) in test with that name. If you run the script first using the Fabric API, followed by a deploy, this issue will no longer occur.
Of course I do also have the Powershell script for this better way of working:
$testStageName = "Test"
$pipelineName = "<YOUR PIPELINE NAME>"
$resourceUrl = "https://api.fabric.microsoft.com"
$fabricHeaders = @{}
function SetFabricHeaders() {
# Set default subscription
Update-AzConfig -DefaultSubscriptionForLogin $subscriptionId
# Login to Azure
Connect-AzAccount | Out-Null
# Get authentication
$fabricToken = (Get-AzAccessToken -ResourceUrl $resourceUrl).Token
$fabricHeaders = @{
'Content-Type' = "application/json"
'Authorization' = "Bearer {0}" -f $fabricToken
}
}
SetFabricHeaders
# Get the pipelineId of the deployment pipeline
$deploymentPipelineUrl = "https://api.fabric.microsoft.com/v1/deploymentPipelines"
$deploymentPipeline = (Invoke-RestMethod -Headers $fabricHeaders -Uri $deploymentPipelineUrl -Method GET).value
$deploymentPipelineId = ($deploymentPipeline | Where-Object { $_.displayName -eq $pipelineName}).id
# Get the stageId and workspaceId of the test stage
$stageUrl = "https://api.fabric.microsoft.com/v1/deploymentPipelines/$deploymentPipelineId/stages"
$stages = (Invoke-RestMethod -Headers $fabricHeaders -Uri $stageUrl -Method GET).value
$testStage = $stages | Where-Object { $_.displayName -eq $testStageName }
$testStageId = $testStage.id
$testWorkspaceId = $testStage.workspaceId
# Retrieve the orphaned items for deletion
$itemUrl = "https://api.fabric.microsoft.com/v1/deploymentPipelines/$deploymentPipelineId/stages/$testStageId/items"
$itemList = (Invoke-RestMethod -Headers $fabricHeaders -Uri $itemUrl -Method GET).value
$orphanedItems = $itemList |
Where-Object { [string]::IsNullOrEmpty($_.sourceItemId) }
# Build url for item deletion API
$itemUrl = "https://api.fabric.microsoft.com/v1/workspaces/$testWorkspaceId/items/"
# Build an array to store the display names of items that were not deleted
$errorDetails = @()
# Iterate through all orphaned items and delete them
foreach ($item in $orphanedItems) {
$itemId = $item.itemId
$itemName = $item.itemDisplayName
# Construct API call for each orphaned item
$url = "$itemUrl/$itemId"
Write-Host "Deleting item with ID: $itemId"
# Delete orphaned items
try {
Invoke-RestMethod -Headers $fabricHeaders -Uri $url -Method DELETE
Write-Host "item with ID $itemId deleted"
}
catch {
Write-Host "Error making request for ID $itemId : $_"
$errorDetails += [PSCustomObject]@{
ItemName = $itemName
}
}
}
The script now also contains an array that logs the name of the items that fail to be deleted. This is because items that have dependencies built on them (if a notebook is used in a data pipeline for instance) can not be deleted. Until I find a way to deal with that issue in code (and have the time to look into it), I will only log the item name and delete the dependency by hand.
Hope this helps 👍
Hi @SanderTK
Your efforts and detailed explanation will definitely help others facing a similar challenge. If you could mark your response as Accepted Solution, it would make it easier for other community members to find the right answer. Thanks again for your valuable contribution! 🙌
Thanks and regards,
Cheri Srikanth
Since this issue can only occur with reports, I have solved it by looking at the pipeline stage artifacts endpoint of the Power BI REST API (link here).
The Powershell code to delete orphaned reports is this:
$subscriptionId = "<YOUR DEFAULT SUBSCRIPTION>"
$testWorkspaceStage = "<YOUR STAGE NUMBER HERE (0 is the first stage (usually dev) etc)>"
$pipelineId = "<YOUR DEPLOYMENT PIPELINE ID>"
$resourceUrl = "https://api.fabric.microsoft.com"
$fabricHeaders = @{}
function SetFabricHeaders() {
# Set default subscription
Update-AzConfig -DefaultSubscriptionForLogin "$subscriptionId"
# Login to Azure
Connect-AzAccount | Out-Null
# Get authentication
$fabricToken = (Get-AzAccessToken -ResourceUrl $resourceUrl).Token
$fabricHeaders = @{
'Content-Type' = "application/json"
'Authorization' = "Bearer {0}" -f $fabricToken
}
}
# Set the headers
SetFabricHeaders
# Build url for retrieving artifacts API
$artifactUrl = "https://api.powerbi.com/v1.0/myorg/pipelines/$pipelineId/stages/$testWorkspaceStage/artifacts"
# Retrieve all artifacts
$artifacts = Invoke-RestMethod -Headers $fabricHeaders -Uri $artifactUrl -Method GET
# Define orphaned reports for deletion
$orphanedReports = (
$artifacts.reports |
Where-Object { [string]::IsNullOrEmpty($_.sourceArtifactId) } |
Select-Object -ExpandProperty artifactId
)
# Build url for item deletion API
$itemUrl = "https://api.fabric.microsoft.com/v1/workspaces/$testWorkspaceId/items/"
# Iterate through all orphaned reports and delete them
foreach ($report in $orphanedReports) {
# Construct API call for each orphaned item
$url = "$itemUrl/$report"
Write-Host "Deleting report with ID: $report"
# Make the web request
try {
Invoke-RestMethod -Headers $fabricHeaders -Uri $url -Method DELETE
Write-Host "Report with ID $report deleted"
}
catch {
Write-Host "Error making request for ID $report : $_"
}
}
The other possible orphaned items (notebooks, data pipelines, lakehouses etc) can be deleted using the DELETE Items endpoint based on the displayName (as those have to be unique for these items in a workspace).
Hope this helps someone out in the future as well 👍
PS: Since this deletes only reports (Power BI items), it means that the script can also be called by a service principal.
*edit: I have no idea how I didn't see the "List Deployment Pipeline Stage Items" Fabric REST API before (link here) but with that you can solve the entire problem of orphaned items (not just reports, not just based on displayName (since those can differ even when the items are still paired), but everything that is no longer paired).
This is also the solution to the problem that you get when you have an item, you deploy that to test, remove it from dev, and create it back in dev with the same name. Those actions cause your deploy to fail because it is trying to deploy an item to test eventhough there is already an item (unpaired) in test with that name. If you run the script first using the Fabric API, followed by a deploy, this issue will no longer occur.
Of course I do also have the Powershell script for this better way of working:
$testStageName = "Test"
$pipelineName = "<YOUR PIPELINE NAME>"
$resourceUrl = "https://api.fabric.microsoft.com"
$fabricHeaders = @{}
function SetFabricHeaders() {
# Set default subscription
Update-AzConfig -DefaultSubscriptionForLogin $subscriptionId
# Login to Azure
Connect-AzAccount | Out-Null
# Get authentication
$fabricToken = (Get-AzAccessToken -ResourceUrl $resourceUrl).Token
$fabricHeaders = @{
'Content-Type' = "application/json"
'Authorization' = "Bearer {0}" -f $fabricToken
}
}
SetFabricHeaders
# Get the pipelineId of the deployment pipeline
$deploymentPipelineUrl = "https://api.fabric.microsoft.com/v1/deploymentPipelines"
$deploymentPipeline = (Invoke-RestMethod -Headers $fabricHeaders -Uri $deploymentPipelineUrl -Method GET).value
$deploymentPipelineId = ($deploymentPipeline | Where-Object { $_.displayName -eq $pipelineName}).id
# Get the stageId and workspaceId of the test stage
$stageUrl = "https://api.fabric.microsoft.com/v1/deploymentPipelines/$deploymentPipelineId/stages"
$stages = (Invoke-RestMethod -Headers $fabricHeaders -Uri $stageUrl -Method GET).value
$testStage = $stages | Where-Object { $_.displayName -eq $testStageName }
$testStageId = $testStage.id
$testWorkspaceId = $testStage.workspaceId
# Retrieve the orphaned items for deletion
$itemUrl = "https://api.fabric.microsoft.com/v1/deploymentPipelines/$deploymentPipelineId/stages/$testStageId/items"
$itemList = (Invoke-RestMethod -Headers $fabricHeaders -Uri $itemUrl -Method GET).value
$orphanedItems = $itemList |
Where-Object { [string]::IsNullOrEmpty($_.sourceItemId) }
# Build url for item deletion API
$itemUrl = "https://api.fabric.microsoft.com/v1/workspaces/$testWorkspaceId/items/"
# Build an array to store the display names of items that were not deleted
$errorDetails = @()
# Iterate through all orphaned items and delete them
foreach ($item in $orphanedItems) {
$itemId = $item.itemId
$itemName = $item.itemDisplayName
# Construct API call for each orphaned item
$url = "$itemUrl/$itemId"
Write-Host "Deleting item with ID: $itemId"
# Delete orphaned items
try {
Invoke-RestMethod -Headers $fabricHeaders -Uri $url -Method DELETE
Write-Host "item with ID $itemId deleted"
}
catch {
Write-Host "Error making request for ID $itemId : $_"
$errorDetails += [PSCustomObject]@{
ItemName = $itemName
}
}
}
The script now also contains an array that logs the name of the items that fail to be deleted. This is because items that have dependencies built on them (if a notebook is used in a data pipeline for instance) can not be deleted. Until I find a way to deal with that issue in code (and have the time to look into it), I will only log the item name and delete the dependency by hand.
Hope this helps 👍
Hi @SanderTK
The best approach to resolve your issue is use Object IDs instead of display names.
Could please follow the below steps and have a try.
1) Get All Items from Dev and Test Workspaces:
****************************************************************************
$devWorkspaceId = "YOUR_DEV_WORKSPACE_ID"
$testWorkspaceId = "YOUR_TEST_WORKSPACE_ID"
$accessToken = "YOUR_ACCESS_TOKEN"
$headers = @{
"Authorization" = "Bearer $accessToken"
}
$devReports = Invoke-RestMethod -Uri "https://api.powerbi.com/v1.0/myorg/groups/$devWorkspaceId/reports" -Headers $headers
$testReports = Invoke-RestMethod -Uri "https://api.powerbi.com/v1.0/myorg/groups/$testWorkspaceId/reports" -Headers $headers
****************************************************************************
2) Compare Items Using IDs & Identify Orphans
****************************************************************************
$devReportIds = $devReports.value.Id
foreach ($report in $testReports.value) {
if ($devReportIds -notcontains $report.Id) {
Write-Host "Deleting orphaned report: $($report.name)"
Invoke-RestMethod -Uri "https://api.powerbi.com/v1.0/myorg/groups/$testWorkspaceId/reports/$($report.Id)" -Method DELETE -Headers $headers
}
}
****************************************************************************
If the above information helps you, please give us a Kudos and marked the reply Accept as a Solution.
Thanks,
Cheri Srikanth
There is no overlap in these IDs either. The objectid changes when the report moves from dev to test it seems as well. I will now test it using the artifacts endpoint since this one lists a sourceartifactid in test which should be the artifactid of the report in dev. Will report (no pun intended) back once I get the script running
Hi @SanderTK
When using Microsoft Fabric Deployment Pipelines, Power BI does not automatically remove orphaned items (notebooks, lakehouses, pipelines, reports, etc.) from test and production workspaces when they are deleted from development.
Below are the Steps to use the Power BI REST API to detect and remove such orphaned items.
1: Register an Azure AD App and get API permissions.
2: Get an Access Token for authentication.
3: Retrieve all items from Development & Test Workspaces.
4: Compare items between Dev & Test to find orphaned items.
5: Use Power BI REST API to delete orphaned items.
6: Automate the process using Power Automate or Azure Functions.
I am happy to assist you if required the deailed steps.
If the above information helps you, please give us a Kudos and marked the reply Accept as a solution.
Thanks,
Cheri Srikanth
I have made a powershell script that does this, using the Power BI Rest API.
The problem is in step 4; the only way to compare items is by their displayName but since it's possible to have for example two reports with the same name in two different folders, I can never delete one of those reports (the one that is orphaned) from test. The compare will see that there is an item with that report name in dev (located in a different folder but folder id's are unique as well so can't check that the folder name is the same) and so will not delete either of the two (or more) reports from test.
Hi @SanderTK , I think you could track items by their unique Item IDs instead of names to avoid issues with duplicates. Then, automate a cleanup script to compare Item IDs between dev and test
The problem with this is that paired items between dev and test for example have different item IDs (hence the uniqueness).
The items will be deleted in the sprints between deployments so I would have to categorize all item IDs in dev post deployment and then compare that weeks later to the list of item IDs in dev before the next deployment to see which items have been deleted. It is then still impossible to link that to the items in test because of the unique item IDs.
Oh then cant you use metadata or tags like created_by_dev and tracking the item relationships manually through a separate process
This is your chance to engage directly with the engineering team behind Fabric and Power BI. Share your experiences and shape the future.
Check out the June 2025 Fabric update to learn about new features.
User | Count |
---|---|
57 | |
32 | |
14 | |
14 | |
5 |
User | Count |
---|---|
71 | |
65 | |
26 | |
8 | |
7 |