top of page
Search
Writer's pictureMark Webb

Creating a Call Queue PowerShell function for Teams - Part 1

Make 4 become 1 (not an updated Spice Girls song)


This post isn't about a new feature but something I just wanted to have a play with to learn a bit about creating functions in PowerShell and also to look at a way to make it slightly quicker to create a Call Queue in Teams using PowerShell.


At the moment there's not one PowerShell command you can run to create a fully functioning Call Queue in Teams. You need to string a few together to get everything working.


First, you need to create a resource account for the Call Queue using the New-CsOnlineApplicationInstance cmdlet. Once you've done that, if you need to assign a phone number to the Call Queue you then need to run the Set-CsPhoneNumberAssignment cmdlet to assign a phone number to the Resource Account.


Then you need to create the Call Queue using New-CsCallQueue....and just when you think you're done, you then need to associate the Resource Account with the Call Queue using the New-CsOnlineApplicationInstanceAssociation cmdlet.


That's four potential commands to run to set everything up. It's not massively time consuming but you've got to run them all with the necessary parameters and remember certain values to pass between them. So I thought I'd go about creating a PowerShell function where you can pass all the parameters you need to a single function/cmdlet and it'll do all of the above.


The end goal is to run one command and the function will then run all of the commands above as part of the script to create the Resource Account, create the Call Queue, assign a phone number to the Resource Account and then associate the Resource Account with the Call Queue.


This post will detail how we can create a function to achieve that. I then got to thinking how we could take it one step further by linking in some Microsoft Forms, SharePoint lists, Azure Automation and Logic Apps to automate a request process where someone could submit a form with details of what they wanted for their Call Queue and it would fire off an automated script to create it all without an admin needing to do anything.


I'll look at that in part 2 of this series.


Creating the function


Set out the Paramters


The example in this post isn't going to be exhaustive in terms of being able to copy it and create a Call Queue with every single permutation you could think of. You could easily achieve that though by just expanding some structure and building on the parameters set out in it.


This is just showing an example of how you can bring multiple elements together in one function to streamline the process.


So to start with, we define our function name and set out the parameters we're going to require. Where applicable, I set out any validation to make sure you can't pass a value that's outside a properties accepted inputs.


