Author: Tony Redmond
Maester Framework Continues to Prosper
New Maester Capabilities Added Recently
The Maester project is a PowerShell-based “test automation framework” to check tenant configurations to highlight potential issues for administrators to deal with. When I first covered the Maester project in April 2024, the initiative seemed like an interesting example of how the technical community can come together to build something of obvious value to Microsoft 365 and Entra ID administrators. By October 2024, Maester had added the ability for tenants to add custom tests to extend coverage to basically anywhere that the Microsoft Graph API could reach. In December, the developers published V1.0 of the Maester PowerShell module.
A glance at the latest Maester documentation shows just how much work has been put into its development. I was especially taken by the methods enabled to monitor Microsoft 365 tenants using Azure DevOps Pipeline, GitHub Actions, Azure Automation, and Azure Container App Jobs, including the ability to notify administrators through email or messages posted to Teams or Slack channels. There’s lots of value to explore here.
Running Tests Against User Accounts
The Maester documentation has examples of writing custom tests. If you want more, Clayton Tyger has created a GitHub repository for custom tests. Most of the current tests cover missing properties for Entra ID accounts, like phone numbers, city, department, hire date, employee identifier, and so on. Checking for missing properties isn’t difficult and given the importance of fully-populated accounts for components like the Microsoft 365 user profile card, it’s a good thing to do.
Venturing into tests for user account properties introduces a level of complexity over many of the other Maester tests. Often, a standard test checks for the presence of a setting which is either enabled or disabled, like who are allowed to create guest accounts in a tenant. Many conditional access policy settings are reviewed in tests to ensure that a tenant is well protected, and so on.
These kinds of tests can be completed quickly. Processing tests fast is important when Maester might run 120 or more tests to check a tenant configuration. You don’t want to get bogged down with waiting for details of 20,000 user accounts to be fetched for checking.
Maester uses a function called Invoke-MtGraphRequest to fetch data from Graph resources. My assumption is that the function is a developed version of the standard Invoke-MgGraphRequest cmdlet from the Microsoft Graph PowerShell SDK that adds functionality like automatic pagination and support for consistency headers for advanced queries. As such, Maester tests have no problem fetching large quantities of user objects to check, if you have time to wait.
Identifying Human Accounts
But then we get to the really difficult problem: how to identify “real” user accounts that should have values in all their properties? Finding all Entra ID member accounts isn’t a good way to proceed because Entra ID member accounts are created for room mailboxes, shared mailboxes, and other purposes. In addition, Entra ID creates member accounts for accounts synchronized from other tenants in a multi-tenant organization (MTO). Failing a Maester test because the account used by a shared mailbox doesn’t have its City property populated is probably not a valuable outcome.
The normal approach is to apply a filter to find user accounts with licenses on the basis that non-human accounts probably don’t have licenses. The only problem is that the accounts used for some shared mailboxes are licensed to allow the mailboxes to have archives or a higher storage quota. A variation on the theme is to filter user accounts with a specific service plan that isn’t usually assigned to non-human accounts. Finding such a service plan becomes the issue here. The service plan can’t be an Exchange plan, yet it must be assigned to all user accounts. The Teams service plan might be a possibility.
Another solution is to use one of the custom properties available in Entra ID to mark accounts. This approach allows a precise filter to find the set of Entra ID accounts used by humans at the expense of the overhead needed to mark accounts by updating the selected custom property.
The Alternative
An alternative is to use the Get-User cmdlet from the Exchange Online management module to fetch Entra ID accounts with user mailboxes. This approach works if everyone in the organization has a mailbox and it’s easy to check the accounts for missing properties (Figure 1).

Fetching a bunch of accounts to check their properties won’t be fast in large tenants, so this is a good example of processing best left to periodic Azure Automation jobs rather than the kind of on-demand test like those used by Maester. Functionality that works splendidly when processing just a few objects often struggles to cope when asked to do the same thing for thousands.
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
Microsoft Introduces People Administrator Role
People Administrator is the 116th Entra ID Role
Message center notification MC992218 (30 January 2025) announces the arrival of the new People administrator role for Entra ID. There’s nothing remarkable about Microsoft creating a new Entra ID roles because People administrator is the 116th role.
Not every role is used inside every Microsoft 365 tenant. Right now, my tenant uses 36 roles. In fact, the way that things work is that Microsoft defines roles as sets of permissions to allow accounts or apps to perform specific actions. The roles are published as templates which are common across Entra ID and then become “real” roles when first assigned to a holder.
To see the full set of roles, run the Get-MgDirectoryRoleTemplate cmdlet, while to see the set used in a tenant, run Get-MgDirectoryRole. Microsoft documentation also lists the available roles.
New Role to Manage People Settings
People administrator sounds as if it’s like User administrator. In a way, that’s true, but only if equate people with users and assume that the two roles do the same thing, which they don’t. One way of comparing the new roles is that User administrator is all about maintaining user accounts and their attributes whereas People administrator assigns “permissions for managing people-related settings and profile photos without needing the high privileges of Global admin or User admin roles.” In other words, the new role is the implementation of the principle of least privilege to manage settings for the user profile card.
People settings have a very specific meaning within Microsoft 365 and are closely related to the profile card. Along with a bunch of information extracted from different parts of Microsoft 365, you’ll find the user’s photo, pronouns, pronunciation, and custom profiles set by the tenant.
What Assignees with the People Administrator Role Can Do
Holders of the People administrator role will be able to:
- Upload new photos on behalf of users. This capability is controlled by the new user photo update policy. People administrator joins the set of default Entra ID roles allowed to update photos.
- Enable personal pronouns for display on the profile card (Figure 1)
- Enable pronunciation recordings for the profile card.
- Define custom properties for display on the profile card. For instance, many tenants use custom properties to reveal organizational information like cost center designations.

Apart from photos, people administrators cannot update the values that appear in the profile card. Other roles, like User administrator, are required to update settings like change a phone number or a surname.
Processes such as those that update user photos from central (often HR) systems can use the new role instead of a higher-permissioned role like User administrator. This is likely the most important point to review and amend. The other functionality enabled by people administrator are, for now at least, one-off operations. After all, tenants don’t usually customize the properties shown on the people card every week or decide to pronouns off or on periodically.
The interesting aspect of this development is that Microsoft is obviously dedicating resources to building out capabilities around what they call “people-related tasks.” Giving users the ability to record their personal name pronunciation was the most recent example before now. I don’t know what else Microsoft has up their sleeves in this respect, but it’s certainly an interesting area to watch.
Check Your Role Assignments
The advent of the People administrator role is a reminder that some active role assignments might be for elevated roles that aren’t absolutely necessary. It’s probably a good idea to review the set of active and eligible role assignments to decide which remain justified and valid and if any assignments are no longer necessary or require adjustment. Any assignment that’s been in place for a year or more deserves a check. Leaving things alone is a recipe for permission inflation and that’s a horrible thing.
So much change, all the time. It’s a challenge to stay abreast of all the updates Microsoft makes across the Microsoft 365 ecosystem. Subscribe to the Office 365 for IT Pros eBook to receive monthly insights into what happens, why it happens, and what new features and capabilities mean for your tenant.
Interpreting SignIn Audit Records for Service Principals
Service Principal SignIn Audit Records Available for 30 Days
In August 2022, I wrote about the experience of developing and using Azure Automation runbooks. Move forward to today and one of the topics discussed in that article was raised again when I was asked if tenant sign-in logs capture details of access to enterprise apps from inside and outside the organization.
My response was “of course” because Entra ID captures all sign-ins for a tenant, including those for enterprise apps, or rather, the service principals that are the instantiation of enterprise apps within a tenant. Tenants keep sign-in records in audit logs for 30 days and those logs are available through the Entra Audit Logs Graph API, specifically for the signIn resource type and List SignIns API. The Microsoft Graph PowerShell SDK implements the List Signins API with the Get-MgAuditLogSignin cmdlet.
Beta API Supports Filtering SignIn Audit Records by Event Type
The ability to filter sign-in audit records by the type is only available through the beta API. This was also true in August 2022 and it’s a little odd that Microsoft hasn’t upgraded the V1.0 API to support filtering to find sign in records for non-interactive access, managed identities, or service principals. In any case, to filter by signInEventTypes, you need to access the beta endpoint or use the Get-MgBetaAuditLogSignIn cmdlet from the Microsoft Graph PowerShell SDK. For example, this command finds the last 5,000 sign-in audit logs generated for service principals:
[array]$AuditRecords = Get-MgBetaAuditLogSignIn -Filter "(signInEventTypes/any(t:t eq 'servicePrincipal'))" -Top 5000 -Sort "createdDateTime DESC"
Reviewing Service Principal SignIn Audit Records
Once the audit log records are extracted, the task is to interpret the Service Principal signins. Things to look for include:
- Unexpected service principals. Attackers often exploit apps in their attempts to compromise tenants, so the appearance of an unexpected service principal is always worth investigation.
- Access to service principals for enterprise apps coming from outside the organization.
- Unexpected access to registered apps from both inside and outside the organization.
- Use of client secrets (app secrets) to authenticate. This is undesirable unless the app is being tested and isn’t yet in production. Any app that’s in production should use a more secure authentication method like an X.509 certificate.
To help answer these questions, I wrote a script (available from the Office 365 for IT Pros GitHub repository) to parse audit records. The output of the script is an Excel worksheet (or CSV file if the ImportExcel module is not installed on the workstation). Figure 1 shows some sample data from my tenant.

Reviewing the data, I found:
- Adobe still uses client secrets to access the Adobe Acrobat enterprise app.
- Some people still try to use old authentication details for apps that were inadvertently revealed in articles. I don’t consider this to be evidence of anything other than people running code that they’ve found to see what happens, but it does demonstrate how authentication information can be used. The audit records show that people in Warsaw, Frankfurt, and Bengaluru tried to access apps over the last 30 days only to find that the published app secret had either expired or been replaced.
- Running declarative Copilot agents created using Copilot Studio generates a service principal for an enterprise app. The one in my tenant is named 383b6826-fc95-4359-bef6-27680c152c33 (Power Virtual Agents). I assume that the app is used to enable single sign on for agents, but I do not know if the same app is used in all tenants. The app is assigned the Cloud Application Administrator and Reports Reader roles but has no other permissions. The IP addresses used by the agents recorded in the audit records are all owned by Microsoft, indicating that the processing occurs within their datacenters (as you might expect). This is an example of a service principal that appears within a tenant without any notice.
The Worth of Service Principal SignIn Audit Reviews
I’m always relieved to answer a question. In this case, the exercise to prove how Entra ID audit log sign-in records capture information about internal and external access via service principals revealed some interesting information. It just goes to demonstrate that reviewing audit data is something that tenant administrators should do regularly.
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
Entra ID Introduces New Graph Permissions for User Accounts
New Graph Permissions for User Accounts Enable Granular Management
In January 2024, Microsoft introduced the User.ReadBasic.All Graph permission. The development was flagged in message center post MC704030. The new permission was important in terms of restricting access to user account properties when that information is not absolutely required.
Now without fanfare or even another message center notification, a set of new Graph permissions have appeared for the user resource type (user accounts). I came upon the new permissions when assigning permissions to apps in the Entra admin center (Figure 1).

