Licensed software can be a pain, especially with manual user assignment and then deployment.
Fortunately for MS Project and MS Visio it can be automated so users can automatically receive Visio and Project upon being given a license and if you revoke the license, it can automatically uninstall.
Better still, the group creation can be scripted (and who doesn’t love a script!)
In this post I’ll run through both creating the groups via script and also deploying the apps themselves (manually, or fully scripted)
Fully Scripted Deployment
This script will create your Entra ID groups (as described below) and using the Microsoft Graph Powershell samples from Microsoft will create your Microsoft Project and Visio Applications, upload the intunewin file (again described below) and deploy to Intune with the Entra ID groups assigned.
The script is long so I’m not going to run through it line by line, but you can grab a copy here
Manual Option (app deployment)
Let’s start with the script which can be found here
As with my Autopilot script, this uses the Graph module which we need to look for and install first:
#Install MS Graph if not available
if (Get-Module -ListAvailable -Name Microsoft.Graph) {
Write-Host "Microsoft Graph Already Installed"
}
else {
try {
Install-Module -Name Microsoft.Graph -Scope CurrentUser -Repository PSGallery -Force
}
catch [Exception] {
$_.message
exit
}
}
As you can see, it’s using the Current User scope to avoid any issues if you don’t have admin rights
Now we need to import the module
import-module -Name microsoft.graph
And connect to Graph
Select-MgProfile -Name Beta
Connect-MgGraph -Scopes Group.ReadWrite.All, GroupMember.ReadWrite.All, openid, profile, email, offline_access
Now we create the groups, these use the service plan IDs from Azure:
Project: fafd7243-e5c1-4a3a-9e40-495efcb1d3c3
Visio: 663a804f-1c30-4ff0-9915-9db84f0d1cea
So, to create a group of licensed Visio users we need to look for that plan and make sure it’s enabled:
New-MGGroup -DisplayName "Visio-Install" -Description "Dynamic group for Licensed Visio Users" -MailEnabled:$False -MailNickName "visiousers" -SecurityEnabled -GroupTypes "DynamicMembership" -MembershipRule "(user.assignedPlans -any (assignedPlan.servicePlanId -eq ""663a804f-1c30-4ff0-9915-9db84f0d1cea"" -and assignedPlan.capabilityStatus -eq ""Enabled""))" -MembershipRuleProcessingState "On"
For the uninstall group, we’re just looking for anyone who doesn’t have the plan:
New-MGGroup -DisplayName "Visio-Uninstall" -Description "Dynamic group for users without Visio license" -MailEnabled:$False -MailNickName "visiouninstall" -SecurityEnabled -GroupTypes "DynamicMembership" -MembershipRule "(user.assignedPlans -all (assignedPlan.servicePlanId -ne ""663a804f-1c30-4ff0-9915-9db84f0d1cea"" -and assignedPlan.capabilityStatus -ne ""Enabled""))" -MembershipRuleProcessingState "On"
Same for Project with both install and uninstall, just with the new service plan ID:
New-MGGroup -DisplayName "Project-Install" -Description "Dynamic group for Licensed Project Users" -MailEnabled:$False -MailNickName "projectinstall" -SecurityEnabled -GroupTypes "DynamicMembership" -MembershipRule "(user.assignedPlans -any (assignedPlan.servicePlanId -eq ""fafd7243-e5c1-4a3a-9e40-495efcb1d3c3"" -and assignedPlan.capabilityStatus -eq ""Enabled""))" -MembershipRuleProcessingState "On"
$projectuninstall = New-MGGroup -DisplayName "Project-Uninstall" -Description "Dynamic group for users without Project license" -MailEnabled:$False -MailNickName "projectuninstall" -SecurityEnabled -GroupTypes "DynamicMembership" -MembershipRule "(user.assignedPlans -all (assignedPlan.servicePlanId -ne ""fafd7243-e5c1-4a3a-9e40-495efcb1d3c3"" -and assignedPlan.capabilityStatus -ne ""Enabled""))" -MembershipRuleProcessingState "On"
Now onto the app deployment
Both of these need Office closed or the install will fail so I’m going to use the excellent PSADT along with ServiceUI.exe from MDT
The script for the apps are here: Visio | Project
I’ve also included the install and uninstall string, detection method, an icon and a packaged Win32 file
Both are installing the current branch latest version using the XML in the Files folder, feel free to amend for your requirements:
Project:
<Configuration ID="3c5aba9e-67dc-48d2-b1a2-c09cb4f05916">
<Add OfficeClientEdition="64" Channel="Current" ForceUpgrade="TRUE" MigrateArch="TRUE">
<Product ID="ProjectProRetail">
<Language ID="MatchOS" />
</Product>
</Add>
<Property Name="SharedComputerLicensing" Value="0" />
<Property Name="PinIconsToTaskbar" Value="FALSE" />
<Property Name="SCLCacheOverride" Value="0" />
<Property Name="AUTOACTIVATE" Value="0" />
<Property Name="FORCEAPPSHUTDOWN" Value="TRUE" />
<Property Name="DeviceBasedLicensing" Value="0" />
<Updates Enabled="FALSE" />
<RemoveMSI />
<Display Level="None" AcceptEULA="FALSE" />
<Logging Level="Off" />
</Configuration>
Visio:
<Configuration ID="c1230666-d756-491b-b093-2d4d64413b17">
<Add OfficeClientEdition="64" Channel="Current" ForceUpgrade="TRUE">
<Product ID="VisioProRetail">
<Language ID="MatchOS" />
<ExcludeApp ID="Groove" />
<ExcludeApp ID="OneDrive" />
</Product>
</Add>
<Property Name="SharedComputerLicensing" Value="0" />
<Property Name="PinIconsToTaskbar" Value="FALSE" />
<Property Name="SCLCacheOverride" Value="0" />
<Property Name="AUTOACTIVATE" Value="0" />
<Property Name="DeviceBasedLicensing" Value="0" />
<Updates Enabled="TRUE" />
<RemoveMSI />
<Display Level="None" AcceptEULA="FALSE" />
<Logging Level="Off" />
</Configuration>
The uninstallers are also pretty straight forward:
Project:
<Configuration>
<Remove All="FALSE">
<Product ID="ProjectProRetail">
<Language ID="en-us" />
</Product>
</Remove>
<Updates Enabled="TRUE" Channel="Monthly" />
<Display Level="None" AcceptEULA="TRUE" />
<Property Name="AUTOACTIVATE" Value="1" />
<Logging Level="Standard" Path="%temp%" />
</Configuration>
Visio:
<Configuration>
<Remove All="FALSE">
<Product ID="VisioProRetail">
<Language ID="en-us" />
</Product>
</Remove>
<Updates Enabled="TRUE" Channel="Monthly" />
<Display Level="None" AcceptEULA="TRUE" />
<Property Name="AUTOACTIVATE" Value="1" />
<Logging Level="Standard" Path="%temp%" />
</Configuration>
The actual installation is handled by the Deploy-Application.ps1 files in the root
First set some variables (Project)
##*===============================================
##* VARIABLE DECLARATION
##*===============================================
## Variables: Application
[string]$appVendor = 'Microsoft'
[string]$appName = 'Project'
[string]$appVersion = 'Latest'
[string]$appArch = 'x64'
[string]$appLang = 'EN'
[string]$appRevision = '01'
[string]$appScriptVersion = '1.0.0'
[string]$appScriptDate = '01/11/2021'
[string]$appScriptAuthor = 'Andrew Taylor'
##*===============================================
## Variables: Install Titles (Only set here to override defaults set by the toolkit)
[string]$installName = 'Project'
[string]$installTitle = 'Installing MS Project'
Visio
## Variables: Application
[string]$appVendor = 'Microsoft'
[string]$appName = 'Visio'
[string]$appVersion = 'Latest'
[string]$appArch = 'x86'
[string]$appLang = 'EN'
[string]$appRevision = '01'
[string]$appScriptVersion = '1.0.0'
[string]$appScriptDate = '01/11/2021'
[string]$appScriptAuthor = 'Andrew Taylor'
##*===============================================
## Variables: Install Titles (Only set here to override defaults set by the toolkit)
[string]$installName = 'Visio'
[string]$installTitle = 'Installing MS Visio'
Now we make sure the apps are closed
Show-InstallationWelcome -CloseApps 'excel,groove,onenote,outlook,mspub,powerpnt,winword,iexplore,teams,visio,winproj' -CheckDiskSpace -PersistPrompt
Then install
Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/CONFIGURE InstallO365withVisio.xml"
Tell them it’s done
If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'Installation has been completed. You will find Visio in the All Programs section.' -ButtonRightText 'OK' -Icon Information -NoWait }
Or uninstall
Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/CONFIGURE RemoveVisio.xml"
Now we need to deploy to Intune (I’ll just use Project to demonstrate, it’s the same process for Visio)
Grab the intunewin file (or create your own) and add details about the app