Function New-MWCallQueue {

    Param (
        [Parameter(Mandatory=$true)]
        [String]
        $CallQueueName,
    
        [Parameter(Mandatory=$true)]
        [ValidateRange(0,2700)]
        [Int]
        $TimeoutThreshold,
    
        [Parameter(Mandatory=$true)]
        [ValidateRange(0,200)]
        [Int]
        $OverflowThreshold,
    
        [Parameter(Mandatory=$true)]
        [ValidateSet("Attendant","Serial","RoundRobin","LongestIdle")]
        [String]
        $RoutingMethod,
    
        [Parameter(Mandatory=$true)]
        [boolean]
        $PresenceBasedRouting,
    
        [Parameter(Mandatory=$true)]
        [String[]]
        $Users,
    
        [Parameter(Mandatory=$true)]
        [boolean]
        $DefaultMusicOnHold,
    
        [Parameter(Mandatory=$true)]
        [String]
        $Owner,
    
        [Parameter(Mandatory=$true)]
        [boolean]
        $AllowOptOut,
    
        [Parameter(Mandatory=$true)]
        [boolean]
        $ConferenceMode,
    
        [Parameter(Mandatory=$true)]
        [ValidateRange(15,180)]
        [Int]
        $AgentAlertTime,
    
        [Parameter(Mandatory=$true)]
        [String]
        $WelcomeTextToSpeech
    
    )

I won't go through all of these but the premise is for each parameter/property we need to pass to the function to be able to create the Call Queue/Resource Acccount we set out a parameter, specify it is mandatory so you can't call the function and leave one of them out, define what type of parameter it is i.e. a string or an integer to match the cmdlets requirements and then create the parameter name.


Taking the TimeoutThreshold as an example, we say it's a Parameter and that it is mandatory


[Parameter(Mandatory=$true)]

We then say that the value input has to be in a specific range between 0 and 2700 (to match what the matching property in the New-CsCallQueue cmdlet will accept)


[ValidateRange(0,2700)]

The next line defines that it is an integer


[Int]

Then finally we say what the property name in our function will be called


$TimeoutThreshold


The same principle applies to all of the parameters. They're all either strings or integers apart from the Users parameter which is an Array because we might/probably want to specify a number of users to be agents in the Call Queue.


To specify a parameter is an Array rather than a String we add a set of closed square brackets to the definition. So to specify a parameter is a String we say String[]. To define an Array we say String[[]]


Define some Variables and Connect to Teams and Microsoft Graph


The next part of the function creates a couple of variables based on the inputs and then connects to the Teams and Microsoft Graph PowerShell modules in readiness to execute the required cmdlets.


$CallQueueNameNS = $CallQueueName -replace '\s',''
    
$UPN = "CQ-" + $CallQueueNameNS + "@mwdev20.co.uk"
    
Write-Host "Connecting to Microsoft Teams and Graph, enter your credentials when prompted"
    
Connect-MicrosoftTeams
    
Connect-MgGraph -Scopes Directory.ReadWrite.All, Group.ReadWrite.All, User.Read.All

The first variable ($CallQueueNameNS) takes the Call Queue name that has been passed to the function and remove any spaces. It's likely the name the user inputs will have a space in it somewhere and we need to remove those to form part of the UPN we set next for the Resource Account. If we try and create a UPN with spaces in it, the command will fail so this ensures we have a name with no spaces for that element.


The next variable constructs the UPN for the Resource Account the function will create shortly.

$UPN = "CQ-" + $CallQueueNameNS + "@mwdev20.co.uk"

We prefix the UPN with "CQ-" to create a standardised naming convention for the Resource Accounts. This has a couple of benefits, firstly it means anyone searching for a Call Queue resource account just needs to look for an account starting with "CQ-" and they know it's for a Call Queue.


Secondly, it means we can create a dynamic AAD group based on the UPN starting with "CQ-" and then assign the Teams Shared Resource licence to that group to take care of the licence requirements for the Resource Accounts. We won't need to do that as part of the script or manually elsewhere as it'll get automatically assigned based on the UPN.


Next, the function connects to the Teams and Microsoft Graph PowerShell modules. For the Graph module we specify the scopes we need to make the changes the function requires.


Connect-MicrosoftTeams
    
Connect-MgGraph -Scopes Directory.ReadWrite.All, Group.ReadWrite.All, User.Read.All

The user will be prompted to enter their credentials to connect. This assumes the credentials they use will have the necessary permissions required. For the Teams elements, the minimum admin role required is Teams Communications Administrator.


Construct Group properties


The next part isn't strictly required to create the Call Queue but I've added it in as I think it's a "nice to have" feature. We could just take the input from the user for the users/agents for the Call Queue and pass that array to the New-CsCallQueue cmdlet. However, I think by creating an M365 group and adding those users to it we give ourselves some options later on. For example, let's say in the future or as part of an improvement to the function we want to configure shared voicemail for the Call Queue.


Instead of having to reconstruct the whole function or create a new group to forward the voicemail to we can create a group as part of this function and then use it in this instance to set the agents for the Call Queue and then if down the line voicemail is needed, we already have a group created with all the members and we just need to set that as the voicemail target.


So the next part of the function constructs the properties of the M365 group ready for creation. We're using the Microsoft Graph PowerShell commands for this rather than the Exchange Online ones (the Exchange Online equivalent is New-UnifiedGroup). The main reason I chose Graph over Exchange is because for Part 2 of this series when we look at automating this process via Azure, the New-UnifiedGroup command isn't supported when using a managed identity such as via an automation account so we'd have to use Graph anyway then. Link here to Microsoft docs explaining the lack of support: https://learn.microsoft.com/en-us/powershell/exchange/connect-exo-powershell-managed-identity?view=exchange-ps


The code in the function to create the group properties is this:


$GroupMembers = ForEach ($user in $users) {
    
    "https://graph.microsoft.com/v1.0/users/" + $user
    
    }
    
    $Owners = "https://graph.microsoft.com/v1.0/users/" + $owner
    
    $params = @{
        description = $CallQueueName
        displayName = $CallQueueName
        groupTypes = @(
            "Unified"
        )
        mailEnabled = $true
        mailNickname = "$CallQueueNameNS"
        securityEnabled = $false
        Visibility = "Private"
        "Members@odata.bind" = @($Groupmembers)
        "Owners@odata.bind" = @($Owners)
    }
    
    

Initially we need to create the properly constructed list of members of the group. We need to pass the name of each member in a specific format "https://graph.microsoft.com/v1.0/users/username" to the command. So the first element here is running a ForEach loop for each username passed to the function and prefixing the username with "https://graph.microsoft.com/v1.0/users/"


$GroupMembers = ForEach ($user in $users) {
    
    "https://graph.microsoft.com/v1.0/users/" + $user
    
    }

The next line does the same (prefixing the username) for the Owner input just not as a ForEach loop as the input for this is a string/single username not a list/array of users. If you did want to have multiple owners then just use the same method as the members to iterate through the list.


The last part specifies the parameters for the group. There are a lot of settings you can configure for the group, the ones in this example are just a few to show how it's constructed, you can add as many as needed to meet your requirements.

$params = @{
        description = $CallQueueName
        displayName = $CallQueueName
        groupTypes = @(
            "Unified"
        )
        mailEnabled = $true
        mailNickname = "$CallQueueNameNS"
        securityEnabled = $false
        Visibility = "Private"
        "Members@odata.bind" = @($Groupmembers)
        "Owners@odata.bind" = @($Owners)
    }

Where there's a variable already defined that can be provided it's added here, such as the display name or nickname for example.


Creating the resources


Now we get to creating the Group, Resource Account and Call Queue. The function starts off by writing some output to the command line so the user can see which step of the process is being carried out and then creates the M365 group using the parameters set out above. We add in a short pause to allow replication time for later when we want to query the newly creatred group.


Write-Host "Creating new M365 group to store members for Call Queue" -ForegroundColor Green
    
New-MgGroup -BodyParameter $params
    
Write-Host "Wait 10 seconds for replication" -ForegroundColor Green
    
Start-Sleep -Seconds 10

Next the function creates the Resource Account using the provided inputs and then creates a variable to store details of the newly created M365 group.


Write-Host "Creating new resource account for Call Queue" -ForegroundColor Green
    
New-CsOnlineApplicationInstance -UserPrincipalName $UPN -ApplicationId "11cd3e2e-fccb-42ad-ad00-878b93575e07" -DisplayName $CallQueueName
    
Write-Host "Created Resource account, waiting 30 seconds for replication" -ForeGroundColor Green
    
$GroupID = Get-MgGroup -Filter "DisplayName eq '$CallQueueName'"
        
Start-Sleep -Seconds 30

All of the property values here are variables we've either taken from the user input when calling the function or ones created within the function based on the user input. The only one that is static is the ApplicationId property which will always be the same as this is defining that we are creating a Call Queue App Instance.


Again, we add in a 30 second pause to allow the Resource Account creation to replicate for the commands later on.


The Call Queue itself gets created next, using most of the inputs initially provided by the user when calling the function.


Write-Host "Creating Call Queue" -ForegroundColor Green
    
New-CsCallQueue -Name $CallQueueName -OverflowThreshold $OverflowThreshold -TimeoutThreshold $TimeoutThreshold -RoutingMethod $RoutingMethod -PresenceBasedRouting $PresenceBasedRouting -UseDefaultMusicOnHold $DefaultMusicOnHold -DistributionLists $GroupID.ID -AgentAlertTime $AgentAlertTime -AllowOptOut $AllowOptOut -WelcomeTextToSpeechPrompt $WelcomeTextToSpeech -ConferenceMode $ConferenceMode

The only property that isn't one specified by the user is the -DistributionLists property. This is where we provide the ID of the M365 Group we created as the list of agents for the Call Queue. If we didn't want a Group to contain the agents and just wanted to add the agents indivdually we'd replace the -DistributionLists property with -Users and then provide a list of the users Object ID's.


Now we have a Resource Account and a Call Queue, before we finish up and associate them let's add a phone number to the Call Queue so people can call into the Queue from an outside line.


This example assumes you're able to query phone numbers using the Get-CsPhoneNumberAssignment to see which numbers are unallocated. If your phone numbers aren't provided/ported into Microsoft/or searchable via this cmdlet you'll need another way to query them. For this purpose though we'll assume you can.



Write-Host "Adding random unassigned phone number to Call Queue. Script will pause for 2 minutes before executing assignment to allow resource account licence allocation" -ForegroundColor Green
    
Start-Sleep -Seconds 120
    
$SpareNumbers = Get-CsPhoneNumberAssignment | Where {$_.Capability -eq 'VoiceApplicationAssignment' -and $_.PstnAssignmentStatus -eq 'Unassigned'}
    
$CQNumber = $SpareNumbers | Get-Random
    
Set-CsPhoneNumberAssignment -Identity $UPN -PhoneNumber $CQNumber.TelephoneNumber -PhoneNumberType CallingPlan
    
Write-Host "Phone number assigned. Moving on to Associating resource account to Call Queue" -ForegroundColor Green

The code is starting off from the last command and adding in a 2 minute pause to the script. This is to allow the Resource Account we created a little earlier time to pick up its licence assignment. In my dev tenant I'm assigning the Shared Resource account licence the Call Queue needs via group based assignment. I have a dynamic AAD group setup that will add members if the UPN of an account starts with "CQ-". That group then assigns the licence to any members.


The licence assignment takes a minute or two post account creation so we need to pause the script to allow this to take place. If we tried to assign the phone number before the licence has been assigned the command would fail.


Once the script has paused and resumes, it then queries for any phone numbers which are both unallocated and of the Voice Application type and then stores them in a variable.

$SpareNumbers = Get-CsPhoneNumberAssignment | Where {$_.Capability -eq 'VoiceApplicationAssignment' -and $_.PstnAssignmentStatus -eq 'Unassigned'}

It then picks a random number from those returned

$CQNumber = $SpareNumbers | Get-Random

and then runs the command to assign that to the Resource Account

Set-CsPhoneNumberAssignment -Identity $UPN -PhoneNumber $CQNumber.TelephoneNumber -PhoneNumberType CallingPlan

If you were using Direct Routing or Operator Connect you'd change the -PhoneNumberType property to suit.


Associating the resources

The last action is to associate the Resouce Account and Call Queue to finalise the configuration.

$CallQueueID = (Get-CSCallQueue -NameFilter $CallQueueName).Identity
    
$ResourceID = (Get-CsOnlineApplicationInstance -Identity $UPN).objectId
    
New-CsOnlineApplicationInstanceAssociation -ConfigurationId $CallQueueID -Identities $ResourceID -ConfigurationType CallQueue

The script creates two variables with the ID's of the Call Queue and Resource Account and then passes to the command to associate them together.


Done! The Call Queue is setup and ready to go.


Running the Function


All that needs doing now is running/importing the function and we can then call it in PowerShell, add the paramters and it will do the rest. Here's the full code all together:



Function New-MWCallQueue {

    Param (
        [Parameter(Mandatory=$true)]
        [String]
        $CallQueueName,
    
        [Parameter(Mandatory=$true)]
        [ValidateRange(0,2700)]
        [Int]
        $TimeoutThreshold,
    
        [Parameter(Mandatory=$true)]
        [ValidateRange(0,200)]
        [Int]
        $OverflowThreshold,
    
        [Parameter(Mandatory=$true)]
        [ValidateSet("Attendant","Serial","RoundRobin","LongestIdle")]
        [String]
        $RoutingMethod,
    
        [Parameter(Mandatory=$true)]
        [boolean]
        $PresenceBasedRouting,
    
        [Parameter(Mandatory=$true)]
        [String[]]
        $Users,
    
        [Parameter(Mandatory=$true)]
        [boolean]
        $DefaultMusicOnHold,
    
        [Parameter(Mandatory=$true)]
        [String]
        $Owner,
    
        [Parameter(Mandatory=$true)]
        [boolean]
        $AllowOptOut,
    
        [Parameter(Mandatory=$true)]
        [boolean]
        $ConferenceMode=$True,
    
        [Parameter(Mandatory=$true)]
        [ValidateRange(15,180)]
        [Int]
        $AgentAlertTime,
    
        [Parameter(Mandatory=$true)]
        [String]
        $WelcomeTextToSpeech
    
    )
    
    $CallQueueNameNS = $CallQueueName -replace '\s',''
    
    $UPN = "CQ-" + $CallQueueNameNS + "@mwdev20.co.uk"
    
    Write-Host "Connecting to Microsoft Teams and Graph, enter your credentials when prompted"
    
    Connect-MicrosoftTeams
    
    Connect-MgGraph -Scopes Directory.ReadWrite.All, Group.ReadWrite.All, User.Read.All
    
    $GroupMembers = ForEach ($user in $users) {
    
    "https://graph.microsoft.com/v1.0/users/" + $user
    
    }
    
    $Owners = "https://graph.microsoft.com/v1.0/users/" + $owner
    
    $params = @{
        description = $CallQueueName
        displayName = $CallQueueName
        groupTypes = @(
            "Unified"
        )
        mailEnabled = $true
        mailNickname = "$CallQueueNameNS"
        securityEnabled = $false
        Visibility = "Private"
        "Members@odata.bind" = @($Groupmembers)
        "Owners@odata.bind" = @($Owners)
    }
    
    
    Write-Host "Creating new M365 group to store members for Call Queue" -ForegroundColor Green
    
    New-MgGroup -BodyParameter $params
    
    Write-Host "Wait 10 seconds for replication" -ForegroundColor Green
    
    Start-Sleep -Seconds 10
    
    Write-Host "Creating new resource account for Call Queue" -ForegroundColor Green
    
    New-CsOnlineApplicationInstance -UserPrincipalName $UPN -ApplicationId "11cd3e2e-fccb-42ad-ad00-878b93575e07" -DisplayName $CallQueueName
    
    Write-Host "Created Resource account, waiting 30 seconds for replication" -ForeGroundColor Green
    
    $GroupID = Get-MgGroup -Filter "DisplayName eq '$CallQueueName'"
        
    Start-Sleep -Seconds 30
    
    Write-Host "Creating Call Queue" -ForegroundColor Green
    
    New-CsCallQueue -Name $CallQueueName -OverflowThreshold $OverflowThreshold -TimeoutThreshold $TimeoutThreshold -RoutingMethod $RoutingMethod -PresenceBasedRouting $PresenceBasedRouting -UseDefaultMusicOnHold $DefaultMusicOnHold -DistributionLists $GroupID.ID -AgentAlertTime $AgentAlertTime -AllowOptOut $AllowOptOut -WelcomeTextToSpeechPrompt $WelcomeTextToSpeech -ConferenceMode $ConferenceMode
    
    Write-Host "Adding random unassigned phone number to Call Queue. Script will pause for 2 minutes before executing assignment to allow resource account licence allocation" -ForegroundColor Green
    
    Start-Sleep -Seconds 120
    
    $SpareNumbers = Get-CsPhoneNumberAssignment | Where {$_.Capability -eq 'VoiceApplicationAssignment' -and $_.PstnAssignmentStatus -eq 'Unassigned'}
    
    $CQNumber = $SpareNumbers | Get-Random
    
    Set-CsPhoneNumberAssignment -Identity $UPN -PhoneNumber $CQNumber.TelephoneNumber -PhoneNumberType CallingPlan
    
    Write-Host "Phone number assigned. Moving on to Associating resource account to Call Queue" -ForegroundColor Green
    
    $CallQueueID = (Get-CSCallQueue -NameFilter $CallQueueName).Identity
    
    $ResourceID = (Get-CsOnlineApplicationInstance -Identity $UPN).objectId
    
    New-CsOnlineApplicationInstanceAssociation -ConfigurationId $CallQueueID -Identities $ResourceID -ConfigurationType CallQueue

We run the command, example below:



New-MWCallQueue -CallQueueName "Call Queue Example" -TimeoutThreshold 30 -OverflowThreshold 30 -RoutingMethod RoundRobin -PresenceBasedRouting $true -Users "mark.webb@mwdev20.co.uk","adelev@mwdev20.co.uk" -DefaultMusicOnHold $true -Owner "mark.webb@mwdev20.co.uk" -AllowOptOut $true -ConferenceMode $true -AgentAlertTime 45 -WelcomeTextToSpeech "example welcome message"



and to confirm everything is setup, we can see the M365 group has been created and the members we provided added




Then in the Teams Admin Centre, we can see the Call Queue with the group added as agents and with the settings as we provided (couple of screenshots below)




Summary


I think this type of function would be useful to reduce admin overheard when creating Call Queues. Rather than having to run a number of commands, admins can just run one cmdlet, provide the inputs and get a Call Queue quicker.


The next step to take this further would be to try and automate the process entirely without an admin needing to do anything. My thoughts are looking at something like an MS Form which asks a user to provide a set of inputs. The form would then write to a SharePoint list. A Logic App would then check that list for a specific trigger and when met fire off an Automation Runbook which runs the same function/commands discussed here, taking the SharePoint list values and creating the Call Queue. It could then send an email to the user who subitted the form and let them know the Call Queue has been created.


I'll write that up in another post!






884 views0 comments

Comments


Post: Blog2_Post
bottom of page