In today’s fast-paced analytics landscape, Microsoft Fabric has become the leader of enterprise BI implementations, one of the fundamental concepts that allows us to work with Fabric and Power BI is workspace which acts as a starting point in every implementation. But manually creating and configuring workspaces across all the environments is error-prone, time-consuming, and hard to audit. Because of that in most cases I recommend to automate this work.
There is many ways to automate workspace creation, but today I would like to show you how to achieve that via Azure DevOps Pipelines. In this post, we’ll walk through a production-ready YAML pipeline that automatically creates Fabric workspaces with consistent naming, assigns them to a Fabric premium capacity, and applies role-based access control (RBAC) using Microsoft Entra security groups — all in a single, idempotent run. Let’s see how it works.
trigger: - none pool: vmImage: 'ubuntu-latest'
- Manual trigger only (trigger: none) – perfect for controlled deployments that are executed on demand.
- Runs on Ubuntu agent for broad tool compatibility, in this case we use REST API only so there is no need to use Windows specific machine.
After that we have parameters that we will modify every single time:
parameters:
- name: numberOfEnvironments
displayName: 'Number of environments'
type: string
default: '3'
values: ['1', '3']
- name: functionalName
displayName: 'Functional name'
type: string
default: ''
- name: unit
displayName: 'Unit'
type: string
default: 'All'
values: ['All', 'Denmark', 'Poland', 'USA']
- name: businessReadersGroupId
displayName: 'Business Readers Group ID'
type: string
default: ''
These parameters are descriptive enough, but just to be clear:
- numberOfEnvironments – How many workspaces we want to create. For me it’s typically 3 (DEV, TEST, PROD), but sometimes only one is needed. In this approach we treat each workspace as an “environment”.
- functionalName – Just a meaningful name for the workspace.
- unit – Indicates which branch or organizational unit this workspace belongs to in your organization.
- businessReadersGroupId – The object ID (from Entra ID) of the group that should get Reader access by default. We could also retrieve this automatically from Entra ID if the Service Principal used in the Service Connection has permission to read groups — here I’ve kept it simple by allowing you to enter the ID manually.
Let’s move on to the next section, which contains a simple variable that holds the workspace name — it’s built dynamically.
# Map unit to code
case "${{ parameters.unit }}" in
All) unitCode="" ;;
Denmark) unitCode="DK" ;;
Poland) unitCode="PL" ;;
"USA") unitCode="US" ;;
*) unitCode="" ;;
esac
baseName="IT"
[ -n "$unitCode" ] && baseName="$baseName - $unitCode"
[ -n "$functionalName" ] && baseName="$baseName - $functionalName"
workspaceName="$baseName - [$env]"
Next we determine based on parameter environment and how many of them we want:
if [ "${{ parameters.numberOfEnvironments }}" == "3" ]; then
envs=("DEV" "TEST" "PROD")
else
envs=("PROD")
fi
It works in a very straightforward way:
- If you ask for three environments → DEV, TEST, and PROD are created.
- If you ask for only one → it automatically becomes PROD.
Our workspace naming is done so let’s try to authenticate. Below one-liner uses the Azure CLI to fetch a short-lived access token specifically for the Power BI API and stores it in the token variable – ready to be used in the next curl or REST API calls.
token=$(az account get-access-token --resource https://analysis.windows.net/powerbi/api --query accessToken -o tsv)
The curl part is pretty clear: we use the GET method on the https://api.powerbi.com/v1.0/myorg/groups endpoint to locate the workspace and capture its ID if it already exists.
token=$(az account get-access-token --resource https://analysis.windows.net/powerbi/api --query accessToken -o tsv)
for env in "${envs[@]}"; do
if [ -n "$env" ]; then
workspaceName="$baseName - [$env]"
else
workspaceName="$baseName"
fi
echo "Processing workspace: $workspaceName"
echo "Checking if workspace exists..."
groups_response=$(curl -s -X GET "https://api.powerbi.com/v1.0/myorg/groups" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json")
workspaceId=$(echo "$groups_response" | jq -r ".value[] | select(.name==\"$workspaceName\") | .id")
if [ -z "$workspaceId" ]; then
echo "Workspace does not exist. Creating $workspaceName..."
create_response=$(curl -s -X POST "https://api.powerbi.com/v1.0/myorg/groups" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d "{\"name\": \"$workspaceName\"}")
workspaceId=$(echo "$create_response" | jq -r '.id')
if [ -z "$workspaceId" ] || [ "$workspaceId" == "null" ]; then
echo "Workspace creation failed. Response: $create_response"
exit 1
fi
echo "Created workspace: $workspaceId"
else
echo "Workspace '$workspaceName' already exists with ID: $workspaceId"
fi
if [ -z "${{ variables.capacityId }}" ] || [ "${{ variables.capacityId }}" == "null" ]; then
echo "Capacity ID is null or empty. Exiting."
exit 1
fi
- If exists → skip creation
- If not → POST /groups → extract id
Next part is assigning to capacity – it is just another REST API call (we have to know capacityId so it is good to keep it as a variable in the script):
curl -X POST "/groups/$workspaceId/AssignToCapacity" -d "{\"capacityId\": \"${{ variables.capacityId }}\"}"
In many cases, I have some predefined technical roles that should be applied to all workspaces. The table below is just a quick summary of how those groups are assigned and where:
| Role | Group | Applied To |
|---|---|---|
| Admin | adminGroupId, adminGroupId2 |
All workspaces |
| Contributor | opsGroupId |
All workspaces |
| Viewer | viewerGroupId |
All workspaces |
| Viewer (Business) | Readers, Testers, Creators | PROD only |
| Viewer (Testers/Creators) | Testers, Creators | TEST only |
| Environment | Business Readers | Business Testers | Business Creators |
|---|---|---|---|
| PROD | Viewer | Viewer | Viewer |
| TEST | – | Viewer | Viewer |
| DEV | – | – | – |
Only relevant stakeholders get access — least privilege principle applied by default.
Error Handling & Safety
- set -e → fail fast on any error
- Every API call checks for “error” in response
- Logs clear success/failure messages
- Exits with code 1 on failure → pipeline fails visibly
How to Use This Pipeline
- Store in Azure Repos in my case it is fabric-workspace-creation.yml
- Create service connection: in my case it power-bi-automation-service-connection (with Power BI + Entra permissions if you don’t want to hardcode groups IDs)
- Set variables in pipeline (Library or YAML):
variables:
capacityId: ‘DFE4E7B2-B6EC-429A-A922-A9SAB6821E23A’
adminGroupId: ‘7eefcaca-…’
# … etc
Run manually on demand fill parameters and it is done!
Conclusion
This pipeline turns a 30-minute manual process into a 3-minute automated, governed, and repeatable deployment. Whether you’re managing 1 or 100 analytics solutions, this pattern scales.
Below you can find entire script that you can adjust for your needs, I didn’t explain every single detail but I think right now it is pretty simple.
trigger:
- none
pool:
vmImage: 'ubuntu-latest'
parameters:
- name: numberOfEnvironments
displayName: 'Number of environments'
type: string
default: '3'
values:
- '1'
- '3'
- name: functionalName
displayName: 'Functional name'
type: string
default: ''
- name: unit
displayName: 'Unit'
type: string
default: 'Group'
values:
- 'Denmark'
- 'Poland'
- 'USA'
- 'All'
- name: businessReadersGroupId
displayName: 'Business Readers Group ID'
type: string
default: ''
- name: businessTestersGroupId
displayName: 'Business Testers Group ID'
type: string
default: ''
- name: businessCreatorsGroupId
displayName: 'Business Creators Group ID'
type: string
default: ''
variables:
adminGroupId: 'a1b2c3d4-5678-90ef-ghij-klmnopqrst12'
adminGroupId2: 'e5f6g7h8-9012-34ij-klmn-opqrstuvwx34'
opsGroupId: 'b9c8d7e6-f5a4-3b2c-1d0e-f9g8h7i6j5k4'
viewerGroupId: 'p0o9i8u7-y6t5-r4e3-w2q1-abcdef123456'
capacityId: 'F8G9H1J2-K3L4-M5N6-O7P8-Q9R0S1T2U3V4'
steps:
- task: AzureCLI@2
displayName: 'Create Fabric Workspace(s) with naming pattern'
inputs:
azureSubscription: 'platform.bi-automation-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
set -e
# Map unit to code
case "${{ parameters.unit }}" in
Denmark) unitCode="DK" ;;
Poland) unitCode="PL" ;;
USA) unitCode="US" ;;
"All") unitCode="" ;;
*) unitCode="" ;;
esac
# Prepare workspace base name
baseName="IT"
if [ -n "$unitCode" ]; then
baseName="$baseName - $unitCode"
fi
if [ -n "${{ parameters.functionalName }}" ]; then
baseName="$baseName - ${{ parameters.functionalName }}"
fi
# Determine environments
if [ "${{ parameters.numberOfEnvironments }}" == "3" ]; then
envs=("DEV" "TEST" "PROD")
else
envs=("PROD")
fi
echo "Getting access token..."
token=$(az account get-access-token --resource https://analysis.windows.net/powerbi/api --query accessToken -o tsv)
for env in "${envs[@]}"; do
if [ -n "$env" ]; then
workspaceName="$baseName - [$env]"
else
workspaceName="$baseName"
fi
echo "Processing workspace: $workspaceName"
echo "Checking if workspace exists..."
groups_response=$(curl -s -X GET "https://api.powerbi.com/v1.0/myorg/groups" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json")
workspaceId=$(echo "$groups_response" | jq -r ".value[] | select(.name==\"$workspaceName\") | .id")
if [ -z "$workspaceId" ]; then
echo "Workspace does not exist. Creating $workspaceName..."
create_response=$(curl -s -X POST "https://api.powerbi.com/v1.0/myorg/groups" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d "{\"name\": \"$workspaceName\"}")
workspaceId=$(echo "$create_response" | jq -r '.id')
if [ -z "$workspaceId" ] || [ "$workspaceId" == "null" ]; then
echo "Workspace creation failed. Response: $create_response"
exit 1
fi
echo "Created workspace: $workspaceId"
else
echo "Workspace '$workspaceName' already exists with ID: $workspaceId"
fi
if [ -z "${{ variables.capacityId }}" ] || [ "${{ variables.capacityId }}" == "null" ]; then
echo "Capacity ID is null or empty. Exiting."
exit 1
fi
echo "Assigning workspace $workspaceId to capacity ${{ variables.capacityId }}..."
assign_capacity_response=$(curl -s -X POST "https://api.powerbi.com/v1.0/myorg/groups/$workspaceId/AssignToCapacity" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d "{\"capacityId\": \"${{ variables.capacityId }}\"}")
if echo "$assign_capacity_response" | grep -q '"error"'; then
echo "Failed to assign workspace to capacity. Response: $assign_capacity_response"
exit 1
fi
# Get current users for the workspace
users_response=$(curl -s -X GET "https://api.powerbi.com/v1.0/myorg/groups/$workspaceId/users" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json")
# Assign Admin permissions to both admin groups
for adminGroup in "${{ variables.adminGroupId }}" "${{ variables.adminGroupId2 }}"; do
has_admin=$(echo "$users_response" | jq -r ".value[] | select(.identifier==\"$adminGroup\" and .groupUserAccessRight==\"Admin\") | .identifier")
if [ -z "$has_admin" ]; then
echo "Assigning Admin permission to group $adminGroup..."
assign_response=$(curl -s -X POST "https://api.powerbi.com/v1.0/myorg/groups/$workspaceId/users" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d "{\"identifier\": \"$adminGroup\", \"groupUserAccessRight\": \"Admin\", \"principalType\": \"Group\"}")
if echo "$assign_response" | grep -q '"error"'; then
echo "Failed to assign admin group. Response: $assign_response"
exit 1
fi
else
echo "Group $adminGroup already has Admin permission. Skipping."
fi
done
# Assign Contributor (Ops)
has_contributor=$(echo "$users_response" | jq -r ".value[] | select(.identifier==\"${{ variables.opsGroupId }}\" and .groupUserAccessRight==\"Contributor\") | .identifier")
if [ -z "$has_contributor" ]; then
echo "Assigning Contributor permission to group ${{ variables.opsGroupId }}..."
assign_ops_response=$(curl -s -X POST "https://api.powerbi.com/v1.0/myorg/groups/$workspaceId/users" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d "{\"identifier\": \"${{ variables.opsGroupId }}\", \"groupUserAccessRight\": \"Contributor\", \"principalType\": \"Group\"}")
if echo "$assign_ops_response" | grep -q '"error"'; then
echo "Failed to assign ops group. Response: $assign_ops_response"
exit 1
fi
else
echo "Group ${{ variables.opsGroupId }} already has Contributor permission. Skipping."
fi
# Assign Viewer
has_viewer=$(echo "$users_response" | jq -r ".value[] | select(.identifier==\"${{ variables.viewerGroupId }}\" and .groupUserAccessRight==\"Viewer\") | .identifier")
if [ -z "$has_viewer" ]; then
echo "Assigning Viewer permission to group ${{ variables.viewerGroupId }}..."
assign_viewer_response=$(curl -s -X POST "https://api.powerbi.com/v1.0/myorg/groups/$workspaceId/users" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d "{\"identifier\": \"${{ variables.viewerGroupId }}\", \"groupUserAccessRight\": \"Viewer\", \"principalType\": \"Group\"}")
if echo "$assign_viewer_response" | grep -q '"error"'; then
echo "Failed to assign viewer group. Response: $assign_viewer_response"
exit 1
fi
else
echo "Group ${{ variables.viewerGroupId }} already has Viewer permission. Skipping."
fi
# Assign Business groups as Viewers on PROD only
if [ "$env" == "PROD" ]; then
for groupId in "${{ parameters.businessReadersGroupId }}" "${{ parameters.businessTestersGroupId }}" "${{ parameters.businessCreatorsGroupId }}"; do
if [ -n "$groupId" ]; then
has_biz_viewer=$(echo "$users_response" | jq -r ".value[] | select(.identifier==\"$groupId\" and .groupUserAccessRight==\"Viewer\") | .identifier")
if [ -z "$has_biz_viewer" ]; then
echo "Assigning Viewer permission to business group $groupId on PROD..."
assign_biz_response=$(curl -s -X POST "https://api.powerbi.com/v1.0/myorg/groups/$workspaceId/users" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d "{\"identifier\": \"$groupId\", \"groupUserAccessRight\": \"Viewer\", \"principalType\": \"Group\"}")
if echo "$assign_biz_response" | grep -q '"error"'; then
echo "Failed to assign business group as viewer. Response: $assign_biz_response"
exit 1
fi
else
echo "Business group $groupId already has Viewer permission on PROD. Skipping."
fi
fi
done
fi
# Only assign TEST permissions if numberOfEnvironments is 3
if [ "${{ parameters.numberOfEnvironments }}" == "3" ]; then
# Assign Business Testers and Creators as Viewers on TEST
if [ "$env" == "TEST" ]; then
for groupId in "${{ parameters.businessTestersGroupId }}" "${{ parameters.businessCreatorsGroupId }}"; do
if [ -n "$groupId" ]; then
has_biz_viewer=$(echo "$users_response" | jq -r ".value[] | select(.identifier==\"$groupId\" and .groupUserAccessRight==\"Viewer\") | .identifier")
if [ -z "$has_biz_viewer" ]; then
echo "Assigning Viewer permission to business group $groupId on TEST..."
assign_biz_response=$(curl -s -X POST "https://api.powerbi.com/v1.0/myorg/groups/$workspaceId/users" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d "{\"identifier\": \"$groupId\", \"groupUserAccessRight\": \"Viewer\", \"principalType\": \"Group\"}")
if echo "$assign_biz_response" | grep -q '"error"'; then
echo "Failed to assign business group as viewer. Response: $assign_biz_response"
exit 1
fi
else
echo "Business group $groupId already has Viewer permission on TEST. Skipping."
fi
fi
done
fi
fi
echo "Workspace $workspaceName processed successfully."
done
I hope you find it useful!
- Automating Microsoft Fabric Workspace Creation with Azure DevOps Pipelines - November 18, 2025
- Creating Private Endpoint to Microsoft Fabric workspace - October 12, 2025
- Setup Microsoft Fabric EventStream Custom Endpoint - October 6, 2025


Last comments