Taking a Deeper Look into the AuditLog Query API
Update (10 April, 2025): In message center notification MC1052169, Microsoft announced that they have moved the AuditLogQuery API back from production (V1.0) to beta status to address some problems. The API continues to work via the beta endpoint. Microsoft says that they will republish the API to V1.0 after they have fixed the problems.
A year ago, Microsoft launched the preview of the AuditLog Query Graph API. The new API is a Graph implementation of the API used by the Microsoft Purview Audit solution, which switched from on-demand synchronous audit searches to asynchronous background processing several years ago. While it’s more than capable of running audit log searches, the AuditLog Query API isn’t quite as capable in some respects to the options available through the Purview Audit GUI. However, that doesn’t stop it from deserving a deeper look.
Those considering using the AuditLog Query API will probably compare it to the Search-UnifiedAuditLog cmdlet from the Exchange Online management module. Microsoft would like you to use asynchronous searches whenever possible because they believe that this approach makes better use of service resources. The decision about which route to take depends on the solution you’re trying to create. In some cases, processing requirements will dictate that a fast synchronous audit log search is the best approach. In others, code can wait for an audit job to finish running before proceeding to process the job results. Processes run as Azure Automation runbooks are especially good candidates for using the AuditLog Query API.
Running audit jobs with the AuditLog Query API requires consent for the AuditLogsQuery.Read.All permission. This permission allows access to all audit events. Some granular permissions are available to restrict access to specific events, such as only for SharePoint Online. The permissions are available in both delegated and application form. To use a delegated AuditLogs permission in an interactive Microsoft Graph PowerShell SDK session, the signed-in account must be a member of the Purview Audit Manager role group.
Cmdlets for the AuditLog Query API are available in the Microsoft Graph PowerShell SDK. I use straightforward API requests here, but it’s easy to convert the requests into cmdlets if you wish.
Advantages of Using the AuditLog Query API
Using the AuditLog Query API to run audit log searches comes with some advantages. For example, each administrator can submit up to ten audit jobs to run concurrently. One of the jobs can be unfiltered, meaning that it fetches every available audit record for the search period. The other jobs (or all ten jobs) can apply different search criteria to fetch specific sets of audit records. When the jobs finish, their results can be combined or processed separately.
Results for an audit job don’t have to be processed immediately because they remain available online for 30 days after the job finishes. The results can be fetched at any time until Purview removes them automatically.
Another advantage is that audit log searches executed using the AuditLog Query API can return more records than the 50,000 limit for the Search-UnifiedAuditLog cmdlet. The highest number fetched by an audit job I have run is 415,406 records (Figure 1).