A slightly different set of delegated permissions are available for assignment. The User.Read and User.Write permissions deal with updates to the profile (account settings) for the signed-in user. Remember, application permissions apply to all user accounts in a tenant while delegated permissions are used in interactive Microsoft Graph PowerShell SDK sessions.
The set of permissions include ones introduced earlier to help with granular management, such as User.RevokeSessions.All (revoke all sessions for a user account).
The New Granular Graph Permissions for User Accounts
According to the Graph change log, Microsoft added or updated some permissions for the user resource on December 23, 2024. These permissions are candidates for assignment to apps used by help desk personnel who need to maintain user accounts. The updated permissions
- User.EnableDisableAccount.All allows a user’s account to be enabled or disabled (sets the accountEnabled property for the account). This permission was added in February 2023. The latest update removes the need to use the Directory.AccessUserAs.All permission (allows the same directory access as the signed-in user) to read and update the accountEnabled property. The least privileged combination for delegated access to enable or disable accounts is now this permission with User.Read.All.
The new Graph permissions are:
- User-Mail.ReadWrite.All allows the management of the otherMails property for a user account. The property is used to hold one or more alternate mail addresses that is mandatory when enabling MFA for administrator roles. The alternative mail address is also used for self-service password reset.
- User-PasswordProfile.ReadWrite.All supports the management of password-related details for a user account, such as the password and whether the user must change the password the next time it’s used. If using delegated permissions, an additional administrative role is usually required to update password information, so make sure that an appropriate role is assigned to the help desk (using Privileged Identity Management for on-demand temporary assignments).
- User-Phone.ReadWrite.All allows updates to the businessPhones and mobilePhone properties of a user account. If used with delegated permissions, you’ll also need the User.Read.All permission.
The change log also notes the December 23, 2024 addition of the User.DeleteRestore.All permission to control the ability to delete a user account, restore a soft-deleted user account from the recycle bin, and remove a soft-deleted user account permanently. This permission is used in examples in the Automating Microsoft 365 with PowerShell eBook, so I’ve obviously come across it in the past.
Using the New Graph Permissions for User Accounts
To demonstrate the use of the new permissions, let’s consider the situation where you don’t want help desk personnel using interactive Microsoft Graph PowerShell SDK sessions to work with user data because of the way that the SDK accrues permissions over time. The solution is to create a new app and assign the app the necessary permissions to allow the agents to do their job. Then agents can sign into the Graph with the app to work in app-only mode and use application permissions.
Here we sign into the Graph using an app, authenticating with a certificate thumbprint loaded into the app. The only permission available is User.Read.All to allow agents to see details of all user accounts in the tenant. However, they cannot update any property of a user account.
Connect-MgGraph -AppId $AppId -TenantId $TenantId -CertificateThumbprint $Thumbprint -NoWelcome Get-MgContext ClientId : aeeb6b93-5d43-409c-8548-674c931b7888 TenantId : 22e90715-3da6-4a78-9ec6-b3282389492b Scopes : {User.Read.All} AuthType : AppOnly TokenCredentialType : ClientCertificate CertificateThumbprint : 32C9529B1FFD08BCD483A5D98807E47A472C5318
After assigning the User-Phone.ReadWrite.All permission, an agent can update the phone numbers for any account.
Update-MgUser -UserId 'aa345971-b991-46cf-b1d7-b0d80d0d9245' -MobilePhone '+1 416 174 0012' -BusinessPhones '+1 215 145 1452' Get-MgUser -UserId 'aa345971-b991-46cf-b1d7-b0d80d0d9245' | Format-Table Id, MobilePhone, BusinessPhones Id MobilePhone BusinessPhones -- ----------- -------------- aa345971-b991-46cf-b1d7-b0d80d0d9245 +1 416 174 0012 {+1 215 145 1452}
But attempts to update another property of the user account fail:
Update-MgUser -UserId 'aa345971-b991-46cf-b1d7-b0d80d0d9245' -OtherMails 'Random@contoso.com' Update-MgUser_UpdateExpanded: Insufficient privileges to complete the operation.
If consent is now granted for the User-Mail.ReadWrite.All permission, the operation succeeds.
Let’s say that an agent needs to change the password for a user account. They build a password profile and run Update-MgUser again:
$NewPasswordProfile = @{} $NewPasswordProfile.Add("Password", "RandomPasswordForAccount!") $NewPasswordProfile.Add("ForceChangePasswordNextSignIn", $true) Update-MgUser -UserId 'aa345971-b991-46cf-b1d7-b0d80d0d9245' -PasswordProfile $NewPasswordProfile Update-MgUser_UpdateExpanded: Insufficient privileges to complete the operation.
Once the app has consent for the User-PasswordProfile.ReadWrite.All permission, the update succeeds. The need for an additional administrative role to update an account holding specific roles doesn’t apply because the interactive session uses app-only mode.
No Need to Upgrade Code
There’s no need to change existing scripts or runbooks to use the new Graph permissions for user accounts. If everything works, leave it as is unless you want to ensure that code runs with the lowest possible level of permissions. Put it on the list to consider!
So much change, all the time. It’s a challenge to stay abreast of all the updates Microsoft makes across the Microsoft 365 ecosystem. Subscribe to the Office 365 for IT Pros eBook to receive monthly insights into what happens, why it happens, and what new features and capabilities mean for your tenant.
Monthly Update #116 for Office 365 for IT Pros
February 2025 Update for Office 365 for IT Pros (2025 Edition)
The Office 365 for IT Pros writing team is delighted to announce that monthly update #116 is now available. This is the sixth monthly update for Office 365 for IT Pros (2025 edition). Subscribers can download the updated files from their Gumroad.com account or by using the View content link in the receipt emailed after taking out a subscription (Figure 1). That link always downloads the latest book files.

Our change log lists the details of the changes made in update #116 and the other monthly updates. For more information about fetching updates, see our FAQ.
Update #8 for Automating Microsoft 365 with PowerShell
As previously announced, we released update #8 for the Automating Microsoft 365 with PowerShell eBook on January 27, 2025. Office 365 for IT Pros subscribers can download the updated PDF and EPUB files for the PowerShell book using the same link as they use to fetch the other book updates. Regretfully, we can’t update the print (paperback) version of Automating Microsoft 365 with PowerShell that’s sold on an on-demand basis through Amazon. It’s kind of hard to take back print copies, remove all the updated pages, and paste new content into the book.
Dealing with Stamped PDFs
Some of our subscribers have reported problems downloading their copies of the PDF file from Gumroad.com. The symptom is that although the file is visible to them, the button to download the file is greyed out. This can happen for the Office 365 for IT Pros eBook or the Automating Microsoft 365 with PowerShell book, or both.
Each PDF is stamped with the subscriber’s email address. This is done automatically by a background process run on Gumroad’s servers. We are a special case because we issue monthly updates, and each update requires Gumroad to restamp the PDFs. The problem appears to be caused by the background process failing to stamp copies for some subscribers, and because a stamped copy is unavailable, the download button is disabled in Gumroad’s UI.
We have reported the issue several times to Gumroad. Their head of development assures us that a permanent fix has been found and is being deployed. However, we know of at least one instance where a subscriber was unable to download the PDFs for update #116. If you encounter the problem, please send email to support AT Gumroad,com to report that you need help to download your PDF. In the message, be clear that the issue is with the stamped PDF being unavailable for download. The Gumroad support team is pretty responsive and will help you to get the files.
Agents Simpler to Create than Spreadsheets
Lots of change continues across the Microsoft 365 ecosystem. I was taken by Satya Nadella’s assertion that Microsoft wants to make agents as easy to build as creating an Excel spreadsheet (remarks after Microsoft’s FY25 Q2 results). On the one hand, being able to create intelligent agents to get your work done sounds marvelous. On the other, I wonder how tenant administrators are going to cope with a flood of no-code or low-code agents created by users. It seems like we have been here before when Microsoft launched new tools without a full lifecycle framework to support creation, deployment, and ongoing management. Basic questions like what happens to agents created by a user account when that person leaves the organization remain unanswered. I sure hope that we’re not heading for choppy waters as the delights of AI-driven tooling unfolds.
Just another thing to keep on the to-do list for busy Microsoft 365 tenant administrators!
Microsoft Reannounces Teams Policy to Suppress In-Product Messages
In-Product Message Suppression is Great, But No Way Exists to Control Irritating Teams Pop-up Messages
Sometimes the appearance of a notification in the Microsoft 365 message center is less understandable than the norm. Badly written message center notifications are not unknown, but usually the intent and meaning can be interpreted with a little effort.
MC989968 (28 January 2025) is different. I can’t understand why Microsoft published this notification because it seems to repeat MC808161 (3 July 2024). Both notifications cover the topic of a welcome setting in the Teams update management policy to suppress some in-product announcements of news and conferences.
MC989968 says “Some in-product messages in Microsoft Teams can now be controlled with the New-CsTeamsUpdateManagementPolicy command. Using this control, tenant admins can limit in-product messages relating to periodic What’s New and Conferences updates.” MC808161 is now offline, but as far as I can tell from the multiple sites that republish message center updates, that notification covers the same ground.
In passing, I wonder why so many people think that republishing message center updates is a valuable service to humanity. It seems like none add much value in terms of interpreting the information communicated by Microsoft, so what gets put online is no more than a mild form of plagiarism.
The Irritation of Teams Pop-up Messages
A frustrating aspect of Teams is its continuing insistence on displaying pop-up messages about every new tweak added to its feature set. Figure 1 is a collection of pop-up messages generated in a few minutes by navigating to the OneDrive, Calendar, and Planner apps. In the age of artificial intelligence, you’d imagine that Teams could learn about user preferences and adopt its behavior to match the way people work. A series of quick clicks to dismiss pop-ups should be sufficient evidence that I don’t want to see these annoying interruptions to my workflow.

While I appreciate that some like to be notified about the ongoing stream of new features and changes that appear in Teams, there should be some way to turn off pop-up messages. The Teams settings app covers many different categories of notifications from chats to channels to meetings, but there’s nothing available to allow users to decline seeing any product pop-ups. That seems to be a missed opportunity to please users.
After all, if Microsoft can dedicate effort to allowing users to decide where notifications should be located when using the Teams Windows desktop client (Figure 2). Surely it should be possible to construct a a similar setting to allow or suppress pop-ups?

