30-second answer
collect runs one collector against one tenant and writes evidence documents.
It evaluates nothing. Reading and deciding are separate commands because the
evidence has to be inspectable before anybody argues about what it means.
m365-governance collect [-h] --client-id CLIENT_ID --output OUTPUT
[--site-url SITE_URL] [--tenant-url TENANT_URL]
[--device-login] [--count-unique-scopes]
[--dry-run]
SLICEThe slices
SLICE is what to look at. Each one is a different question, and most of them
are about a single site.
| Slice | What it observes |
|---|---|
agents | The Copilot agents in one site, and the sources each declares |
sites | Every site this identity can enumerate |
owners | Who administers one site |
modernity | How one site is built: template, branding, publishing |
sharing | What one site permits, and its default link |
tenant-sharing | What the organisation permits, which every site inherits by default |
activity | When a person last changed something on one site |
classification | What a site records about the kind of content it holds |
permissions | Every visible list on a site, and its inheritance |
spfx | A site's app catalog: which solutions lag their version |
Options
--client-id is required, and the help text says why: PnP.PowerShell has
shipped no application of its own since 2.12.0, so you supply an Entra ID app
registration.
--site-url and --tenant-url. Which one a slice needs is a property
of the slice, and the command says so rather than failing obscurely. Ask for
sharing without --tenant-url and it tells you that sharing settings are a
tenant property about a site. --tenant-url is the admin centre,
https://<tenant>-admin.sharepoint.com.
--device-login authenticates with a device code, for hosts with no
browser.
--count-unique-scopes applies to permissions only, and walks every item
of every list. It is off by default because it is the expensive path.
--dry-run prints the command and reaches no tenant.
It reports while it runs
A collection reaches a network. Against a large tenant it takes minutes, and each line the collector writes appears as it is written:
connected as an application identity
321 sites enumerated by this identity
./evidence/sites/contoso-marketing.jsonNothing is held back until the end. That matters for one specific reason: the only thing distinguishing a working collection from a hung one is what it has told you so far.
How a collection ends
In one of four words, and partial is not one of the failures.
| State | Means | Exit |
|---|---|---|
completed | Everything the slice asked for | 0 |
partial | Usable evidence, incomplete coverage, with the reason | 0 |
failed | No usable artefact | 1 |
cancelled | You stopped it, and what was written is kept | 1 |
A collection that reached part of an estate produced evidence worth exactly that part:
2 evidence documents in 41.3s, and the collection is PARTIAL.
contoso-marketing.json: owners not read (owners: permission-denied — the
identity is not a site collection administrator)
Evaluating this is valid. Where the gap could change an answer, a rule returns
`unknown` rather than a pass.cancelled is never guessed. A collector killed by the network and one you
stopped with Ctrl-C exit the same way, so only the caller knows which happened.
What was already written stays, and says what it covers.
Nothing is written to the tenant on any of these paths. A collector has no write path, and CI proves it on every release by parsing every file in the collector tree.
It writes down what it did
Beside the evidence, collection-manifest.json:
{
"state": "partial",
"because": [
"2 evidence documents were written",
"contoso-marketing.json: owners not read (owners: permission-denied — ...)"
],
"coverage": {
"requested": ["owners", "sites"],
"completed": ["sites"],
"unavailable": { "owners": { "state": "permission-denied", "detail": "..." } }
}
}Written on every path, including the failure that produced no evidence at all, which is precisely the case you cannot reconstruct from an empty directory later.
It is not evidence and is never evaluated as one. Full contract in The collection manifest.
What it tells you next
On every path that produced something it names the profile to evaluate with, rather than leaving you to guess:
Evaluate with: m365-governance evaluate --profile profiles/<slice>.yaml --evidence <output>That pairing matters. Evaluating a collection against the wrong profile
produces a wall of unknown for facts nobody asked to be collected.
Exit codes
0 collected, whether completed or partial. 2 refused before reaching
the tenant: a missing argument, or a preflight problem with the local
environment. 1 the collection failed or was cancelled.
0 is not "everything was read". Read the state, not the exit code: that
collapse is the reason the states exist.