How to inventory SharePoint agents without pretending you saw the whole tenant
How many agents exist in my organisation, and how would I know?
30-second answer
Connect-PnPOnline -Url https://contoso-admin.sharepoint.com -Interactive -ClientId $clientId
$sites = Get-PnPTenantSite
"$($sites.Count) sites enumerated by this identity"
Start with the denominator, not the count. An agent inventory is a number over a population, and the population is whatever your identity could see.
What this proves
How many agents were found, in how many sites, by one identity, at one moment. Every one of those four qualifiers is load bearing.
What it does not prove
- How many agents exist. Sites this identity cannot open contribute nothing, and their number is not knowable from inside the run.
- That zero means none. A site with no agents and a site you could not read return the same empty result. Treating the second as the first is inferring success from an absence.
- That the number is still true. Anyone who can create a file in a site can create an agent in it, so this is a photograph rather than a state.
PowerShell
$clientId = '00000000-0000-0000-0000-000000000000'
Connect-PnPOnline -Url https://contoso-admin.sharepoint.com -Interactive -ClientId $clientId
$sites = Get-PnPTenantSite
$read = 0
$refused = 0
$agents = @()
foreach ($site in $sites) {
try {
Connect-PnPOnline -Url $site.Url -Interactive -ClientId $clientId
$agents += Get-PnPCopilotAgent
$read++
}
catch {
$refused++
}
}
[pscustomobject]@{
SitesEnumerated = $sites.Count
SitesRead = $read
SitesRefused = $refused
AgentsFound = $agents.Count
}
The catch is not defensive programming. A refusal is a result, and a
script that swallows it silently reports a smaller number as if it were a
smaller reality.
Example output
SitesEnumerated SitesRead SitesRefused AgentsFound
--------------- --------- ------------ -----------
412 389 23 17
Seventeen agents across three hundred and eighty-nine sites, with twenty-three sites unread and an unknown number never enumerated. That sentence is the finding. The number seventeen on its own is not.
Explanation
Microsoft gives two ways to find agents, and both have the same boundary written into them.
The first is search: agents are .agent files, so searching for *.agent
finds them, and the documentation states that "the results only include agent
files that you have permission to access". Submit-PnPSearchQuery reaches the
same index.
The second is the SharePoint Advanced Management agent insights report, which a SharePoint administrator generates. The documentation is candid about what that costs and gives: it needs the licence, and it notes that the output "might include agent files that the site owners and site admins don't have access to". A higher privilege sees more, and it is still a specific privilege seeing a specific amount.
There is no view from nowhere. Every path to this inventory is a path through an identity, which is why the honest artefact is not a count but a count with its denominator and its refusals attached.
That is also why the enumeration above deliberately does not filter. Filtering before counting hides the population, and the population is the part that makes the count mean something.
Production considerations
- This is a read. Nothing here creates, edits or removes an agent, and the administrative report cmdlets are not on this page precisely because the one that produces a report writes tenant state.
- Record the refusals with the result, in the same object. A coverage figure kept in a separate file is a coverage figure nobody reads.
- A delegated run and an administrative run answer different questions. Neither is the true one. Say which was used.
- Re-running it later is the point. A single inventory tells you what was there; two tell you what changed, and change is where the governance value in this surface actually is.