However, I don’t think Microsoft will grant my wish. MC949959 (6 December 2024, Microsoft 365 roadmap item 469491) says that being able to move meeting notifications to different screen locations will “make notifications more convenient and less disruptive, enhancing both focus and productivity.” I guess a similar rationale is needed to eradicate product pop-up messages and just saying that removing daily irritation won’t be enough.
Suppressing In-Product Messages
After seeing MC989968, I wondered if the Teams update policy that I updated when MC808161 appeared to suppress both what’s new and conference updates was still valid. Nothing has changed and the documentation for the New-CsTeamsUpdateManagementPolicy and Set-CsTeamsUpdateManagementPolicy cmdlets remains as before when it comes to manipulating the “campaign” categories of in-product messages to suppress. As a reminder, this code does the job for both What’s New and Conference (advertising) messages.
[array]$DisabledMessages = "edf2633e-9827-44de-b34c-8b8b9717e84c", "91382d07-8b89-444c-bbcb-cfe43133af33" Set-CsTeamsUpdateManagementPolicy -Identity Global -DisabledInProductMessages $DisabledMessages
The Need to Remove Frustration
Nothing appears to have changed in the messaging, intent, or implementation of the content announced in MC808161 in July 2024 and MC989968 in January 2025. However, the latter did a service by reminding me that a Teams policy exists to control in-product messages. The ability to disable some categories of in-product messages is a good thing. It would be so much better if Teams went the extra yard to remove user frustration by introducing a setting to control the display of pop-up messages.
So much change, all the time. It’s a challenge to stay abreast of all the updates Microsoft makes across the Microsoft 365 ecosystem. Subscribe to the Office 365 for IT Pros eBook to receive monthly insights into what happens, why it happens, and what new features and capabilities mean for your tenant.
Microsoft Cloud Revenues Keep on Heaping Up
Microsoft Cloud Revenues Break the $40 Billion Mark for the First Time
On Wednesday, January 29, 2025, Microsoft released their FY25 Q2 earnings and discussed the results at an analyst meeting afterward (transcript available here). Given Microsoft’s recent focus on anything branded as Copilot, a lot of attention was paid to what’s happening around artificial intelligence, but the headline number released was the $40.9 billion revenue for the Microsoft Cloud (an annual run rate of $163.6 billion).
The Microsoft Cloud is an amorphous grouping of products that includes Microsoft 365, Azure, Dynamics 365, and LinkedIn. The growth in cloud revenues has been strong and steady
The free edition of GitHub Copilot in Visual Studio Code notched up over a million signups in the first week post-launch. As I’ve noted here, GitHub Copilot is a great help to any developer, including those working with PowerShell for Microsoft 365.
Progress with Microsoft 365 Copilot
Satya Nadella said that customers who bought Copilot (for Microsoft 365) had expanded the number of seats by ten times over the last 18 months. That sounds impressive, but we don’t know the real numbers and when you start from a low base any increase seems large. Nadella also said that the number of people who use Copilot (for Microsoft 365) daily more than doubled over the last quarter with “usage intensity” increasing 60%. Usage intensity is Microsoft’s way of measuring how often people use Copilot for Microsoft 365 and what they do.
According to the documentation the statistic is based on “the average number of Copilot actions taken per user per month.” Microsoft 365 message notification MC986522 (23 January 2025, reports the addition of usage intensity and retention metrics in the Microsoft Copilot dashboard to allow customers to see how active their users are. It’s also possible to use the Graph Copilot usage API to analyze Copilot interactions and decide if people are active enough to keep their expensive licenses.
Copilot agents also received attention, with the claim being advanced that Copilot Studio makes it “as simple to build an agent as it is to create an Excel spreadsheet.” This is an aspiration rather than reality because creating a Copilot agent today (Figure 1) requires substantially more effort and expertise than firing up Excel to calculate some numbers.

CFO Amy Hood noted that the annual run rate for AI surpassed $13 billion and is above Microsoft’s expectations. The gap between the capital spending of circa $20 billion/quarter for the last several quarters and current revenues is one that Microsoft wants to close, and that’s why customers see so much stress being placed on Copilot.
No Detail about Microsoft 365 Seats
Microsoft failed to update the user numbers for Office 365, Microsoft 365, or Teams. The only clue was the statement that Microsoft 365 commercial seats grew by 7% year-over-year with revenue growth of 15% in constant currency. The growth was attributed to people switching to Microsoft 365 licenses and Copilot, but no details were given. A year ago, Microsoft said that the number of paid Office 365 seats was over 400 million. Applying a growth rate of 7% puts that number at around 428 million, which is as close as we can guess.
I couldn’t find a single mention of Teams in the analyst meeting transcript, so the official number for Teams users remains at 320 million as stated in October 2023. If Teams maintained the same ratio of seats to Office 365, it would be at around 350 million, bit Microsoft is staying silent on the topic for some reason. It’s interesting that the poster child of Microsoft investment briefings from a couple of short years ago has been left in the dust by the gallop toward AI.
Tactics to Generate Microsoft Cloud Revenues Won’t Change
I don’t expect much to change for Q3 (the current quarter). Microsoft will continue to be ultra-focused on driving Copilot revenue. Along will selling Microsoft 365 Copilot licenses, they’ll continue trying to convince customers to upgrade to higher base products, like Microsoft 365 E5. Given an apparent slowdown in new user acquisition, it’s the only way to keep the Microsoft 365 portion of the Microsoft Cloud revenues to grow.
Support the work of the Office 365 for IT Pros team by subscribing to the Office 365 for IT Pros eBook. Your support pays for the time we need to track, analyze, and document the changing world of Microsoft 365 and Office 365.
Primer: Using Exchange High Volume Email with Azure Automation
Use HVE with Azure Automation Runbooks to Send Email
The last article in the series about using Azure Automation with Microsoft 365 showed how to send email with the Send-MgUserMail cmdlet. The cmdlet can create and send messages from a designated user or shared mailbox to any valid SMTP recipient. As explained in the article, the approach works well for communicating details of background processing, such as reviewing audit records for suspicious activity, especially when run as a scheduled job. Computer systems are better than humans at remembering when they have work to do.
HVE and ECS Email Options
An issue raised in the Office 365 for IT Pros GitHub repository about a script I wrote for an article about the Exchange Online High Volume Email (HVE) feature prompted me to think about using HVE in an Azure Automation runbook.
HVE processes email often created by line of business applications or messages intended for high-volume circulation, such as corporate updates or marketing communications. The focus for HVE is internal email. Although HVE can send mail externally, Microsoft restricts external recipients to 2,000 daily per HVE account. The Azure Email Communication Service is a better solution for external email.
The HVE Scenario and Credentials
Let’s put together a scenario to test how to use HVE with Azure Automation. I decided to check a fact published on a website and act depending on the website content. As you might know, we publish monthly updates for the Office 365 for IT Pros eBook. The date of the most recent update and its release both appear on the book’s page (Figure 1).

The runbook uses the Invoke-WebRequest cmdlet to fetch the webpage content. It then parses the page to find the latest published release date. For test purposes, the runbook compares the publication date against a hardcoded date and sends email to a distribution list if it detects a new publication date. In a production environment, the date of the last known update could be stored and updated in a repository like a SharePoint Online list.
HVE supports basic and OAuth authentication. Microsoft plans to remove support for basic authentication for the SMTP AUTH protocol in September 2025. HVE uses a different SMTP endpoint and its traffic is limited to clients that can connect using a HVE account, so it’s unaffected by the deprecation in September. For now, you can pass the username and password for a HVE account to authenticate. When Microsoft removes this capability, the code will need to be updated to fetch an access token to use to authenticate.
I use an Azure Key Vault in the Azure account used for Azure Automation to store the HVE account credentials. Storing information in Key Vault eliminates any need for hardcoded credentials, which is always good.
Outline of the Script to use HVE with Azure Automation
The good news is that an automation account doesn’t need any additional resources to be loaded or consent for any special permissions before it can send email via HVE. The cmdlets necessary to interact with Azure Key Vault are in the AZ modules that Azure Automation automatically maintains for automation accounts (you don’t need to install or update these modules in the same way as you need to manage Microsoft 365 modules). The same is true for the Import-WebRequest and Send-MailMessage cmdlets. Send-MailMessage is now an old cmdlet that Microsoft would like to deprecate, but it works to send HVE messages.
The script goes through these steps.
- Uses a managed identity to connect to Azure.
- Fetches the user credentials for the HVE account from Azure Key Vault and builds a credentials object.
- Retrieves the content of the target web page and checks it to find the last update for the book.
- If an update is found, constructs a message and sends it to the distribution group via HVE (Figure 2).

You can download the complete runbook (script) from the Office 365 for IT Pros GitHub repository.
It was surprisingly easy to put together this proof of concept and demonstrate how to use HVE with Azure Automation. It’s a good example of how a scheduled background process can check if something has changed and take action when necessary. The hardest part was figuring out how to extra the publication date from the website. Once that was done, the code came together quickly because I was able to “borrow” bits from scripts written for previous articles.
Need some assistance to write and manage PowerShell scripts for Microsoft 365? Get a copy of the Automating Microsoft 365 with PowerShell eBook, available standalone or as part of the Office 365 for IT Pros eBook bundle.
Primer: Running Audit Searches and Sending Email from Azure Automation
Use Azure Automation for Audit Searches
In the past, I published articles covering the basics of using Azure Automation to process Microsoft 365 data. The articles cover the basics of using an automation account to execute runbooks (PowerShell scripts based on the Microsoft Graph PowerShell SDK), how to output results to a SharePoint Online list, and how to attach runbooks to automation schedules to make sure that processes execute automatically and reliably.
This article covers how to execute Microsoft 365 audit searches in runbooks and how to send the results extracted from the audit searches via email. I’m going to use the scenario discussed on 24 January about a flaw found in Entra ID that allowed users to change their user principal names. Microsoft has since addressed the problem, but the fact still remains that changes to user principal names can have consequences for services other than authentication. Any change like this deserves oversight. The purpose is to explore the principles rather than the details of a solution, and the techniques used here can be applied to any audit log search.
Basic Outline to Create a Runbook for a Microsoft 365 Audit Search
Two methods are available to search the unified audit log.
- Synchronous searches performed by the Search-UnifiedAuditLog cmdlet from the Exchange Online module.
- Asynchronous searches performed by the AuditLogQuery Graph API. The API is in still in beta but it’s how the Purview Audit search solution works. SDK cmdlets are also available for the API.
Opting for the Graph API makes sense for an Azure Automation job. Asynchronous searches take longer but that doesn’t matter when the job executes in the background, especially if it’s a scheduled run. In terms of the code, Microsoft has temporarily withdrawn the Get-MgBetaSecurityAuditLogQuery cmdlet and the Get-MgBetaSecurityAuditLogQueryRecord sometimes doesn’t work, so we use Graph API requests in this example. I used V2.25 of the Graph SDK and the cmdlets might have returned by the time you read this text.
The basic processing steps are:
- Construct the parameters for the audit log search. The Update User operation captures changes made to user accounts, so that’s what the search looks for over the last seven days.
- Submit the search and monitor its progress until completion.
- Retrieve the audit records.
- Process the audit records to check if any are for changes to the userPrincipalName property and capture details of these events.
- Create a HTML fragment containing the events and use it to create the HTML content for a message.
- Run the Send-MgUserMail cmdlet to send the message to a predetermined recipient. This can be any valid email address. In production, it’s likely that the recipient would be a distribution list, but it could be a Microsoft 365 group, or even a Teams channel.
Testing the Runbook for a Microsoft 365 Audit Search
As always, it’s wise to test the runbook code by running it interactively in a Microsoft Graph PowerShell SDK session. The automation account must have the AuditLogsQuery.Read.All application permission to access audit logs and Mail.Send to be able to send email. See my earlier post for how to assign Graph permissions to automation accounts. In production scenarios, you should use RBAC for Applications to restrict access for the automation account to specific mailboxes.
To mimic what happens when Azure Automation executes the runbook, use an app-only session by signing in with an app identifier, tenant identifier, and a certificate uploaded to the app. The app must have consent to use the two permissions listed above. Once everything works interactively, copy the code and create a new Azure Automation runbook and test that the code runs in that environment (Figure 1).

When everything checks out, you can register the runbook with an automation schedule. This check is a good example of something that should be done bi-weekly. Of course, what the recipients do when they receive the message (Figure 2) is up to them.

Better than Microsoft 365 Audit Policies?
Similar functionality in terms of sending email notifications for events found by Microsoft 365 audit searches available through Microsoft 365 audit policies. The reason why a DIY version might be preferable is that you have full control over the content presented in messages and the advice given to recipients, plus any associated processing you might want to do. For instance, you could log the highlighted audit events in a SharePoint Online list and require administrators to attest that they checked each event to make sure that it’s appropriate. That might be too much, but it’s possible.
The code I used for testing can be downloaded from the Office 365 for IT Pros GitHub repository.
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
Update #8 Available for Automating Microsoft 365 with PowerShell
The Most Comprehensive PowerShell Book for Microsoft 365

The Office 365 for IT Pros team is delighted to announce the availability of monthly update #8 for the Automating Microsoft 365 with PowerShell eBook. The book is included with the Office 365 for IT Pros eBook and is also available separately, including in a paperback edition published on a print on demand basis by Amazon.
Updates for the Automating Microsoft 365 with PowerShell eBook are published monthly, just like the Office 365 for IT Pros eBook. However, we try to make the PowerShell update available a few days ahead of the main book because it allows us to clear the deck to work on chapter updates for the main book at the end of each month.
The Office 365 for IT Pros eBook contains many PowerShell examples. Originally, it also included a PowerShell chapter. Because the book is already quite large and we wanted to give more coverage to the important topic of the Microsoft Graph and how it can be used to process Microsoft 365, the decision was made to create the Automating Microsoft 365 with PowerShell eBook and to update its content on an ongoing basis, just like we do with Office 365 for IT Pros. The net result is that the book now spans five chapters and 280 pages of practical and useful information about how to use PowerShell with Microsoft 365.
Mastering the Graph
In particular, we’ve spent a lot of time working out how to exploit the Microsoft Graph PowerShell SDK. This is an incredibly important component that not only replaces the AzureAD and Microsoft Online Services modules (due for imminent retirement) but also opens up the possibilities of accessing data such as Exchange Online mailboxes, SharePoint Online sites, pages, and lists, Planner plans and tasks, and so on. The book also covers how to access Microsoft 365 data with Azure Automation.
The nice thing about mastering the maze of Microsoft Graph APIs, permissions, and SDK cmdlets is that once you understand how things work, the same techniques can be applied to all sorts of data.
If you’re still struggling to convert scripts from using the AzureAD and MSOL modules, you’ll find a lot of value in Automating Microsoft 365 with PowerShell. We can’t convert scripts for you, but we can give you the knowledge needed to smoothen and quicken the process.
The Print Edition
Due to the 1,200-page (plus) size of the Office 365 for IT Pros eBook, we’ve never been able to provide a print edition. Printing a book that’s updated monthly sounds like a bit of fool’s errand, but we have received many requests because some people like consulting print books for technical topics. Some even went so far as to print the PDF issued for each monthly update.
There are many print on demand services available for books. We decided to try Amazon, and people can now buy a paperback edition of Automating Microsoft 365 with PowerShell. It’s impossible to update a print copy, so if you buy a printed book, it contains whatever content is current at the time of purchase. Two major differences exist between the print and electronic versions (PDF and EPUB). The print version obviously can’t support hyperlinks, so these become footnotes. The second difference is that we provide an index for the print version to replace the search capabilities that the electronic versions have.
The paperback has proven to be more popular than anticipated, so we’ll keep it going for those who want this option.
On to Update #9
In line with our regular cadence, monthly update #116 for the Office 365 for IT Pros eBook will be available for download on February 1, 2025. Subscribers can download the update #8 files for Automating Microsoft 365 with PowerShell now. We hope that you find the books useful. Let us know if we should cover other topics by adding a comment for this article.
Entra ID Allows People to Update their User Principal Names
No Good Reason Why Users Can Update User Principal Names
It’s unclear if Microsoft has updated the default permissions assigned to Entra user accounts, but it is now possible for unprivileged users to update user principal names through interfaces like the Entra admin center and PowerShell. To be clear, an unprivileged user can update the user principal name for their Entra ID account and not for other accounts. Nevertheless, I can’t think of a good reason why any organization would want to allow people to update something fundamental like a user principal name, but they can.
To test the theory, I created a new account for Eric Hammon and attempted to sign into the Entra admin center. I then navigated to the Users section to access the account properties. As you can see from Figure 1, the user principal name is editable.

I went ahead and updated the account to make the user principal name Eric.B.Hammond@office365itpros.com. For good measure, I uploaded a new user photo. The update proceeded without a problem and the result is shown in Figure 2.

A side effect of updating a user principal name is that the user’s primary SMTP address also changes. This is because of the dual write arrangement between Exchange Online and Entra ID whereby updates to mail-related properties occur in both directories. The update to the user principal name also updates the account’s Mail property, and this ripples through to Exchange Online, meaning that the full set of proxy addresses includes a new primary SMTP address (indicated by SMTP:). The previous primary SMTP address is preserved as a proxy to make sure that Exchange Online can deliver messages addressed to the old primary SMTP address.
Get-Mailbox -identity eric.b.hammond@office365itpros.com | Select-Object -ExpandProperty emailaddresses SIP:eric.b.hammond@office365itpros.com SMTP:Eric.B.Hammond@office365itpros.com smtp:Eric.A.Hammond@office365itpros.com smtp:Eric.Hammond@office365itpros.com
Update User Principal Name with PowerShell
After validating that it is possible for a user to update their user principal name and photo via the Entra admin center, I tried with the Microsoft Graph PowerShell SDK (Figure 3). I expected this to work because much of the Entra admin center is built on top of the Microsoft Graph, especially anything to do with user accounts and groups (you can validate this by running the Graph X-Ray tool).

Essentially, these tests indicate that any tool based on the Microsoft Graph Users API will allow users to update their user principal name. I’m not bothered by the Entra admin center allowing people to update their photo because that facility is available elsewhere, notably in OWA and the new Outlook for Windows.
Blocking Access to the Entra Admin Center
Some control can be exerted for the Entra admin center by setting the option to restrict access to users that hold administrative roles (Figure 4).

This is only a partial block because accounts with relatively unprivileged roles, like Reports Reader, can still access the Entra admin center and update their user principal names. On the other hand, it does block casual access and is therefore a recommended setting to have in place.
Blocking Access to the Microsoft Graph PowerShell SDK
The ability to create an interactive session with the Microsoft Graph PowerShell SDK is governed by controls on the Microsoft Graph Command Line Tools enterprise app. Like other enterprise apps created by third parties for use in multiple Entra ID tenants, the instantiation for the app is a service principal that holds the consented permissions available in Graph SDK sessions. It can also hold a set of users and groups who are allowed to access the app. By default, the list of users assigned to the app is empty, which means that any user can run the Connect-MgGraph cmdlet in a PowerShell session to connect to the Graph.
Obviously, allowing open access to such a powerful capability isn’t a good idea, and tenants should take steps to secure access to the Microsoft Graph Command Line Tools app. With controls in place, anyone who isn’t on the approved list will see an AADSTS50105 error and be blocked from access (Figure 5).

And if you’re blocking access to PowerShell for the Graph SDK, consider doing the same for other Microsoft 365 PowerShellmodules.
No Apparent Justification for People to Update User Principal Names
Microsoft doesn’t make changes without reason, so something must have happened to convince the Entra ID developers to allow users to update user principal names. I can’t think of a convincing reason for such a change, but perhaps the logic will become apparent over time. In the meantime, if you don’t like people being able to change user principal names, consider applying the blocks described above.
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
Primer: How to Schedule Azure Automation Runbooks to Process Microsoft 365 Data
Let Azure Automation Execute Runbooks Using an Automation Schedule
After covering the basics of using Azure Automation runbooks to access Microsoft 365 data, the second part of the primer covers how to write data generated by a runbook to a SharePoint Online list. Populating data on an on-demand basis is valuable, but consistent gathering of data for comparison purposes can be so much more valuable. For that to happen, we need Azure Automation to execute the runbook on a schedule.
Runbook Details
The runbook that I have been using is called GetRecentAccounts. Figure 1 shows the runbook properties as viewed through the Azure portal. Essential details here are the name of the automation account associated with the runbook (M365Automation) and the resource group it uses (ExoAutomation).

Resource groups are containers that Azure uses to hold resources. Resource groups are also used to track consumption, such as the processing performed by services such as Microsoft 365 backup or SharePoint document translation. An automation schedule is a resource, so it’s managed in a resource group. You might be familiar with the Windows Task Scheduler. The automation schedule is a more secure and better solution for running PowerShell scripts.
Creating an Automation Schedule
Two methods exist to create an automation schedule: through the GUI of the Azure portal or PowerShell using cmdlets from the Az.Accounts and Az.Automation modules. Because this discussion is all about PowerShell, we’ll take the latter option. Besides, using PowerShell to manage object often exposes details that are otherwise easily overlooked.
After installing the latest version of the modules from the PowerShell gallery, connect to the Azure account like this:
Connect-AzAccount -TenantId $TenantId -SubscriptionId $SubscriptionId -AccountId Tony.Redmond@office365itpros.com
You’ll need to know identifier for the tenant and Azure subscription and the account you sign in with must be entitled to manage Azure resources. The subscription identifier is listed in the runbook details (Figure 1) and the tenant identifier is easily found by running the Get-MgOrganization cmdlet:
(Get-MgOrganization).Id
After establishing a connection, you can manage the Azure automation resources available to the account. This code shows how to create a new automation schedule to run at 17:30 every weekday. The names of the automation account and resource group are required. This automation schedule doesn’t have an end date. If you want to add an end date, pass a valid date value in the EndTime parameter. More details about creating automation schedules are in the documentation.
# Set up when the new schedule will start (today at 17:30) $StartTime = (Get-Date "17:30:00") $ResourceGroup = "ExoAutomation" $ScheduleName = "WeekDaySchedule" $AutomationAccountName = "M365Automation" # Define what days of the week the schedule will run [System.DayOfWeek[]]$WeekDays = @([System.DayOfWeek]::Monday..[System.DayOfWeek]::Friday) # Create the new schedule New-AzAutomationSchedule -AutomationAccountName $AutomationAccountName -Name $ScheduleName -StartTime $StartTime -WeekInterval 1 -DaysOfWeek $WeekDays -ResourceGroupName $ResourceGroup -Description 'Schedule to execute runbooks at 17:30 UTC on weekdays' # Check its details Get-AzAutomationSchedule -ResourceGroupName $ResourceGroup -AutomationAccountName $AutomationAccountName StartTime : 22/01/2025 17:30:00 +00:00 ExpiryTime : 31/12/9999 23:59:00 +00:00 IsEnabled : True NextRun : 22/01/2025 17:30:00 +00:00 Interval : 1 Frequency : Week MonthlyScheduleOptions : WeeklyScheduleOptions : Microsoft.Azure.Commands.Automation.Model.WeeklyScheduleOptions TimeZone : Etc/UTC ResourceGroupName : ExoAutomation AutomationAccountName : M365Automation Name : WeekDaySchedule CreationTime : 22/01/2025 17:12:50 +00:00 LastModifiedTime : 22/01/2025 17:16:02 +00:00 Description : Schedule to execute runbooks at 17:30 UTC on weekdays
Register the Runbook with an Automation Schedule
Before Azure Automation can execute a runbook without human intervention, the runbook must be registered with an automation schedule. This code does the job, as does the Link to schedule option shown in Figure 1.
# Name of runbook to schedule $RunbookName = "GetRecentAccounts" Register-AzAutomationScheduledRunbook -AutomationAccountName $AutomationAccountName -Name $RunbookName -ScheduleName $ScheduleName -ResourceGroupName $ResourceGroup Register-AzAutomationScheduledRunbook: The runbook has no published version. Runbook name GetRecentAccounts.
Or rather, it would if the runbook was published. Up to now, the runbook has been under active development, and we’ve used the test pane feature to make sure that the runbook code runs as expected. Eventually, a runbook reaches the point where the code works and is stable. At that point, you should publish the runbook to create a version that Azure Automation can schedule. Development can still continue, but Azure Automation will only run the published version.
To create a published version, edit the runbook and choose the Publish option (Figure 2).

Alternatively, use the Publish-AzAutomationRunbook cmdlet to publish the runbook:
$Params = @{} $Params.Add("Name", $RunbookName) $Params.Add("ResourceGroupName", $ResourceGroup) $Params.Add("AutomationAccountName", $AutomationAccountName) Publish-AzAutomationRunbook @Params Location : westeurope Tags : {} JobCount : 0 RunbookType : PowerShell72 Parameters : {} LogVerbose : False LogProgress : False LastModifiedBy : State : Published ResourceGroupName : ExoAutomation AutomationAccountName : M365Automation Name : GetRecentAccounts CreationTime : 20/01/2025 15:03:41 +00:00 LastModifiedTime : 22/01/2025 18:06:47 +00:00 Description : Find recently created accounts
Either way, the runbook can now be registered with an automation schedule in the Azure portal or by running the Register-AzAutomationScheduledRunbook cmdlet. Confirmation of the scheduling can be obtained by examining the details of the schedule in the Azure portal (Figure 3) or by running the Get-AzAutomationScheduledRunbook cmdlet:
Get-AzAutomationScheduledRunbook -RunbookName $RunbookName -ScheduleName $ScheduleName -ResourceGroupName $ResourceGroup -AutomationAccountName $AutomationAccountName ResourceGroupName : ExoAutomation RunOn : AutomationAccountName : M365Automation JobScheduleId : 9a2aadd0-c3ac-4baa-8cfb-855b12a1a277 RunbookName : GetRecentAccounts ScheduleName : WeekDaySchedule

To confirm that everything’s running as expected, wait until after the job should run and check the output location for new data or use the Get-AzAutomationJob cmdlet:
Get-AzAutomationJob -ResourceGroupName $ResourceGroup -RunbookName $RunbookName -AutomationAccountName $AutomationAccountName | Where-Object {$_.Status -eq 'Completed'} | Format-Table StartTime, EndTime, RunbookName, Status StartTime EndTime RunbookName Status --------- ------- ----------- ------ 22/01/2025 17:30:59 +00:00 22/01/2025 17:31:53 +00:00 GetRecentAccounts Completed
The Value of Scheduled Runbooks
There’s nothing like an automated schedule to make sure that jobs get run at the right time. The combination of runbooks and schedules means that you can be sure that important Microsoft 365 automation processes are executed. That’s a nice feeling.
Need some assistance to write and manage PowerShell scripts for Microsoft 365? Get a copy of the Automating Microsoft 365 with PowerShell eBook, available standalone or as part of the Office 365 for IT Pros eBook bundle.
Primer: Output Data Generated with an Azure Automation Runbook to a SharePoint List
Execute an Azure Automation Runbook and Store its Results in a SharePoint Online List Item
Yesterday, I explained the basics of how to use Azure Automation to run a script using Microsoft Graph PowerShell SDK cmdlets. Today, I want to extend the knowledge outlined in that article to demonstrate another important aspect: How to output information from an Azure Automation runbook.
Azure Automation runbooks execute on headless servers that you don’t control. Runbooks can create output data but need to get that information back to whoever needs it. Available Microsoft 365 methods to share information include:
- Creating a file in a SharePoint Online document library.
- Posting a message to a Teams chat or channel.
- Sending email.
- Creating items in a list in a SharePoint Online site.
This article covers how to use the last method because SharePoint lists are a good way to capture the information generated by background processes. The script used yesterday reports user accounts created in the last 30 days. We’ll extend it to find some additional information and create a list item containing the data.
The Basics – Resources and Permissions
The first version of the script uses two modules of the Microsoft Graph PowerShell SDK for authentication and to find user accounts. To interact with SharePoint sites, we must add the Microsoft.Graph.Sites module and because the script generates some information about Microsoft 365 Groups, add the Microsoft.Graph.Groups module too.
The automation account already has the User.Read.All Graph permission. To read details of groups, it needs the Group.Read.All permission. The interaction with sites is both read (to access the site and find the target list) and write (to create items in the target list), so the automation account needs the Sites.ReadWrite.All permission. We’ll add the two permissions using PowerShell as follows:
Connect-MgGraph -Scopes AppRoleAssignment.ReadWrite.All # Add Graph permissions to the service principal for the automation account $GraphApp = Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'" $TargetSP = Get-MgServicePrincipal -filter "displayname eq 'M365Automation'" [array]$Permissions = "Group.Read.All", "Sites.ReadWrite.All" ForEach ($Permission in $Permissions){ $Role = $GraphApp.AppRoles | Where-Object {$_.Value -eq $Permission} # Create the parameters for the new assignment $AppRoleAssignment = @{} $AppRoleAssignment.Add("PrincipalId",$TargetSP.Id) $AppRoleAssignment.Add("ResourceId",$GraphApp.Id) $AppRoleAssignment.Add("AppRoleId",$Role.Id) $RoleAssignment = New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $TargetId -BodyParameter $AppRoleAssignment If ($RoleAssignment.AppRoleId) { Write-Host ("{0} permission granted to {1}" -f $Role.Value, $TargetSP.DisplayName) } }
After these commands execute, Figure 1 shows what you should see when viewing the permissions for the automation account in the enterprise apps section of the Entra admin center.

Prepare the Target List
Although you can create a list in a SharePoint Online site with PowerShell (here’s how), it’s easier to do this work through the SharePoint browser interface. Select a target site and create a list there. I used a minimal set of fields to capture details like the number of user accounts, the number of Microsoft 365 groups, the names of recently added user accounts, and a timestamp. Don’t get too worried about what data is output: we’re just exploring principles here instead of creating a fully-fledged solution. Remember to note the name of the fields you add to the list because they’ll need to be stated in the script (field names are case sensitive).
Amend the Runbook Code
Yesterday’s script is simple and spans just a few commands to find and list recently added user accounts. Today’s script needs more code. Let’s investigate.
First, we need to connect to the SharePoint site that holds the target list. This code takes a URL for a site and converts it to a SharePoint site identifier that can be used to find a site. We can then look for the target list and fetch its details.
$Uri = "https://office365itpros.sharepoint.com/sites/Office365Adoption" $SiteId = $Uri.Split('//')[1].split("/")[0] + ":/sites/" + $Uri.Split('//')[1].split("/")[2] $Site = Get-MgSite -SiteId $SiteId If (!$Site) { Write-Output ("Unable to connect to site {0} with id {1}" -f $Uri, $SiteId) Exit } $List = Get-MgSiteList -SiteId $Site.Id -Filter "displayName eq 'Tenant Statistics'" If (!$List) { Write-Output ("Unable to find list 'Tenant Statistics' in site {0}" -f $Site.DisplayName) Exit }
The script includes some simple code to find user accounts and Microsoft 365 groups:
[array]$UserAccounts = Get-MgUser -All -PageSize 500 -Filter "userType eq 'Member'" [array]$M365Groups = Get-MgGroup -Filter "groupTypes/any(c:c eq 'unified')" -All -PageSize 500
Finally, the runbook has the code to create an item in the target list. This is accomplished by creating a hash table to hold details of the fields (inside a separate hash table). What seems to be an odd structure is because PowerShell is mimicking a JSON structure for a payload body submitted to the Graph request to add the item. In any case, here’s the code:
$NewItemParameters = @{ fields = @{ Title = Get-Date ($Date) -format s Rundate = $RunDate NumberM365Groups = $M365Groups.Count NumberUserAccounts = $UserAccounts.Count RecentUserAccounts = $RecentUserAccounts } } $NewItem = New-MgSiteListItem -SiteId $Site.Id -ListId $List.Id -BodyParameter $NewItemParameters If ($NewItem) { Write-Output ("Added item to list {0}" -f $List.DisplayName) } Else { Write-Output "Failed to add item to list"
I can’t emphasize too much the importance of testing code interactively before submitting it to Azure Automation. When that happens, after running in test mode, the runbook should report that it created a new item in the list (Figure 2).

To verify that the runbook succeeded, go to the SharePoint site and open the target list. The item(s) created by the runbook should be present.

Running Azure Automation Runbooks Aren’t So Hard After All
I hope that by now you’ll understand that running PowerShell scripts with Azure Automation is not particularly difficult. Once a runbook can output data, that data can be processed further. Lists are particularly adaptable in this way because there are many ways to reuse the data through channels like Power Apps or Power BI.
There are many code examples available that can help to solve automation problems. But the most important thing is to get the basics right. When that happens, everything clicks into place.
The script I used for the runbook can be downloaded from the Office 365 IT Pros repository on GitHub.
Support the work of the Office 365 for IT Pros team by subscribing to the Office 365 for IT Pros eBook. Your support pays for the time we need to track, analyze, and document the changing world of Microsoft 365 and Office 365.
Primer: How to Use Azure Automation to Run Microsoft Graph PowerShell SDK Scripts
Running PowerShell in Azure Automation Runbooks Seems Complex – But Is it?
Over this past weekend, I was quizzed about why many people recommend using Azure Automation runbooks to run PowerShell scripts when setting everything up is so complex. I guess that I’ve been using this stuff for so long that I just accept how it works and parse out some of the issues that people struggle with. In an attempt to help, I thought that I’d create a really simple example as a starting point. Let’s see how I can do.
The Nature of Azure Automation
Azure Automation is a cloud-based service that supports running PowerShell scripts on headless servers. The scripts are called runbooks and the code that runs in Azure Automation is similar to the scripts that you run interactively.
The big difference is that Azure Automation is a non-interactive environment. Prompts don’t exist and any output is only seen when scripts finish. That being said, it is not difficult to take code written for interactive use and move it to Azure Automation. In fact, because debugging code in a non-interactive environment is difficult, it’s always best to make sure that a script runs without problems in an interactive environment before attempting to move it to Azure Automation.
Starting Off with Azure Automation
To begin, you’ll need an Azure subscription with an associated credit card to pay for the resources used to run code. Microsoft has Azure free account and pay-as-you-go options.
With an Azure account, you can create a resource group (to hold the resources needed by Azure Automation) and an automation account (Figure 1). The automation account holds the permissions and roles needed to access Microsoft 365 data.

Resources for the Automation Account
Before writing any code to access Microsoft 365 via Azure Automation, you’ll need to add some resources to the automation account. The resources are the PowerShell modules containing the cmdlets needed by your scripts. When you execute a runbook, Azure Automation loads the modules into the session created on the headless server.
To add a PowerShell module, access the automation account and go to Modules under Shared Resources. Click Browse gallery and input the name of the module to add (Figure 2).This example features a script to list recently created Entra ID user accounts, so I added the Microsoft.Graph.Authentication (needed for any Graph SDK script) and the Microsoft.Graph.Users modules.

How do you know what modules to add? In some cases, like Exchange Online, a single module (ExchangeOnlineManagement) is needed. The Microsoft Graph PowerShell SDK is more complex because it’s composed of multiple sub-modules. An easy way to find out which sub-module is needed for a specific cmdlet is to use Get-Command in an interactive session. For instance, Get-Command reports that the source for the Get-MgUser cmdlet is the Microsoft.Graph.Users module:
Get-Command Get-MgUser | Format-Table Name, Source Name Source ---- ------ Get-MgUser Microsoft.Graph.Users
Permissions for the Automation Account
As you’re probably aware, any access to data via the Microsoft Graph is governed by permissions. Automation accounts use application permissions and therefore have access to any data in the tenant allowed by the assigned permissions.
The permissions also include Entra ID roles needed to access the data you want to process. For instance, cmdlets from the Exchange Online module assume that they’re run by administrators, so the automation account must be added to the Entra ID Exchange administrator role and have consent for the Exchange ManageAsApp permission.
The permissions granted to automation accounts are held by the service principal for each account. You can see details of the service principal for an automation account in the enterprise apps section of the Entra admin center. Figure 3 shows that the automation account called M365Automation has a single assigned Graph permission (User.Read.All).

The Entra admin center allows you to see assigned permissions but not assign other permissions. You can only assign permissions with PowerShell. This is a little messy, but once you know how, it will make sense. First, find the details of the Graph application (which always has the same identifier) and the service principal for the automation account.
$GraphApp = Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'" $TargetSP = Get-MgServicePrincipal -filter "displayname eq 'M365Automation'"
Next, find the identifier for the app role (permissions) we want to assign.
$Role = $GraphApp.AppRoles | Where-Object {$_.Value -eq "User.Read.All"}
Now build a hash table containing the parameters for the new role assignment. As you can see, the parameters are the identifiers for the service principal, resource (Microsoft Graph), and the role.
$AppRoleAssignment = @{} $AppRoleAssignment.Add("PrincipalId",$TargetSP.Id) $AppRoleAssignment.Add("ResourceId",$GraphApp.Id) $AppRoleAssignment.Add("AppRoleId",$Role.Id)
Finally, run the New-MgServicePrincipalAppRoleAssignment cmdlet to make the assignment and report success if an application role assignment identifier is returned.
$RoleAssignment = New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $TargetId -BodyParameter $AppRoleAssignment If ($RoleAssignment.AppRoleId) { Write-Host ("{0} permission granted to {1}" -f $Role.Value, $TargetSP.DisplayName) }
Write Some Code for an Azure Automation Runbook
All the steps above have created the environment to write and run some PowerShell code. My example is to return the names of Entra ID accounts created in the last month. In an interactive session, the code is:
$Date = (Get-Date).ToUniversalTime().AddDays(-30).ToString("yyyy-MM-ddTHH:mm:ssZ") [array]$Users = Get-MgUser -Filter "createdDateTime ge $Date" -Property Id, displayName, UserType, CreatedDateTime |Sort-Object UserType If ($Users) { $Users | Format-Table DisplayName, UserType }
The same code works in Azure Automation. Go to the automation account and create a PowerShell runbook based on V7.2. Copy the same code into the runbook and add a line to authenticate using a managed identity:
Connect-MgGraph -Identity -NoWelcome
A managed identity is a system-managed highly secure identity. All the major Microsoft 365 PowerShell modules support system-assigned managed identities. Using a managed identity for authentication means that you don’t need to worry about passwords, secrets, or X.509 certifications.
After copying the code into the runbook and adding the connection via a managed identity, the runbook should look like Figure 4.

The test pane allows you to test the code under Azure Automation. When the test pane loads, click Start. Azure Automation goes through its process to allocate a server, provision the server with the necessary resources, and then run the code. When the code finishes, you’ll see the output (Figure 5). It’s always nice to see the expected result when an Azure automation runbook stops.

Lots More Possible with Azure Automation Runbooks
We’ve been through a basic example to explore the principles involved in creating an Azure Automation account, adding resources and permissions, and running some code. There’s lots more to do from this point: code will be more complex and probably create some output like email, SharePoint Online documents, or Teams messages, more resources and permissions will be needed, and you’ll probably want to explore how to schedule jobs so that they run on a regular basis. For instance, checking audit events weekly for signs of any problems with tenant security.
Azure Automation isn’t overly complex. Like all of us, it just needs to be appreciated in its own way.
Need some assistance to write and manage PowerShell scripts for Microsoft 365? Get a copy of the Automating Microsoft 365 with PowerShell eBook, available standalone or as part of the Office 365 for IT Pros eBook bundle.
How to Replace Group Owners When They Leave the Organization
Replace Group Owners to Avoid Ownerless Groups
An ownerless group is not a thing of beauty, but it can happen when someone leaves an organization and the remobal of their Entra ID user account results in some groups becoming ownerless. The Microsoft 365 group ownership policy helps, but it’s better to avoid the problem in the first place by proactively replacing the to-be-deleted account with a new group owner.
Writing PowerShell to Replace Group Owners
Which brings me to a PowerShell snippet posted in LinkedIn to address the problem. Here’s the code:
$OldOwnerUPN = "user@domain.com" $NewOwnerUPN = "user1@domain.com" $Groups = Get-UnifiedGroup -ResultSize Unlimited foreach ($Group in $Groups) { $Owners = Get-UnifiedGroupLinks -Identity $Group.Identity -LinkType Owners if ($Owners.PrimarySmtpAddress -contains $OldOwnerUPN) { Remove-UnifiedGroupLinks -Identity $Group.Identity -LinkType Owners -Links $OldOwnerUPN -Confirm:$false Add-UnifiedGroupLinks -Identity $Group.Identity -LinkType Members -Links $NewOwnerUPN -Confirm:$false Add-UnifiedGroupLinks -Identity $Group.Identity -LinkType Owners -Links $NewOwnerUPN -Confirm:$false Write-Output "$($Group.DisplayName): Replaced $OldOwnerUPN with $NewOwnerUPN as owner" } }#
This is a great example of proof-of-concept code that runs superbly in a small tenant where there are fewer than a hundred or so groups but rapidly runs out of steam rapidly as the number of objects to process escalates. The big performance sink is running the Get-UnifiedGroup cmdlet to fetch every Microsoft 365 group in the tenant. Because of the number of properties it retrieves, Get-UnifiedGroup is not a fast cmdlet. It’s a cmdlet that should be used judiciously rather than being the default method to fetch details of Microsoft 365 groups.
Use the Graph for Best Performance
The Get-MgUserOwnedObject cmdlet from the Microsoft Graph PowerShell SDK is faster and more scalable. Get-MgUserOwnedObject is based on the Graph list OwnedObjects API. Its purpose is to find the set of Entra ID directory objects owned by a user account and requires consent for the Directory.Read.All permission to find those objects.
This example fetches the set of Microsoft 365 Groups owned by a user account. Unfortunately, it’s not possible to use a server-side filter to extract the Microsoft 365 groups from the set of directory objects fetched by the cmdlet. The set of owned objects can include other group types such security groups and distribution lists and other objects like applications and service principals. Using a client-side filter isn’t a huge issue here because most of the fetched objects are likely to be Microsoft 365 groups.
[array]$Groups = Get-MgUserOwnedObject -UserId Kim.Akers@office365itpros.com -All | Where-Object {$_.additionalProperties.groupTypes -eq "unified"}
Like many Graph SDK cmdlets, the output for a group is determined by the underlying Graph request. If you look at the information returned for an object in the output array, you’ll see something like this:
$Groups[0] | Format-List DeletedDateTime : Id : f9b6dcb7-609d-48ca-83c1-5afbfe888fe0 AdditionalProperties : {[@odata.type, #microsoft.graph.group], [createdDateTime, 2020-06-08T16:59:06Z], [creationOptions, System.Object[]], [description, Sunny Days]…}
If you examine the details of the additionalProperties property for a group, you’ll see the “normal” properties that a cmdlet like Get-MgGroup returns for a group.
$Groups[0].additionalProperties @odata.type #microsoft.graph.group createdDateTime 2020-06-08T16:59:06Z creationOptions {SPSiteLanguage:1033, HubSiteId:00000000-0000-0000-0000-000000000000, SensitivityLabel:00000000-0000-0000-0000-000000000000, ProvisionGro… description Sunny Days displayName Sunny Days groupTypes {Unified} mail SunnyDays@office365itpros.com mailEnabled True mailNickname SunnyDays proxyAddresses {SMTP:SunnyDays@office365itpros.com, SPO:SPO_c5365af4-7636-4bca-a9d2-e1eb9bbfe9f6@SPO_b… renewedDateTime 2020-06-08T16:59:06Z resourceBehaviorOptions {} resourceProvisioningOptions {} securityEnabled False securityIdentifier S-1-12-1-4189510839-1221222557-4217028995-3767503102 visibility Private onPremisesProvisioningErrors {} serviceProvisioningErrors {}
The most important property is the group identifier because it’s needed to update group membership. The other group properties can be accessed by prefixing them with additionalProperties (which is case sensitive). For example:
$Groups[0].additionalProperties.displayName Office 365 Planner Tips
Rewriting the Original Code to Replace Group Owners
The original code can be adjusted to replace Get-UnifiedGroup with Get-MgUserOwnedObject. Apart from the performance boost gained by only finding the set of Microsoft 365 groups owned by the user instead of all groups, eliminating the need to run the Get-UnifiedGroupLinks cmdlet to check the ownership of each group improves code execution further:
[array]$Groups = Get-MgUserOwnedObject -UserId $OldOwnerUPN -All | Where-Object {$_.additionalProperties.groupTypes -eq "unified ForEach ($Group in $Groups) { Remove-UnifiedGroupLinks -Identity $Group.Id -LinkType Owners -Links $OldOwnerUPN -Confirm:$false Add-UnifiedGroupLinks -Identity $Group.Id -LinkType Members -Links $NewOwnerUPN -Confirm:$false Add-UnifiedGroupLinks -Identity $Group.Id -LinkType Owners -Links $NewOwnerUPN -Confirm:$false Write-Output "$($Group.additionalPropertiesDisplayName): Replaced $OldOwnerUPN with $NewOwnerUPN as owner" }
The lesson here is that you can mix and match cmdlets from different Microsoft 365 PowerShell modules to solve problems. In this case, Get-MgUserOwnedObject finds groups to process before the group memberships are updated using the Remove-UnifiedGroupLinks and Add-UnifiedGroupLinks cmdlets.
Running a Pure Microsoft Graph PowerShell SDK Version
You might want to write a script based solely on the Microsoft Graph PowerShell SDK instead of combining Exchange Online and Graph SDK cmdlets. To do this, we replace the Exchange cmdlets with the following SDK cmdlets:
- New-MgGroupMember: Add the new owner as a member of the group.
- New-MgGroupOwnerByRef: Add the new owner as a group owner. This should be done before removing the original owner to avoid the risk of failure because Entra ID won’t allow cmdlets to remove the last owner of a group
- Remove-MgGroupOwnerDirectoryObjectByRef: Remove the old owner as a group owner. This also removes the account from group membership.

To see how to use these cmdlets, check out this script, available from the Office 365 for IT Pros GitHub Repository. You’ll notice that I include calls to Get-MgGroupMember and Get-MgGroupOwner to avoid attempting to add the new owner as a group member and owner if they already hold these roles. The best thing of all is that the script (Figure 1) is extremely fast.
To complete the job, you should update any security groups, distribution lists, and dynamic distribution lists owned by the soon-to-depart account. The required code isn’t difficult, so I shall leave it to the reader to write.
Need some assistance to write and manage PowerShell scripts for Microsoft 365? Get a copy of the Automating Microsoft 365 with PowerShell eBook, available standalone or as part of the Office 365 for IT Pros eBook bundle.
Microsoft 365 User Profile Card Gets Name Pronunciation
Name Pronunciation Recordings Helps People Get Names Right
Message center notification MC917748 (last update 13 November 2024, Microsoft 365 roadmap item 420329) marks the latest update for the Microsoft 365 user profile card. This time round, users get the opportunity to add a recording of up to ten seconds to help colleagues understand how to pronounce their name correctly (Figure 1).

Microsoft says that the “new feature helps promote diversity by giving working colleagues relevant information about each other. Names are a crucial part of a person’s identity. The incorrect pronunciation of a person’s name can lead to anxiety and offense in some cases. Correctly pronouncing a person’s name helps to create an inclusive environment.” Having the proven ability to make a mess of many peoples’ names in my career, I should find this feature useful.
Once a name pronunciation is available, people can play the recording back by clicking the playback button beside the user’s name (Figure 2).

General availability is scheduled for mid-January 2025, so the update is currently rolling out. The roadmap item tags the feature for Teams, but MC917748 correctly notes that it’s also available in OWA and the new Outlook for Windows (but not yet in Outlook classic). Over time, I assume that name pronunciation recording will show up everywhere that the Microsoft 365 profile card is visible.
Where Name Pronunciation Recordings are Stored
MC917748 says “Pronunciation data is stored in each user’s mailbox until the user deletes the recording.” Keeping the data in user mailboxes means that pronunciation recordings are available across all clients across all workstations and avoid the kind of problems encountered with Outlook classic where settings are usually held in the system registry.
The non-IPM folders of a mailbox are not visible to normal email clients like Outlook. Applications often use folders in this section to store configuration and other data. The new Outlook for Windows and OWA store many mailbox settings in sub-folders of ApplicationDataRoot, and browsing through those folders with the MFCMAPI utility reveals a folder called ApplicationDataRoot8c22b648-ee54-4ece-a4ca-3015b6d24f8esource_sourcenamepronunciation.
The folder holds a single message item containing the pronunciation recording. Figure 3 shows how the item appears in MFCMAPI. In my case, the recording takes 440,728 bytes (approximately 430 KB), which seems about right for a six-second recording.

Enabling Name Pronunciation Recordings
According to MC917748, the feature is off by default, meaning that you don’t see the icons to record and play back name pronunciation recordings in the user profile card. However, the feature is enabled in every tenant that I checked, possibly because Microsoft decided to enable it when the feature was in preview or first deployed to targeted release tenants late last year.
Control over name pronunciation recordings is via the Graph namePronunciationSettings resource type with APIs available to Get and Update the setting controlling whether users see the record and playback buttons. For instance, to get the current setting with the Microsoft Graph PowerShell SDK, run these commands:
$Uri = "https://graph.microsoft.com/beta/admin/people/namePronunciation" Invoke-MgGraphRequest -Uri $Uri -Method Get Name Value ---- ----- isEnabledInOrganization True @odata.context https://graph.microsoft.com/beta/$metadata#admin/people/namepronunciation/$entity
To update the setting to disable name pronunciation recordings, construct a hash table containing the new value and update (patch) the resource:
$Settings = @{} $Settings.Add("isEnabledInOrganization", $false) Invoke-MgGraphRequest -Uri $Uri -Method Patch -Body $Settings
The setting is on or off for a complete tenant. You cannot enable name pronunciation recording and playback for some mailboxes and not for others. This is very similar to the way that the setting controlling the display of personal pronouns (introduced in March 2023) is managed:
Uri = "https://graph.microsoft.com/V1.0/admin/people/pronouns" Invoke-MgGraphRequest -Uri $Uri -Method Get Name Value ---- ----- isEnabledInOrganization True @odata.context https://graph.microsoft.com/beta/$metadata#admin/people/pronouns/$entity
Unfortunately, the API requests to control the name pronunciation settings currently fail with a 404 not found error. I’m sure that this is a transient problem that Microsoft will sort out soon.
Up to Organizations to Decide
Some consider this kind of addition to the user profile to be so much woke fluff. Others consider getting pronouncing names correctly is an essential part of business discourse. Both are entitled to their opinion. It’s good to have the choice within a world where dealing with different cultures and names is a reality for most.
Learn how to exploit the data available to Microsoft 365 tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work.
Microsoft Launches Copilot for All Initiative
New Agent Capabilities for the Free Microsoft 365 Copilot Chat App
Infused with the zealotry of true believers, Microsoft announced Copilot for All on January 15, 2025 to reveal the details of the complicated Copilot renaming they previewed in December. And the new logo, of course.
In a nutshell, Microsoft is creating an “on-ramp” to give Microsoft 365 tenants that haven’t invested in expensive Microsoft 365 Copilot licenses the chance to use agent technology “grounded in Microsoft Graph data.” The idea here is to encourage commercial customers to run a mix of Copilot with some having the full-blown licensed version while others experience with the free-to-use version. Figure 1 shows the relative capabilities of the two Copilot options.

.Lots of Functionality in Microsoft 365 Copilot Chat
The free-to-use Microsoft 365 Copilot Chat app includes a lot of functionality in terms of its ability process user prompts against information available on web sites (providing those sites are indexed by Bing). Recently, Microsoft added features like Copilot pages and the image generator (Figure 2). Microsoft says that limitations exist on the number of images that can be generated daily. I guess I don’t create many images as I haven’t experienced any problems.

The Chat client has enterprise data protection, so data is secure, protected, and actions are audited and captured in compliance records.
Pay-as-you-go Agents
The big news is that customers will be able to create and run custom agents grounded against “work data” on a pay-as-you-go (PAYG) metered basis. PAYG means the tenant must sign up for an Azure subscription with a valid credit card before the agent will run. Agent activity will be charged against the subscription. Grounding against work data means that the agents can interrogate information available in the Microsoft Graph. This includes data stored in Exchange, Teams, SharePoint, and OneDrive plus anything imported into the Graph through a third-party connector. This is where the magic lies because if an organization can import its data into the Graph, agents can reason over that data to create responses to user prompts, providing PAYG is set up for the tenant.
The custom agents are developed with Copilot Studio. I have spent some time working with Copilot Studio to build simple agents over the last few weeks. It’s not a terribly difficult task, but organizations do need to take the time to chart out how they plan to develop, deploy, and manage agents rather than rushing headlong into the brand-new world. Like any software, agents work best when some structure is in place.
The Big Differences between Microsoft 365 Copilot Chat and Microsoft 365 Copilot
Paying for agents to use Graph data does not deliver the full suite of capabilities enjoyed by those who invest in Microsoft 365 Copilot licenses. Figure 1 shows that Microsoft 365 Copilot includes a bunch of personal assistants where Copilot is built into Microsoft 365 apps like Teams, Word, Outlook, PowerPoint, and Excel. Sometimes, as in the case of the automatic document summary generated by Copilot in Word, the help is unwanted, but the personal assistants are very good at helping with other tasks, like summarizing long email threads or recapping Teams meetings.
Microsoft 365 Copilot also includes SharePoint Advanced Management (SAM). However, although Microsoft announced at Ignite 2024 that tenants with Microsoft 365 Copilot licenses would get SAM in early 2025, there’s no trace of these licenses turning up in any tenant that I have access to. License management can be complex and I’m sure that SAM will turn up soon.
Finally, PAYG access to Graph data does not include the semantic index. The index is generated automatically from Graph data in tenants with Microsoft 365 Copilot licenses to create a vector-based index of the relationships of items in the Graph. It’s an untrue urban legend that Microsoft 365 Copilot needs the semantic index to function. The semantic index enhances search results, but it’s not required for the chat app or agents to work.
In Simple Terms, Two Copilot Products
It’s easy to become confused by the naming of different elements within the Microsoft 365 Copilot ecosystem. It boils down to Microsoft offering free (with PAYG capabilities) and expensive Copilot products to Microsoft 365 customers. Microsoft obviously hopes that the free version will act as the on-ramp to full-fledged Copilot. It’s a reasonable tactic. Time will tell if it’s successful.
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering the Microsoft 365 ecosystem.
Final Days for the MSOnline and AzureAD PowerShell Modules
Time Ebbing Away Before AzureAD and MSOnline Module Retirement
On January 13, 2025 Microsoft posted what I am sure they hope will be the last notification about retirement details for the MSOnline and AzureAD PowerShell modules. This has been a long-running saga that’s taken almost as long as the effort to eradicate basic authentication for Exchange Online connection protocols.
The original August 2021 announcement that Microsoft intended to retire the two modules set a date of June 30, 2022. Following customer feedback that the date was too aggressive, Microsoft pushed the date out by a year and then by another nine months. The transition to a new Microsoft 365 licensing platform in mid-2024 forced people to take notice when PowerShell cmdlets stopped being able to assign or update licenses. Microsoft has now set what they say is the final schedule for the retirement (Figure 1).

Final Red Flags for MSOnline Module Retirement
The end of support for both modules is March 30, 2025, just eleven weeks away. But the really interesting note here is the temporary outage tests Microsoft plans for the MSOnline module cmdlets starting on January 20, 2025. What this means is that the MSOnline cmdlets will stop working at least twice between January 20 and February 28. The outages will last between three and eight hours and happen at different times during the day.
The two short outages will be followed in March 2025 by a longer outage. Microsoft hasn’t said how long the longer outage will last. What they have said is that the outages are “To ensure that customers are ready for this retirement of MSOnline PowerShell.”
Customers might view the outages in a different light, especially if the outages stop production scripts running. But to be fair to Microsoft, they have been up front and patient as the process for the retirement of the MSOnline and AzureAD modules unfolded since August 2021. The outages are no more than a final red flag warning to tenants. If you ignore the warnings, be prepared for disruption when the MSOnline module retirement finally completes sometime in April 2025. At that point, all the MSOnline cmdlets will stop working permanently.
AzureAD Module Retirement Follows in Third Quarter
To allow customers to focus on upgrading scripts from the older MSOnline module, Microsoft is targeting the third quarter of 2025 for the final retirement of the AzureAD module. Remember, its cmdlets have already lost their license management capability, so scripts used for other purposes such as reporting accounts and groups need to be upgraded now.
Upgrade to Entra PowerShell or the Microsoft Graph PowerShell SDK
Two upgrade options are available:
- The Entra PowerShell module (still in preview).
- The Microsoft Graph PowerShell SDK.
Microsoft built the Entra module from Graph SDK components wrapped up with some tweaks to make it slightly easier to migrate from MSOnline or AzureAD. Although I respect the opinion of those who advocate for the Entra module as the best migration target, I think this approach is a very short-term tactical step. You might end up being able to migrate scripts, but in doing so you’ll miss the opportunity to master the Graph SDK (and also be able to migrate your scripts).
Missing out on the Graph SDK might not sound like such a big deal, especially when the Entra module is available to handle the immediate need to migrate scripts before the old modules stop working. However, mastering the Graph SDK opens up the opportunity to use PowerShell to interact with many other forms of Microsoft 365 data instead of “just Entra ID.” The same techniques learned to interact with users, groups, and devices can be applied to teams, SharePoint Online sites, OneDrive for Business accounts, Exchange mailboxes, Planner plans and tasks, and so on. Understanding how the Microsoft Graph works is the better strategic choice for the longer term.
Whatever choice you make, time is ebbing away. If you need help to migrate, consider investing in a copy of the Office 365 for IT Pros eBook, which includes the Automating Microsoft 365 with PowerShell eBook (also available separately as an eBook or paperback). The hundreds of practical examples contained in these eBooks include many worked-out solutions for applying the Microsoft Graph PowerShell SDK to solve problems.
Using the SharePoint Pages Graph API
Create and Publish SharePoint Pages API with the Microsoft Graph PowerShell SDK
In April 2024, Microsoft announced the General availability for the Graph API for SharePoint Pages (also in message center notification MC789609 and Microsoft 365 roadmap item 101166). Despite Microsoft proclaiming that they were thrilled with the new API, I never got around to looking at it, largely because other work got in the way.
Given the period since general availability, it is no surprise that cmdlets for the SharePoint Pages API are available in the Microsoft Graph PowerShell SDK. However, some functionality is missing, and the Get- cmdlet to fetch pages for a site doesn’t work very well. Let’s discuss.
Get SharePoint Pages for a Site
The Get-MgSitePageAsSitePage cmdlet retrieves details of the pages for a site. You’ll need to fetch the site identifier for the target site first. The site identifier is not the site URL. A full site identifier looks something like this:
office365itpros.sharepoint.com,8e0a5589-b91d-496e-a5be-3473a75f2fe2,22d7a59d-d93c-498e-a806-6c9475717c88
If you know the URL for a site, you can compute a form of the site identifier that SharePoint will accept to lookup a site like this:
$Uri = "https://office365itpros.sharepoint.com/sites/BlogsAndProjects" $SiteId = $Uri.Split('//')[1].split("/")[0] + ":/sites/" + $Uri.Split('//')[1].split("/")[2] $Site = Get-MgSite -SiteId $SiteId
With the site identifier, you can run Get-MgSitePageAsSitePage. Here’s how to return the set of site pages sorted in date created order:
[array]$Posts = Get-MgSitePageAsSitePage -SiteId $Site.Id -All | Sort-Object {$_.CreatedDateTime -as [datetime]} -Descending
Unfortunately, the cmdlet doesn’t return values for many interesting properties, such as createdByUser. Better results are obtained by using the Graph API request:
$Uri = ("https://graph.microsoft.com/V1.0/sites/{0}/pages/microsoft.graph.sitepage" -f $Site.Id) $Data = Invoke-MgGraphRequest -Uri $Uri -Method Get $Pages = $Data.Value
Create a Page (a News Post) with the SharePoint Pages API
The example of creating a SharePoint page features see a large JSON structure composed of many properties. I wanted to simplify things to create a simple News Post page by running the New-MgSitePage cmdlet.
In PowerShell terms, the JSON structure is represented by a set of hash tables and arrays. It’s usually easier to manipulate the contents of hash tables and arrays programmatically, so that’s what I do here to create a page with a news item about a recent Office 365 for IT Pros article featuring the top five SharePoint features shipped in 2024.
$PostTitle = 'Microsoft Describes Top Five SharePoint Features Shipped in 2024' $PostName = ("News Post {0}.aspx" -f (Get-Date -format 'MMddyyy-HHmm')) $PostImage = "https://i0.wp.com/office365itpros.com/wp-content/uploads/2025/01/Top-Five-SharePoint-Features.png" $PostContent = '<p> An interesting article by Mark Kashman, a Microsoft marketing manager, lists his top five SharePoint features shipped in 2024. Four of the five features involve extra cost. Is the trend of Microsoft charging extra for most new features likely to continue in 2025? The need to generate additional revenues from the Microsoft 365 installed base probably means that this is the new normal.</p><a href="https://office365itpros.com/2025/01/07/top-five-sharepoint-features-2024" target="_blank">Read full article</a>' # The title area $TitleArea = @{} $TitleArea.Add("enableGradientEffect", $true) $TitleArea.Add("imageWebUrl", $PostImage) $TitleArea.Add("layout", "imageAndTitle") $TitleArea.Add("showAuthor",$true) $TitleArea.Add("showPublishedDate", $true) $TitleArea.Add("showTextBlockAboveTitle", $true) $TitleArea.Add("textAboveTitle", $PostTitle) $TitleArea.Add("textAlignment", "center") $TitleArea.Add("imageSourceType", $null) $TitleArea.Add.("title", "News Post") # A news item only needs one web part to publish the content $WebPart1 = @{} $WebPart1.Add("id", "6f9230af-2a98-4952-b205-9ede4f9ef548") $WebPart1.Add("innerHtml", $PostContent) $WebParts = @($WebPart1) # The webpart is in a single column $Column1 = @{} $Column1.Add("id", "1") $Column1.Add("width", "12") $Column1.Add("webparts", $webparts) $Columns = @($Column1) $Section1 = @{} $Section1.Add("layout", "oneColumn") $Section1.Add("id", "1") $Section1.Add("emphasis", "none") $Section1.Add("columns", $Columns) $HorizontalSections = @($Section1) $CanvasLayout = @{} $CanvasLayout.Add("horizontalSections", $HorizontalSections) # Bringing all the creation parameters together $Params = @{} $Params.Add("@odata.type", "#microsoft.graph.sitePage") $Params.Add("name", $PostName) $Params.Add("title", $PostTitle) $Params.Add("pagelayout", "article") $Params.Add("showComments", $true) $Params.Add("showRecommendedPages", $false) $Params.Add("titlearea", $TitleArea) $Params.Add("canvasLayout", $CanvasLayout) $Post = New-MgSitePage -SiteId $site.Id -BodyParameter $Params If ($Post) { Write-Host ("Post {0} successful" -f $PostTitle) }
Update (Promote) a SharePoint Page to be a News Post
After creating a page, we might need to update it. In this case, I update the page to promote it to be a news post so that it will appear in the News section of the site. I also add a description to appear under the title in the card shown for the item in the News section.
The Update-MgSitePage cmdlet reported an “API not found” error, so I used the Graph API request:
$UpdateBody = ‘{ "@odata.type": "#microsoft.graph.sitePage", "promotionKind": "newsPost", "description": "Microsoft Lists Top Five SharePoint Online features shipped in 2024" }’ $Uri = ("https://graph.microsoft.com/V1.0/sites/{0}/pages/{1}/microsoft.graph.sitePage" -f $Site.Id, $Post.Id) $Status = Invoke-MgGraphRequest -Uri $Uri -Method Patch -Body $UpdateBody If ($Status) { Write-Host 'Post Updated'}
Publish the News with the SharePoint Pages API
The news item that’s created is in a draft state. It must be published to make it visible to other site members. I couldn’t find a cmdlet to publish a news item, so I used the Graph API request:
$Uri = ("https://graph.microsoft.com/V1.0/sites/{0}/pages/{1}/microsoft.graph.sitePage/publish" -f $Site.Id, $Post.Id) Invoke-MgGraphRequest -Uri $Uri -Method Post
If an error isn’t reported, we can assume that SharePoint has published the page. Figure 1 shows how the page appears as a news item. I still have some bugs to figure out because the image I selected isn’t visible. There’s always something to do!

Acceptable SharePoint Pages API but Problematic Cmdlets
As far as I can tell, the SharePoint Pages Graph API works pretty well but the Microsoft Graph PowerShell SDK cmdlets generated from the API isn’t in great shape. I admit that some of the issues might be due to my lack of experience with SharePoint pages, but you do expect to be successful when you follow the documentation. I expect things to improve over time. At least, I hope improvement comes…
Need more help to write and manage PowerShell scripts for Microsoft 365? Get a copy of the Automating Microsoft 365 with PowerShell eBook, available standalone or as part of the Office 365 for IT Pros eBook bundle.
How to Post Video Clips in Teams Channel Posts and Replies
One-Minute Video Clips to Liven Up Channel Conversations
In September 2022, Microsoft launched the ability to record and send one-minute video clips in Teams chats. Message center notification MC947832 (3 December 2024, Microsoft 365 roadmap item 383740) reports that users will be able to post the same kind of video clips to channel posts and replies. Targeted release tenants should have this capability now while general availability is due from mid-January with full worldwide deployment to complete by late January 2025.
The new capability is available in Teams desktop (Windows and Mac) and browser clients. The feature is also available in Teams mobile clients, albeit with a slightly different implementation.
Basics of Video Clip for Teams Channel Conversations
The idea behind supporting video clips is that sometimes a brief message delivered in person is a better way to communicate. To record and post a video clip, open the + menu (where options like schedule message and delivery options are found) and select Record video clip to reveal the video capture screen (Figure 1).

Apart from not having a 3-2-1 countdown before recording a video, the implementation for Teams channels is similar to the video capture screen used to record and send a video message with Outlook. I noticed that Teams allows the user to upload a custom background (which is what I use in Figure 1) whereas Outlook is restricted to a set of standard backgrounds. Apart from that, the same options are available to add a script and apply various screen effects to make the video stand out (or distract the viewer).
Once posted, the video clip looks like any other embedded content in a Teams channel conversation (Figure 2).

Although it seems that Teams and Outlook use the same component for video capture, the difference in implementations is interesting. Outlook uses a Loop component to hold the captured video while Teams embeds the video clip like anything else you might paste into a Teams channel conversation. I guess that Teams already has its own way of handling graphic objects posted in channels and Outlook needed some form of container to hold video clips.
For more information about sending video clips with Teams, see the Microsoft documentation.
Controlling Video Clips
Microsoft enables the ability to record and send video clips by default. To disable the feature, update the Teams messaging policies assigned to the user accounts that you want to block and make sure that the AllowVideoMessages setting is $False:
Set-CsTeamsMessagingPolicy -Identity Global -AllowVideoMessages:$False
Where Teams Stores Video Clips
Where Teams stores video clips depends on the client used and the target location:
- Clips posted to channels with the Teams mobile client are stored in the SharePoint Online folder for the channel.
- Clips posted to channels with the Teams desktop and browser clients are stored in a Microsoft video service.
- Clips posted to chats are stored in the same Microsoft video service as used for clips posted to channels.
The storage location for video clips is revealed by using the MFCMAPI utility to examine the compliance records generated for channel posts and replies that contain video clips. You’ll see that the content of the message includes items like this:
<video src="https://eu-api.asm.skype.com/v1/objects/0-neu-d9-37aeb24ad5a76108f93a90b93ef50f88/views/video" itemscope="" itemtype="http://schema.skype.com/AMSVideo" data-duration="PT6.276S" width="1280" height="720"> Video-Clip</video>
The difference in how the clients store clips is likely explained by the reuse of code to send video clips to chats and channel messages by the desktop and browser clients. The mobile clients likely use the Graph APIs to upload the clips they generate to SharePoint Online. The other clients could do the same, but sometimes it’s just simpler to have the same code running for both chats and channel messaging.
If retention policies are in force for Teams channels and chats, the video clips are removed upon the expiration of the message they are embedded.
New Function Likely to be Popular
Given the huge popularity of video messaging popularized in other apps, video clips are likely to be a popular addition for Teams channels. This assertion is idle speculation on my part. Microsoft doesn’t provide any data for the kind of content posted to Teams, but it should be possible to use the Graph APIs to extract some statistics about how many video clips exist.
So much change, all the time. It’s a challenge to stay abreast of all the updates Microsoft makes across the Microsoft 365 ecosystem. Subscribe to the Office 365 for IT Pros eBook to receive monthly insights into what happens, why it happens, and what new features and capabilities mean for your tenant.