Add the install and uninstall commands (included in the source files). Here we are using ServiceUI.exe to allow desktop interaction with popup messages to close apps.

It’s a 64-bit version so make sure to limit the device type, you can pick any Windows version here, I generally just select the last supported version to try and tempt the dinosaurs into upgrading

For detection you are looking for the executable which is either winproj.exe (Project) or visio.exe (Visio)

It shouldn’t have any dependencies so you can skip this section and unless you already have a version to replace, skip Supersedence as well
Now we deploy to our newly created dynamic groups. I normally do a silent uninstall because the users don’t need to know

Now you have a fully automated deployment of Project and Visio, enjoy!
This was a really helpful article. I modified the scripts to work properly with the 4.1 version of the App Deployment Toolkit.
Thanks very much for the guide and references. I’ve linked my version below on Github.
https://github.com/gamegekko/automation
Amazing, thank you 🙂
Thanks Andrew, worked wonderfully and really helped out deploy to our small business!
I’m running the automatic script, it creates the groups, I see the apps in Intune but they are in a “Your app is not ready yet. If app content is uploading, wait for it to finish”.
I ran the script as admin, but didn’t consent to Graph for the whole organization, I got these errors. Do I need to consent Graph for the whole org?
Write-Error: C:\Users\justin\Downloads\install-project-visio.ps1:1440
Line |
1440 | Invoke-UploadWin32Lob -SourceFile “$SourceFile” -DisplayName “Microso …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Aborting with exception: System.Management.Automation.ParameterBindingValidationException: Cannot bind argument
| to parameter ‘Exception’ because it is null. at
| System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception
| exception) at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame)
| at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame) at
| System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
Invoke-MgGraphRequest: C:\Users\justin\Downloads\install-project-visio.ps1:1489
Line |
1489 | Invoke-MgGraphRequest -Uri $uri1 -Method Post -Body $JSON1 -ContentTy …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| POST
| https://graph.microsoft.com/Beta/deviceAppManagement/mobileApps/156eb267-1e76-4219-bcc1-11700557114a%20686224c4-c167-423a-9718-4c284a6f3f7e/assign HTTP/2.0 400 Bad Request Vary: Accept-Encoding Strict-Transport-Security: max-age=31536000 request-id: c10d2bcb-e57b-4b7e-ba64-5033cb11338f client-request-id: 299d33b8-98b0-4cbe-bd4f-a3fd34e298a7 x-ms-ags-diagnostic: {“ServerInfo”:{“DataCenter”:”North Central US”,”Slice”:”E”,”Ring”:”4″,”ScaleUnit”:”001″,”RoleInstance”:”CH01EPF00095FCF”}} Date: Wed, 04 Mar 2026 13:08:04 GMT Content-Type: application/json {“error”:{“code”:”BadRequest”,”message”:”{\r\n \”_version\”: 3,\r\n \”Message\”: \”Invalid app id: ‘156eb267-1e76-4219-bcc1-11700557114a 686224c4-c167-423a-9718-4c284a6f3f7e’. – Operation ID (for customer support): 00000000-0000-0000-0000-000000000000 – Activity ID: 299d33b8-98b0-4cbe-bd4f-a3fd34e298a7 – Url: https://proxy.amsua0102.manage.microsoft.com/AppLifecycle_2602/StatelessAppMetadataFEService/deviceAppManagement/mobileApps('156eb267-1e76-4219-bcc1-11700557114a%20686224c4-c167-423a-9718-4c284a6f3f7e‘)/microsoft.management.services.api.assign?api-version=5025-10-02\”,\r\n \”CustomApiErrorPhrase\”: \”\”,\r\n \”RetryAfter\”: null,\r\n \”ErrorSourceService\”: \”\”,\r\n \”HttpHeaders\”: \”{}\”\r\n}”,”innerError”:{“date”:”2026-03-04T13:08:05″,”request-id”:”c10d2bcb-e57b-4b7e-ba64-5033cb11338f”,”client-request-id”:”299d33b8-98b0-4cbe-bd4f-a3fd34e298a7″}}}
Hi, it looks like the app upload either failed, or didn’t complete in time. Can you try again with consent for the org and if that still fails, let me know if it was Project or Visio and I’ll test it
Thanks
Hi Andrew, thanks for the reply. I deleted the groups it created, and the half uploaded apps in Intune. I restarted my computer, and re-ran the automatic script, it’s still failing and not prompting me to consent for the whole org for Graph, even though I manually disconnected via powershell to reconnect. I’ve updated Graph and also installed beta Graph, very odd. I tried this in a totally different tenant as well on a different computer, and get the same failures. Just wanted to update you.
Hi Andrew, not sure how to get this auto script working, I’ve updated my graph, installed beta graph.
Write-Error: C:\Users\justin\Downloads\install-project-visio.ps1:1345:5
Line |
1345 | Invoke-UploadWin32Lob -SourceFile “$SourceFile” -DisplayName “Mic …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Aborting with exception: System.Management.Automation.ParameterBindingValidationException: Cannot bind argument to parameter ‘Exception’ because it is null. at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext
| funcContext, Exception exception) at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame) at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame) at
| System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
Invoke-MgGraphRequest: C:\Users\justin\Downloads\install-project-visio.ps1:1395:1
Line |
1395 | Invoke-MgGraphRequest -Uri $uri -Method Post -Body $JSON -ContentType …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| POST https://graph.microsoft.com/Beta/deviceAppManagement/mobileApps/e055aad0-02e4-4d84-88d6-9d384abb354e/assign HTTP/2.0 400 Bad Request Vary: Accept-Encoding Strict-Transport-Security: max-age=31536000 request-id: bb7acf95-4dfc-40a8-89ac-556f9c8ff846
| client-request-id: df7852e9-a323-4b18-b940-a2966b5a68a1 x-ms-ags-diagnostic: {“ServerInfo”:{“DataCenter”:”North Central US”,”Slice”:”E”,”Ring”:”4″,”ScaleUnit”:”005″,”RoleInstance”:”CH01EPF0000C16D”}} Date: Wed, 11 Mar 2026 13:52:59 GMT Content-Type:
| application/json {“error”:{“code”:”BadRequest”,”message”:”{\r\n \”_version\”: 3,\r\n \”Message\”: \”Invalid operation: app’s PublishingState is not ‘Published’. – Operation ID (for customer support): 00000000-0000-0000-0000-000000000000 – Activity ID:
| df7852e9-a323-4b18-b940-a2966b5a68a1 – Url:
| https://proxy.amsua0102.manage.microsoft.com/AppLifecycle_2602/StatelessAppMetadataFEService/deviceAppManagement/mobileApps('e055aad0-02e4-4d84-88d6-9d384abb354e‘)/microsoft.management.services.api.assign?api-version=5026-02-07\”,\r\n
| \”CustomApiErrorPhrase\”: \”\”,\r\n \”RetryAfter\”: null,\r\n \”ErrorSourceService\”: \”\”,\r\n \”HttpHeaders\”:
| \”{}\”\r\n}”,”innerError”:{“date”:”2026-03-11T13:53:00″,”request-id”:”bb7acf95-4dfc-40a8-89ac-556f9c8ff846″,”client-request-id”:”df7852e9-a323-4b18-b940-a2966b5a68a1″}}}
I think this is what I’m running into https://dutchscriptingguys.com/why-your-intune-file-commits-fail-in-powershell-7-4/
Yes, it could well be, I know some parts of that don’t like PS7, can you try it in PS5?
That was it 🙂 Thank you Andrew, I was going down the path of using an older Powershell 7 version, 5.1 worked wonders. Thanks for your assist on this.
This all looks amazing!
I was secretly hopeful that with Intune Apps having a “Microsoft apps 365” deployment template – that merely adding to that would be simple.
Maybe a second profile for Visio 2 etc.
But in testing this seems to be far far from the case. So this has been helpful!
Nothing is ever straight forward with Intune, I’ve used this approach for years though and it’s about as hands-off as you can get
Andrew,
I am trying to wrap my own intunewin because I want to update to Monthly Enterprise which requires updating the XML. What should my setup file be if I use the Deploy-Application.exe I get a failure. If I use the Deploy-Application.ps1 we succeeds but doesn’t use any of the interactive PSADT logic that I would like to use but the install succeeds.
Little confused what I should be doing here. Any help would be greatly appreciated!
Either should work, that’s the old version of PSADT, but as long as you are using ServiceUI.exe to interact with the user, it should work whichever you pick
Hi Andrew. I have the groups, i just need to deploy Visio to be installed via Company Portal by the users. it works fine, except it takes sometimes up to 3 hours to install. Same sometimes for uninstall from CP. do you have any idea why it takes so long ?
Thanks
Hi, are there any other processes running in the background? Does it happen with other apps as well?
Hey.
As far as i know it only happens with visio. I changed the display level in the xml file so i can see the progress bar and check what’s happening. Once it started just fine, asked me to close outlook and then got stuck at 2% (setup.exe in task manager was at 0% CPU ). other times there is a message that visio cannot install as there is another installation in progress (but there is none , at least i could not find anything). As for other processes, what i observed is that the click-to-run process just hangs there. as i kept testing and tried it just opens up new click-to-run processes and they all hang there.
Could your Office apps be updating when you close them? I would check for any firewalls etc. as well
Having a slight problem getting the fully automated deployment working
Keeps trying to look for the non preview azureAD module.
Second question will this be updated for graph
Yes, I’ll update it for Graph, not sure how I missed this script when I switched the others
v2.0 online now using Graph modules. Hopefully it all works as expected
Thank you Andrew, appreciate that
Upload-Win32Lob isnt recognized not sure what it was changed to though of if it exists?
What line does it say that is on?
Try the new version I’ve just uploaded
Hi Andrew,
Script seems to have run without an error now apps exists and so do the groups
Looks like it is fixed
Thank you so much
Glad it’s working ok 🙂
Great article – are you deploying MDT and PSADT to devices using Intune first?
Hi, no need to deploy separately, PSADT and service UI are packaged into the intunewin file
Thanks for the reply, that makes sense. One other thing I noticed, the ps1 tells user to close Office applications but not for the new Outlook and Teams.
Would that be as simple as just adding the application name into that script and re-packaging into a new intunewin file?
They are appx packages so won’t need to be closed for the core apps to update
Hey Andrew,
Brilliant tutorial and it works very well, Could you please help me with one thing, I am not sure if it is included or not, but I would like to do a silent install so that the App Deployment Toolkit does not come up on screen when a user installs Visio from the company portal.
Your help would be much appreciated.
Hi,
PSADT supports this parameter:
-DeployMode ‘Silent’
If you add that after the deployapplication.exe it should suppress them
Is there a solution for VLSC licenses? I think I’ve played with it in the past and trying to install Visio standalone after m365 doesn’t work that great
I think the config xml can handle VLSC but I’ve never tried it myself
Forgive me i’m not familiar with Visio and project licensing, why do you leave AUTOACTIVATE to 0 for the installs but 1 for the uninstalls?
I just grabbed those straight from the Office config tool, either setting will probably work
Hi Andrew, amazing article. Thank you!
How would the install/uninstall work given the group is user based and the deployments and machine based?
Hi,
It would follow the user, the install will happen at the machine level, but it will install/uninstall based on the current user
While it’s an atypical deployment option, does this mean that on a machine used by multiple users if one user has project it will install, but if another user does not have project it will uninstall when they log in, leading to the first user re-installing it when they log back in?
Yes, that’s correct. For a multi-user machine, you can either just install it at the device and those without a license just can’t use it, or if the machines are the same model etc. you could use device based filtering to exclude the installation
In my opinion I would just leave the uninstall as a manual option for those users so whoever wanted to uninstall it could. There’s always the scenario that a user just no longer needing it but would still having the license for a period of time as sometimes it could take weeks to get them removed from the licensing assignment due to various factors.
That’s only really an option for large companies who can afford to waste money on unused licenses