Dashboards and Publishing Strategies: Enterprise Governance
Introduction
Publishing Power BI content at enterprise scale requires more than just creating reportsβit demands a structured approach to workspace organization, controlled distribution, reliable refresh operations, and comprehensive governance. Without proper publishing strategies, organizations face workspace sprawl, inconsistent access patterns, duplicate content, and frustrated users unable to find trusted data.
This guide provides battle-tested strategies for workspace architecture, Power BI Apps deployment, audience management, refresh orchestration, content certification, and change management processes that scale from dozens to thousands of users.
Prerequisites
- Power BI Pro or Premium Per User licenses
- Power BI Administrator or Workspace Admin role
- Understanding of Power BI service components (workspaces, apps, datasets)
- Familiarity with Azure Active Directory security groups
- Access to Power BI REST API for automation (optional)
Workspace Architecture Strategy
Workspace Types and Purposes
Enterprise Workspace Hierarchy:
βββββββββββββββββββββββββββββββββββββββββββββββ
β DEVELOPMENT WORKSPACES β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β Purpose: Build, test, iterate β
β Access: Report creators, developers β
β Refresh: Manual or test schedules β
β Examples: β
β - Sales-Dev β
β - Finance-Dev β
β - HR-Analytics-Dev β
βββββββββββββββββββββββββββββββββββββββββββββββ
β Deployment Pipeline
βββββββββββββββββββββββββββββββββββββββββββββββ
β TEST WORKSPACES β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β Purpose: UAT, validation, stakeholder reviewβ
β Access: Testers, business stakeholders β
β Refresh: Production-like schedules β
β Examples: β
β - Sales-Test β
β - Finance-Test β
β - HR-Analytics-Test β
βββββββββββββββββββββββββββββββββββββββββββββββ
β Approval Process
βββββββββββββββββββββββββββββββββββββββββββββββ
β PRODUCTION WORKSPACES β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β Purpose: Certified, published artifacts β
β Access: Limited (admins only) β
β Refresh: Production schedules with SLAs β
β Distribution: Via Power BI Apps β
β Examples: β
β - Sales-Prod β
β - Finance-Prod β
β - HR-Analytics-Prod β
βββββββββββββββββββββββββββββββββββββββββββββββ
β Consumed via Apps
βββββββββββββββββββββββββββββββββββββββββββββββ
β DATA WORKSPACES β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β Purpose: Shared datasets, dataflows β
β Access: Downstream report creators (Build) β
β Refresh: Foundational data refresh β
β Examples: β
β - Enterprise-Data-Platform β
β - Finance-Shared-Datasets β
β - HR-Master-Data β
βββββββββββββββββββββββββββββββββββββββββββββββ
Workspace Naming Convention
Standard Format: [Department]-[Function]-[Environment]
Examples:
β
Sales-Analytics-Dev
β
Finance-Reporting-Prod
β
HR-Metrics-Test
β
Marketing-DataPlatform-Prod
Avoid:
β Johns Workspace
β Test123
β New Workspace (1)
β Copy of Sales Reports
Workspace Configuration Script
# Automated workspace creation with governance
function New-GovernedWorkspace {
param(
[string]$Department,
[string]$Function,
[string]$Environment,
[string]$AdminGroupId,
[string]$ContributorGroupId = $null
)
$workspaceName = "$Department-$Function-$Environment"
# Create workspace
$workspace = New-PowerBIWorkspace -Name $workspaceName
# Add admin group
Add-PowerBIWorkspaceUser -Id $workspace.Id `
-UserPrincipalName $AdminGroupId `
-AccessRight Admin `
-PrincipalType Group
# Add contributors if provided
if ($ContributorGroupId) {
Add-PowerBIWorkspaceUser -Id $workspace.Id `
-UserPrincipalName $ContributorGroupId `
-AccessRight Contributor `
-PrincipalType Group
}
# Tag workspace with metadata
$tags = @{
Department = $Department
Environment = $Environment
CreatedDate = (Get-Date).ToString("yyyy-MM-dd")
Owner = $AdminGroupId
}
# Store tags (using naming convention or external database)
Write-Host "Created workspace: $workspaceName"
Write-Host "Tags: $($tags | ConvertTo-Json)"
return $workspace
}
# Create workspace structure for a department
New-GovernedWorkspace -Department "Sales" -Function "Analytics" -Environment "Dev" `
-AdminGroupId "sales-analytics-admins@contoso.com" `
-ContributorGroupId "sales-analysts@contoso.com"
New-GovernedWorkspace -Department "Sales" -Function "Analytics" -Environment "Test" `
-AdminGroupId "sales-analytics-admins@contoso.com"
New-GovernedWorkspace -Department "Sales" -Function "Analytics" -Environment "Prod" `
-AdminGroupId "sales-analytics-admins@contoso.com"
Power BI Apps: Distribution at Scale
App Architecture
Power BI App Structure:
App: "Sales Analytics"
βββ Navigation
β βββ Section: "Executive Dashboard"
β β βββ Report: Sales Overview
β β βββ Report: Key Metrics
β βββ Section: "Regional Performance"
β β βββ Report: North America
β β βββ Report: EMEA
β β βββ Report: APAC
β βββ Section: "Product Analysis"
β βββ Report: Product Performance
β βββ Report: Inventory Insights
β
βββ Audiences (Role-Based Content)
β βββ Audience: "Executives"
β β βββ Sees: Executive Dashboard only
β βββ Audience: "Regional Managers"
β β βββ Sees: Executive + Regional Performance
β βββ Audience: "All Users"
β βββ Sees: All sections
β
βββ Settings
βββ Description: "Official sales analytics..."
βββ Support Contact: sales-analytics@contoso.com
βββ Auto-install: Enabled for specific groups
βββ Navigation pane: Enabled
Creating and Publishing Apps
# Create Power BI App from workspace
function Publish-PowerBIApp {
param(
[string]$WorkspaceId,
[string]$AppName,
[string]$Description,
[string[]]$AudienceGroups,
[hashtable]$Navigation
)
# App configuration
$appConfig = @{
name = $AppName
description = $Description
publishedState = "Published"
audiences = @()
navigation = @{
sections = @()
}
}
# Configure audiences
foreach ($group in $AudienceGroups) {
$appConfig.audiences += @{
groupObjectId = $group
name = "Audience-$group"
}
}
# Configure navigation sections
foreach ($section in $Navigation.Keys) {
$appConfig.navigation.sections += @{
name = $section
reports = $Navigation[$section]
}
}
$body = $appConfig | ConvertTo-Json -Depth 10
# Publish app via REST API
$app = Invoke-PowerBIRestMethod -Url "groups/$WorkspaceId/apps" `
-Method Post -Body $body
Write-Host "Published app: $AppName"
return $app
}
# Example: Publish Sales Analytics app
$navigation = @{
"Executive Dashboard" = @("sales-overview-report-id", "key-metrics-report-id")
"Regional Performance" = @("na-report-id", "emea-report-id", "apac-report-id")
"Product Analysis" = @("product-perf-report-id", "inventory-report-id")
}
Publish-PowerBIApp `
-WorkspaceId "workspace-id" `
-AppName "Sales Analytics" `
-Description "Official sales reporting and analytics for all regions" `
-AudienceGroups @("sales-team-group-id", "executives-group-id") `
-Navigation $navigation
Audience Segmentation Strategy
# Configure app audiences for role-based content access
function Set-AppAudiences {
param(
[string]$AppId,
[array]$Audiences
)
# Example audience configuration:
# Executives: See only high-level dashboards
# Regional Managers: See regional + executive content
# Analysts: See all content
$audienceConfig = @{
audiences = @(
@{
groupObjectId = "executives-group-id"
name = "Executives"
sections = @("Executive Dashboard")
},
@{
groupObjectId = "regional-managers-group-id"
name = "Regional Managers"
sections = @("Executive Dashboard", "Regional Performance")
},
@{
groupObjectId = "all-analysts-group-id"
name = "All Analysts"
sections = @("Executive Dashboard", "Regional Performance", "Product Analysis")
}
)
}
$body = $audienceConfig | ConvertTo-Json -Depth 10
Invoke-PowerBIRestMethod -Url "apps/$AppId/audiences" `
-Method Put -Body $body
Write-Host "Configured audiences for app: $AppId"
}
Audience Best Practices:
- Use Azure AD security groups, not individual users
- Keep audience names descriptive and tied to business roles
- Document which groups see which content
- Review audience membership quarterly
- Avoid creating too many audiences (max 3-5 per app)
Dataset Refresh Orchestration
Refresh Scheduling Strategy
Refresh Schedule Matrix:
Dataset Type β Frequency β Time Window β Dependencies
βββββββββββββββββββββββΌβββββββββββββββββΌβββββββββββββββββΌββββββββββββββ
Master Data (Customers)β Once daily β 2:00 AM β None
Sales Transactions β 4x daily β 6 AM, 12 PM, β Master Data
β β 6 PM, 11 PM β
Inventory β Hourly β Every hour β Master Data
β β 7 AM - 10 PM β
Financial Reporting β Once daily β 7:00 AM β Sales, Inventory
Executive Dashboard β Once daily β 8:00 AM β All above
Key Principles:
1. Foundational data refreshes first (master data)
2. Transactional data refreshes next (sales, orders)
3. Aggregated/reporting datasets refresh last
4. Stagger refreshes to avoid gateway saturation
5. Leave buffer time between dependent datasets
Automated Refresh Configuration
# Configure dataset refresh schedule
function Set-DatasetRefreshSchedule {
param(
[string]$DatasetId,
[string]$WorkspaceId,
[array]$RefreshTimes, # e.g., @("06:00", "12:00", "18:00")
[array]$Days = @("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"),
[string]$TimeZone = "UTC",
[bool]$Enabled = $true,
[bool]$NotifyOnFailure = $true
)
$scheduleConfig = @{
value = @{
days = $Days
times = $RefreshTimes
enabled = $Enabled
localTimeZoneId = $TimeZone
NotifyOption = if ($NotifyOnFailure) { "MailOnFailure" } else { "NoNotification" }
}
}
$body = $scheduleConfig | ConvertTo-Json -Depth 5
Invoke-PowerBIRestMethod `
-Url "groups/$WorkspaceId/datasets/$DatasetId/refreshSchedule" `
-Method Patch -Body $body
Write-Host "Configured refresh schedule for dataset: $DatasetId"
}
# Example: Configure tiered refresh schedule
# Master data: Once daily at 2 AM
Set-DatasetRefreshSchedule `
-DatasetId "master-data-dataset-id" `
-WorkspaceId "data-workspace-id" `
-RefreshTimes @("02:00") `
-TimeZone "Eastern Standard Time"
# Sales data: 4 times daily
Set-DatasetRefreshSchedule `
-DatasetId "sales-dataset-id" `
-WorkspaceId "sales-workspace-id" `
-RefreshTimes @("06:00", "12:00", "18:00", "23:00") `
-TimeZone "Eastern Standard Time"
# Executive dashboard: Once daily at 8 AM
Set-DatasetRefreshSchedule `
-DatasetId "executive-dashboard-dataset-id" `
-WorkspaceId "exec-workspace-id" `
-RefreshTimes @("08:00") `
-Days @("Monday", "Tuesday", "Wednesday", "Thursday", "Friday") `
-TimeZone "Eastern Standard Time"
Refresh Monitoring and Alerting
# Monitor refresh operations and send alerts
function Monitor-DatasetRefreshes {
param(
[int]$LookbackDays = 1,
[string]$AlertEmail = "biops@contoso.com"
)
$failures = @()
# Get all workspaces
$workspaces = Get-PowerBIWorkspace -Scope Organization
foreach ($workspace in $workspaces) {
$datasets = Get-PowerBIDataset -WorkspaceId $workspace.Id
foreach ($dataset in $datasets) {
if ($dataset.IsRefreshable) {
# Get refresh history
$refreshHistory = Get-PowerBIDatasetRefreshHistory `
-DatasetId $dataset.Id `
-WorkspaceId $workspace.Id `
-Top 10
# Check for recent failures
$recentFailures = $refreshHistory | Where-Object {
$_.Status -eq "Failed" -and
$_.StartTime -gt (Get-Date).AddDays(-$LookbackDays)
}
if ($recentFailures) {
foreach ($failure in $recentFailures) {
$failures += [PSCustomObject]@{
Workspace = $workspace.Name
Dataset = $dataset.Name
FailureTime = $failure.EndTime
Error = ($failure.ServiceExceptionJson | ConvertFrom-Json).errorDescription
Duration = ($failure.EndTime - $failure.StartTime).TotalMinutes
}
}
}
}
}
}
# Generate alert report
if ($failures.Count -gt 0) {
$htmlReport = $failures | ConvertTo-Html -Title "Power BI Refresh Failures" -PreContent "<h2>Refresh Failures in Last $LookbackDays Day(s)</h2>"
Send-MailMessage `
-To $AlertEmail `
-Subject "Power BI Refresh Failures Detected: $($failures.Count) Failed Refreshes" `
-Body ($htmlReport | Out-String) `
-BodyAsHtml `
-SmtpServer "smtp.contoso.com" `
-From "powerbi-alerts@contoso.com"
Write-Host "Sent alert email for $($failures.Count) refresh failures"
} else {
Write-Host "No refresh failures detected in last $LookbackDays days"
}
return $failures
}
# Schedule this script to run daily
Monitor-DatasetRefreshes -LookbackDays 1 -AlertEmail "biops@contoso.com"
Refresh Dependency Management
# Orchestrate refresh with dependencies
function Start-DependentRefresh {
param(
[string]$DatasetId,
[string]$WorkspaceId,
[array]$DependencyDatasetIds,
[int]$MaxWaitMinutes = 60
)
Write-Host "Checking dependencies for dataset: $DatasetId"
# Wait for all dependencies to complete
foreach ($depId in $DependencyDatasetIds) {
$startTime = Get-Date
$completed = $false
while (-not $completed -and ((Get-Date) - $startTime).TotalMinutes -lt $MaxWaitMinutes) {
$refreshHistory = Get-PowerBIDatasetRefreshHistory `
-DatasetId $depId `
-WorkspaceId $WorkspaceId `
-Top 1
if ($refreshHistory.Status -eq "Completed") {
Write-Host " Dependency $depId completed successfully"
$completed = $true
} elseif ($refreshHistory.Status -eq "Failed") {
Write-Error " Dependency $depId failed! Aborting refresh of $DatasetId"
return $false
} else {
Write-Host " Waiting for dependency $depId (Status: $($refreshHistory.Status))..."
Start-Sleep -Seconds 30
}
}
if (-not $completed) {
Write-Error " Dependency $depId timed out after $MaxWaitMinutes minutes"
return $false
}
}
# All dependencies completed, start refresh
Write-Host "All dependencies completed. Starting refresh of dataset: $DatasetId"
Invoke-PowerBIRestMethod `
-Url "groups/$WorkspaceId/datasets/$DatasetId/refreshes" `
-Method Post
return $true
}
# Example: Refresh executive dashboard after all dependencies complete
Start-DependentRefresh `
-DatasetId "executive-dashboard-id" `
-WorkspaceId "exec-workspace-id" `
-DependencyDatasetIds @("master-data-id", "sales-id", "inventory-id") `
-MaxWaitMinutes 120
Dataset Certification and Endorsement
Certification Workflow
Dataset Lifecycle:
1. Development
β
2. Testing & Validation
β
3. Promotion (Dataset is "Promoted")
ββ Business owner approval
ββ Technical validation (data quality checks)
ββ Documentation complete
β
4. Certification (Dataset is "Certified")
ββ Meets data governance standards
ββ Refresh SLA documented and monitored
ββ Security/RLS implemented and tested
ββ Approved by data governance committee
ββ Icon displayed to users indicating trust
Certification Criteria Checklist:
β Data lineage documented
β Refresh schedule with SLA defined
β Row-level security implemented (if required)
β Dataset owner and support contact identified
β Data quality validation rules in place
β Incremental refresh configured (for large datasets)
β Performance tested with production data volumes
β Backup and disaster recovery plan documented
β Change management process defined
β User documentation published
Endorsement Configuration
# Endorse (Promote or Certify) a dataset
function Set-DatasetEndorsement {
param(
[string]$DatasetId,
[string]$WorkspaceId,
[ValidateSet("None", "Promoted", "Certified")]
[string]$Endorsement
)
$body = @{
endorsement = $Endorsement
} | ConvertTo-Json
Invoke-PowerBIRestMethod `
-Url "groups/$WorkspaceId/datasets/$DatasetId" `
-Method Patch -Body $body
Write-Host "Set endorsement to '$Endorsement' for dataset: $DatasetId"
}
# Promote dataset after testing
Set-DatasetEndorsement `
-DatasetId "sales-dataset-id" `
-WorkspaceId "sales-workspace-id" `
-Endorsement "Promoted"
# Certify dataset after governance approval
Set-DatasetEndorsement `
-DatasetId "sales-dataset-id" `
-WorkspaceId "sales-workspace-id" `
-Endorsement "Certified"
Certification Audit Report
# Generate dataset certification status report
function Get-DatasetCertificationReport {
$report = @()
$workspaces = Get-PowerBIWorkspace -Scope Organization
foreach ($workspace in $workspaces) {
$datasets = Get-PowerBIDataset -WorkspaceId $workspace.Id
foreach ($dataset in $datasets) {
$report += [PSCustomObject]@{
Workspace = $workspace.Name
Dataset = $dataset.Name
Endorsement = $dataset.Endorsement
IsRefreshable = $dataset.IsRefreshable
Owner = $dataset.ConfiguredBy
LastRefresh = $dataset.RefreshSchedule.LastRefresh
}
}
}
# Summary statistics
$summary = $report | Group-Object Endorsement | Select-Object Name, Count
Write-Host "`nDataset Certification Summary:"
$summary | Format-Table -AutoSize
# Export detailed report
$report | Export-Csv "Dataset-Certification-Report-$(Get-Date -Format 'yyyy-MM-dd').csv" -NoTypeInformation
return $report
}
Get-DatasetCertificationReport
Change Management and Release Process
Release Notes Template
# Power BI Release Notes - [App/Workspace Name]
**Release Date**: 2025-11-23
**Version**: 2.4.0
**Release Type**: Minor Update
## What's New
- Added new "Customer Lifetime Value" report to Executive Dashboard section
- Implemented drill-through from Sales Overview to Customer Details
- Added year-over-year comparison visuals to Regional Performance
## Enhancements
- Improved loading performance for Product Analysis reports (50% faster)
- Updated color scheme to match new corporate branding
- Added tooltips with contextual help to key metrics
## Bug Fixes
- Fixed date filter issue in EMEA report showing incorrect fiscal year
- Corrected currency conversion for international sales
- Resolved dashboard tile refresh issue
## Data Updates
- Added new product categories: "Smart Home" and "Wearables"
- Historical data extended back to 2018 (previously 2020)
- Improved data quality checks for customer addresses
## Breaking Changes
None
## Known Issues
- [Minor] Export to PDF may show slight formatting differences
- [Minor] Custom theme may not apply to all embedded visuals
## Upcoming Features (Next Release)
- Mobile-optimized layouts for all reports
- Predictive analytics for sales forecasting
- Integration with Dynamics 365 data
## Support
Questions? Contact: sales-analytics@contoso.com
Documentation: https://intranet.contoso.com/powerbi/sales-analytics
Automated Release Communication
# Send release notification to app users
function Send-ReleaseNotification {
param(
[string]$AppName,
[string]$Version,
[string]$ReleaseNotesPath,
[string]$TeamsWebhookUrl
)
$releaseNotes = Get-Content $ReleaseNotesPath -Raw
# Create Teams adaptive card
$card = @{
type = "message"
attachments = @(
@{
contentType = "application/vnd.microsoft.card.adaptive"
content = @{
type = "AdaptiveCard"
body = @(
@{
type = "TextBlock"
text = "π $AppName Updated"
size = "Large"
weight = "Bolder"
},
@{
type = "TextBlock"
text = "Version $Version is now available"
isSubtle = $true
},
@{
type = "TextBlock"
text = $releaseNotes
wrap = $true
}
)
actions = @(
@{
type = "Action.OpenUrl"
title = "Open App"
url = "https://app.powerbi.com/groups/me/apps/$AppName"
},
@{
type = "Action.OpenUrl"
title = "View Full Release Notes"
url = "https://intranet.contoso.com/powerbi/releases/$Version"
}
)
}
}
)
} | ConvertTo-Json -Depth 20
Invoke-RestMethod -Uri $TeamsWebhookUrl -Method Post -Body $card -ContentType "application/json"
Write-Host "Release notification sent for $AppName v$Version"
}
# Send notification
Send-ReleaseNotification `
-AppName "Sales Analytics" `
-Version "2.4.0" `
-ReleaseNotesPath "C:\Releases\SalesAnalytics-v2.4.0-ReleaseNotes.md" `
-TeamsWebhookUrl "https://contoso.webhook.office.com/webhookb2/..."
Distribution Patterns and Access Control
Access Pattern Matrix
| Pattern | Use Case | Pros | Cons | Recommended For |
|---|---|---|---|---|
| Power BI App | Broad distribution to business users | Easy updates, structured navigation, audience segmentation | Limited customization per user | Most scenarios, 90% of users |
| Workspace Access | Report creators and developers | Full edit capabilities, immediate updates | Requires Pro license, complex permissions | <10% of users (creators) |
| Embedded (Power BI Embedded) | External portals, customer-facing | Custom branding, application integration | Additional licensing, development effort | External users, ISVs |
| Publish to Web | Public anonymous access | No authentication needed | β οΈ Security risk, limited features | Marketing demos only (avoid for internal data) |
| Secure Embed | SharePoint, Teams | Contextual access, collaboration | Requires authentication | Internal collaboration scenarios |
Implementing App-First Strategy
# Audit and enforce app-first distribution
function Audit-WorkspaceDirectAccess {
$violations = @()
$workspaces = Get-PowerBIWorkspace -Scope Organization
foreach ($workspace in $workspaces) {
# Skip dev/test workspaces
if ($workspace.Name -notlike "*-Prod") {
continue
}
# Get workspace users
$users = Get-PowerBIWorkspaceUser -WorkspaceId $workspace.Id
# Count non-admin users (should be minimal in prod)
$viewers = $users | Where-Object {$_.AccessRight -in @("Viewer", "Contributor")}
if ($viewers.Count -gt 0) {
$violations += [PSCustomObject]@{
Workspace = $workspace.Name
DirectViewers = $viewers.Count
Users = ($viewers.UserPrincipalName -join ", ")
Recommendation = "Migrate to App-based distribution"
}
}
}
if ($violations.Count -gt 0) {
Write-Host "`nβ οΈ Found $($violations.Count) production workspaces with direct user access:"
$violations | Format-Table -AutoSize
Write-Host "`nRecommendation: Migrate users to Power BI Apps for better governance"
} else {
Write-Host "β
All production workspaces follow app-first distribution model"
}
return $violations
}
Audit-WorkspaceDirectAccess
Governance Controls and Policies
Workspace Governance Policies
Governance Policy Document:
1. WORKSPACE CREATION
β Requires approval from BI Admin team
β Must follow naming convention: [Dept]-[Function]-[Env]
β Requester must provide:
- Business justification
- Expected user count
- Data sensitivity classification
- Project timeline
2. WORKSPACE PERMISSIONS
β Production workspaces: Admin-only access
β Dev workspaces: Creator access for developers
β Test workspaces: Limited stakeholder access
β Use Azure AD groups, not individual users
β Review permissions quarterly
3. CONTENT PUBLISHING
β All production content distributed via Apps
β Datasets must be certified before app publication
β Release notes required for all updates
β Change approval required for breaking changes
4. DATA REFRESH
β Refresh schedules documented with SLAs
β Failure alerts configured
β Dependency mapping maintained
β Monthly refresh performance review
5. RETENTION
β Dev workspaces: 90-day inactivity deletion
β Test workspaces: Deleted after project completion
β Prod workspaces: Retain while business-relevant
β Archived workspaces exported to PBIX + metadata
6. AUDIT AND COMPLIANCE
β Monthly governance report generated
β Quarterly access review
β Annual full audit
β Sensitivity labels applied to all datasets
Automated Governance Checks
# Daily governance check script
function Invoke-GovernanceChecks {
$report = @{
Date = Get-Date -Format "yyyy-MM-dd"
Violations = @()
Warnings = @()
Passed = @()
}
# Check 1: Uncertified datasets in production apps
$prodWorkspaces = Get-PowerBIWorkspace -Scope Organization -Filter "name like '%-Prod'"
foreach ($workspace in $prodWorkspaces) {
$datasets = Get-PowerBIDataset -WorkspaceId $workspace.Id
$uncertified = $datasets | Where-Object {$_.Endorsement -ne "Certified"}
if ($uncertified.Count -gt 0) {
$report.Warnings += "Workspace '$($workspace.Name)' has $($uncertified.Count) uncertified datasets"
}
}
# Check 2: Workspaces without recent activity
$allWorkspaces = Get-PowerBIWorkspace -Scope Organization
foreach ($workspace in $allWorkspaces) {
if ($workspace.Name -like "*-Dev" -and
(Get-Date) - $workspace.OnPremisesLastSyncDateTime -gt (New-TimeSpan -Days 90)) {
$report.Violations += "Dev workspace '$($workspace.Name)' inactive for >90 days - candidate for deletion"
}
}
# Check 3: Direct user access to production workspaces
$directAccessViolations = Audit-WorkspaceDirectAccess
if ($directAccessViolations.Count -gt 0) {
$report.Warnings += "$($directAccessViolations.Count) production workspaces have direct user access"
}
# Check 4: Refresh failures in last 24 hours
$refreshFailures = Monitor-DatasetRefreshes -LookbackDays 1
if ($refreshFailures.Count -gt 0) {
$report.Violations += "$($refreshFailures.Count) dataset refresh failures in last 24 hours"
}
# Check 5: Orphaned workspaces (no admin)
foreach ($workspace in $allWorkspaces) {
$users = Get-PowerBIWorkspaceUser -WorkspaceId $workspace.Id
$admins = $users | Where-Object {$_.AccessRight -eq "Admin"}
if ($admins.Count -eq 0) {
$report.Violations += "Workspace '$($workspace.Name)' has NO ADMINS - critical issue"
}
}
# Generate summary
Write-Host "`n=== Power BI Governance Report ==="
Write-Host "Date: $($report.Date)"
Write-Host "Violations: $($report.Violations.Count)"
Write-Host "Warnings: $($report.Warnings.Count)"
if ($report.Violations.Count -gt 0) {
Write-Host "`nβ VIOLATIONS:"
$report.Violations | ForEach-Object { Write-Host " - $_" }
}
if ($report.Warnings.Count -gt 0) {
Write-Host "`nβ οΈ WARNINGS:"
$report.Warnings | ForEach-Object { Write-Host " - $_" }
}
# Export report
$report | ConvertTo-Json -Depth 5 | Out-File "Governance-Report-$(Get-Date -Format 'yyyy-MM-dd').json"
return $report
}
# Schedule to run daily at 8 AM
Invoke-GovernanceChecks
Best Practices Summary
Workspace Management
- Standardize naming: Use consistent [Dept]-[Function]-[Env] format
- Separate environments: Always maintain Dev, Test, Prod separation
- Use deployment pipelines: Automate promotion from Dev β Test β Prod
- Limit production access: Only admins should access production workspaces directly
- Leverage data workspaces: Share datasets via dedicated data workspaces, not report workspaces
App Distribution
- App-first strategy: 90% of users should consume via apps, not workspace access
- Structured navigation: Organize reports into logical sections
- Audience segmentation: Use 3-5 audiences max, based on business roles
- Auto-install: Configure for key user groups to ensure discoverability
- Regular updates: Publish app updates on predictable schedule (weekly/monthly)
Refresh Operations
- Document dependencies: Map all dataset dependencies and refresh order
- Stagger schedules: Avoid concurrent refreshes competing for gateway resources
- Set realistic SLAs: Base on source system availability and processing time
- Monitor proactively: Alert on failures within 15 minutes
- Plan for scale: Use incremental refresh for datasets >10M rows
Certification and Trust
- Certify production datasets: All production datasets should be certified
- Document clearly: Include data lineage, refresh schedule, owner contact
- Enforce standards: Require certification before app publication
- Review quarterly: Re-certify datasets every 90 days
- Communicate clearly: Users should know which datasets to trust
Change Management
- Maintain release notes: Document every app update
- Communicate proactively: Notify users before major changes
- Test thoroughly: UAT in Test workspace before production deployment
- Support users: Provide clear documentation and support contacts
- Track feedback: Use Teams/feedback forms to capture user input
Troubleshooting Guide
Common Issues and Resolutions
Issue 1: Users Can't Find Reports
Symptoms:
- Users report "I can't find the Sales report"
- App not appearing in user's app list
Diagnosis:
# Check if user has access to app
$appId = "app-id"
$userEmail = "user@contoso.com"
# Get app audiences
$app = Invoke-PowerBIRestMethod -Url "apps/$appId" -Method Get | ConvertFrom-Json
# Check if user's group is in audience
$audiences = $app.audiences
$audiences | Format-Table groupObjectId, name
Resolution:
- Verify user is in correct Azure AD group
- Check app audience configuration
- Ensure app is published (not in draft)
- Configure auto-install for user's group
- Have user refresh https://app.powerbi.com/
Issue 2: Stale Data in Reports
Symptoms:
- Reports showing yesterday's data at 10 AM
- "Last refreshed" timestamp is old
Diagnosis:
# Check refresh history
$datasetId = "dataset-id"
$workspaceId = "workspace-id"
$refreshHistory = Get-PowerBIDatasetRefreshHistory -DatasetId $datasetId -WorkspaceId $workspaceId -Top 5
$refreshHistory | Format-Table Status, StartTime, EndTime, @{N='Duration';E={($ _.EndTime - $_.StartTime).TotalMinutes}}
Resolution:
- If Status = Failed: Review error message, check gateway connectivity, verify credentials
- If Status = Disabled: Re-enable refresh schedule
- If no recent refreshes: Check if refresh schedule is configured
- If refresh completes but data old: Verify source system has new data, check Power Query filters
Issue 3: Workspace Sprawl
Symptoms:
- Hundreds of workspaces with unclear purposes
- Duplicate content across workspaces
- Users creating ad-hoc workspaces
Diagnosis:
# Identify workspace sprawl
$workspaces = Get-PowerBIWorkspace -Scope Organization
$devWorkspaces = $workspaces | Where-Object {$_.Name -like "*-Dev"}
$testWorkspaces = $workspaces | Where-Object {$_.Name -like "*-Test"}
$prodWorkspaces = $workspaces | Where-Object {$_.Name -like "*-Prod"}
$uncategorized = $workspaces | Where-Object {$_.Name -notlike "*-Dev" -and $_.Name -notlike "*-Test" -and $_.Name -notlike "*-Prod"}
Write-Host "Total Workspaces: $($workspaces.Count)"
Write-Host " Dev: $($devWorkspaces.Count)"
Write-Host " Test: $($testWorkspaces.Count)"
Write-Host " Prod: $($prodWorkspaces.Count)"
Write-Host " Uncategorized: $($uncategorized.Count) β οΈ"
Resolution:
- Implement workspace approval workflow
- Enforce naming convention
- Audit and archive inactive workspaces
- Consolidate duplicate content
- Educate users on proper workspace usage
Key Takeaways
- Workspace strategy is foundational: Dev/Test/Prod separation with clear naming prevents chaos
- Apps are the primary distribution mechanism: Direct workspace access should be limited to admins and creators
- Refresh orchestration requires planning: Map dependencies, stagger schedules, monitor proactively
- Certification builds trust: Users need clear signals about which datasets are authoritative
- Governance is continuous: Automated checks, regular audits, and policy enforcement are essential
- Change management matters: Release notes, communication, and support prevent user frustration
- Automation scales governance: PowerShell scripts for monitoring, enforcement, and reporting
Next Steps
- Audit current workspace structure and identify violations
- Define and document workspace naming convention
- Create Power BI Apps for production content distribution
- Implement dataset certification workflow
- Configure refresh monitoring and alerting
- Establish deployment pipelines for Dev β Test β Prod
- Schedule daily governance check automation
- Document release management process
- Train content creators on governance policies
- Conduct quarterly governance review
Additional Resources
- Power BI Apps Documentation
- Deployment Pipelines
- Dataset Certification
- Refresh Scheduling
- Power BI REST API
- Governance Whitepaper
Structure. Distribute. Monitor. Govern.