Automating Project and Visio installs with Dynamic Groups

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!

43 thoughts on “Automating Project and Visio installs with Dynamic Groups”

  1. 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″}}}

    Reply
    • 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

      Reply
      • 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.

        Reply
      • 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″}}}

        Reply
  2. 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!

    Reply
  3. 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!

    Reply
  4. 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

    Reply
      • 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.

        Reply
  5. 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

    Reply
  6. 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.

    Reply
  7. 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

    Reply
  8. 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?

    Reply
  9. Hi Andrew, amazing article. Thank you!
    How would the install/uninstall work given the group is user based and the deployments and machine based?

    Reply
      • 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?

        Reply
        • 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

          Reply
          • 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.

Leave a Comment