The time needed by Purview to process an audit job depends on multiple factors, including the current load on the service, the complexity of the filter used by the job, and the number of records retrieved by the search. Most audit jobs should complete within ten minutes. The same jobs might take between four to forty minutes. On the other hand, I’ve seen an unfiltered query that returned 65,000 records complete in just over four minutes.
Everything depends on what’s happening in the service when an audit job is submitted. Like any search, the more focused and precise the query, the faster the search is likely to finish.
Running an Unfiltered Audit Log Search
The simplest kind of audit query is an unfiltered search that fetches all audit data between two dates. To submit an audit job with the AuditLog Query API, the first task is to construct the query parameters. Because we’re going to run an unfiltered query, the only parameters are the start and end dates. This code creates a name for the audit job and specifies that the start time is 30 days ago, and the end time is an hour in the future:
$AuditJobName = ("Full audit job created at {0}" -f (Get-Date -format 'dd-MMM-yyyy HH:mm')) $EndDate = (Get-Date).AddHours(1) $StartDate = (Get-Date $EndDate).AddDays(-30) $AuditQueryStart = (Get-Date $StartDate -format s) $AuditQueryEnd = (Get-Date $EndDate -format s)
The query parameters are inserted into a hash table to be submitted to the audit job queue:
$AuditQueryParameters = @{} $AuditQueryParameters.Add("@odata.type","#microsoft.graph.security.auditLogQuery") $AuditQueryParameters.Add("displayName", $AuditJobName) $AuditQueryParameters.Add("filterStartDateTime", $AuditQueryStart) $AuditQueryParameters.Add("filterEndDateTime", $AuditQueryEnd)
The next step is to submit the audit job to the queue by posting a request containing the hash table to the Queries endpoint:
$Uri = "https://graph.microsoft.com/beta/security/auditLog/queries" $AuditJob = Invoke-MgGraphRequest -Method POST -Uri $Uri -Body $AuditQueryParameters
If an error isn’t flagged, the variable used to receive the status of the audit job contains the job details:
$AuditJob Name Value ---- ----- operationFilters {} objectIdFilters {} filterStartDateTime 07/02/2025 22:04:24 serviceFilters {} id 21a96394-20f4-4505-9f99-72f2fd960eb0 ipAddressFilters {} recordTypeFilters {} keywordFilter displayName Unfiltered Audit job created at 09-Mar-2025 21:04 @odata.context https://graph.microsoft.com/beta/$metadata#security/auditLog/queries/$entity userPrincipalNameFilters {} filterEndDateTime 09/03/2025 22:04:24 status notStarted administrativeUnitIdFilters {}
The problem with unfiltered searches is that they usually return many thousands of records, most of which are not relevant to the information that’s needed. For this reason, it’s always best to limit the period used for unfiltered searches to give administrators or investigators less information to review.
Running a Filtered Audit Log Search
A filtered audit log search uses one or more optional filters to refine the set of audit events returned by the search. Table 1 lists the audit log search filters supported by the AuditLog Query API.
Filter name | Filters against |
RecordTypeFilters | Search based on categories of audit log records, like ExchangeItem and microsoftPurview. Each category covers multiple operations. |
KeywordFilter | Free text search against non-indexed properties of audit log records. |
ServiceFilter | Search against a workload identity, like Exchange or SharePoint. Takes a single string value. |
OperationFilters | A collection (array) of string values to search for common operations, like FileModified or |
userPrincipalNameFilters | A collection of string values to search against the UserPrincipalName property of audit records. These values should contain the user principal names who caused auditable actions to occur. |
objectIdFilters | A collection containing values to search against the ObjectId property in audit records. For SharePoint Online and OneDrive for Business, the full path name to files (supports * wildcard). For Exchange administrative logging, the name of the object modified by a cmdlet. |
administrativeUnitIdFilters | A collection of Entra ID administrative unit identifiers to search against the tags in audit log records. |
Table 1: Optional filters for audit log queries
Filters can be combined to focus in on specific data. For example, this audit log query searches for audit events captured for three different operations performed against files in two different SharePoint Online sites by three different users.
$AuditJobName = ("Audit job created at {0}" -f (Get-Date -format 'dd-MMM-yyyy HH:mm')) $EndDate = (Get-Date).AddHours(1) $StartDate = (Get-Date $EndDate).AddDays(-60) $AuditQueryStart = (Get-Date $StartDate -format s) $AuditQueryEnd = (Get-Date $EndDate -format s) [array]$AuditOperationFilters = "FileModified", "FileDeleted", "FileUploaded" [array]$AuditobjectIdFilters = "https://office365itpros.sharepoint.com/sites/productcreation/*", "https://office365itpros.sharepoint.com/sites/Office365Adoption/*" [array]$AuditUserPrincipalNameFilters = "Ken.Bowers@office365itpros.com", "Lotte.Vetler@office365itpros.com", "tony.redmond@office365itpros.com" $AuditQueryParameters = @{} $AuditQueryParameters.Add("@odata.type","#microsoft.graph.security.auditLogQuery") $AuditQueryParameters.Add("displayName", $AuditJobName) $AuditQueryParameters.Add("OperationFilters", $AuditOperationFilters) $AuditQueryParameters.Add("filterStartDateTime", $AuditQueryStart) $AuditQueryParameters.Add("filterEndDateTime", $AuditQueryEnd) $AuditQueryParameters.Add("userPrincipalNameFilters", $AuditUserPrincipalNameFilters) $AuditQueryParameters.Add("objectIdFilters", $AuditobjectIdFilters) $Uri = "https://graph.microsoft.com/beta/security/auditLog/queries" $AuditJob = Invoke-MgGraphRequest -Method POST -Uri $Uri -Body $AuditQueryParameters
Monitoring Audit Job Progress
The status of an audit job tells you what state it is in. The initial post-submission status is “not started.” When Purview begins to process the query, the job status changes to “running.” To keep an eye on its progress, we can run a loop to check the status of the job until it finally completes, indicated by its status changing from “running” to “succeeded”:
[int]$i = 1 [int]$SleepSeconds = 20 $SearchFinished = $false; [int]$SecondsElapsed = 20 Write-Host "Checking audit query status..." Start-Sleep -Seconds 30 $Uri = ("https://graph.microsoft.com/beta/security/auditLog/queries/{0}" -f $AuditJob.id) $AuditQueryStatus = Invoke-MgGraphRequest -Uri $Uri -Method Get While ($SearchFinished -eq $false) { $i++ Write-Host ("Waiting for audit search to complete. Check {0} after {1} seconds. Current state {2}" -f $i, $SecondsElapsed, $AuditQueryStatus.status) If ($AuditQueryStatus.status -eq 'succeeded') { $SearchFinished = $true } Else { Start-Sleep -Seconds $SleepSeconds $SecondsElapsed = $SecondsElapsed + $SleepSeconds $AuditQueryStatus = Invoke-MgGraphRequest -Uri $Uri -Method Get } }
The code shown above doesn’t cope with status values other than “succeeded”, so it would need to be bullet-proofed for production use to handle status values such as failed or cancelled.
Fetching Audit Job Results
To fetch the records found by an audit job, post a Get request to the queries endpoint for the audit job. A request can fetch a maximum of 999 records, so pagination is often needed to fetch the remaining records. This code shows how to fetch all the records.
$AuditRecords = [System.Collections.Generic.List[string]]::new() $Uri = ("https://graph.microsoft.com/beta/security/auditLog/queries/{0}/records?`$top=999" -f $AuditJob.Id) [array]$AuditSearchRecords = Invoke-MgGraphRequest -Uri $Uri -Method GET [array]$AuditRecords = $AuditSearchRecords.value $NextLink = $AuditSearchRecords.'@Odata.NextLink' While ($null -ne $NextLink) { $AuditSearchRecords = $null [array]$AuditSearchRecords = Invoke-MgGraphRequest -Uri $NextLink -Method GET $AuditRecords += $AuditSearchRecords.value Write-Host ("{0} audit records fetched so far..." -f $AuditRecords.count) $NextLink = $AuditSearchRecords.'@odata.NextLink' } Write-Host ("Audit query {0} returned {1} records" -f $AuditJobName, $AuditRecords.Count)
The code uses a generic string list instead of an array to avoid the performance penalty incurred if an array needs to be rebuilt frequently to add records. A regular array is fine for jobs that return less than a couple of thousand audit records.
After fetching all the audit records, you can process them as required. For example, here’s how to get a quick insight into the auditable actions included in the set of results for an audit log query.
$AuditRecords | Group-Object Operation -NoElement | Sort-Object Count -Descending | Format-Table -AutoSize
Viewing Audit Job Details
When an audit job is submitted, it joins the same queue that’s visible in the Purview Audit solution, which means that you can follow the progress of an audit job through the Purview Audit UI (Figure 2). The advantage of using Purview Audit is that the GUI includes additional information about audit jobs, including the start time of the job, who submitted the job, how long the job has been running for, and how many audit records have been found so far. This information isn’t available through the AuditLog Query API, which is limited to a simple output of the audit jobs on the queue.

The Purview Audit GUI can report incorrect total results numbers for jobs that retrieve large numbers of audit records (more than 50,000). For instance, the third job shown in Figure 2 reports that it has found 172,951 records. When an API request fetched the records, the total retrieved was 415,406 (Figure 1). Treat the number shown in Purview as an estimate rather than an accurate value.
The queue holds all audit jobs for the last 30 days, including those that are running and completed. The most valuable information displayed by listing audit log queries is the identifier, which can be used to retrieve the records found by the job. For example:
$Uri = "https://graph.microsoft.com/beta/security/auditLog/queries" $Data = Invoke-MgGraphRequest -Uri $Uri -Method GET If ($Data) { Write-Output "Audit Jobs found" $Data.Value | ForEach-Object { Write-Host ("{0} {1}" -f $_.id, $_.displayName) } } Else { Write-Output "No audit jobs found" } Audit Jobs found b19c7368-43e4-4f34-b693-9713549dbb80 Full audit job created at 09-Mar-2025 18:54 713ac6f4-bb07-42af-b8b3-2f539ef5e9ea Big Audit job created at 09-Mar-2025 18:15
To fetch the records for an audit log search, all you need to do is add the job identifier to the query to the list audit records API:
$AuditJob = "b19c7368-43e4-4f34-b693-9713549dbb80" $Uri = ("https://graph.microsoft.com/beta/security/auditLog/queries/{0}/records?`$top=999" -f $AuditJob) [array]$AuditSearchRecords = Invoke-MgGraphRequest -Uri $Uri -Method GET
An Option Worth Considering
The AuditLog Query API is worth considering if you need to fetch audit data, especially when time is not of the essence. Processing of the AuditData property is still necessary to extract details about the actions captured in the records. The property is provided as a hash table and doesn’t need to be converted from JSON, as is the case with data fetched by Search-UnifiedAuditLog. Overall, the AuditLog Query API is a welcome addition to the administrator’s toolbox.