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.

PowerShell
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]
                        SLICE

The slices

SLICE is what to look at. Each one is a different question, and most of them are about a single site.

SliceWhat it observes
agentsThe Copilot agents in one site, and the sources each declares
sitesEvery site this identity can enumerate
ownersWho administers one site
modernityHow one site is built: template, branding, publishing
sharingWhat one site permits, and its default link
tenant-sharingWhat the organisation permits, which every site inherits by default
activityWhen a person last changed something on one site
classificationWhat a site records about the kind of content it holds
permissionsEvery visible list on a site, and its inheritance
spfxA 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:

Text
  connected as an application identity
  321 sites enumerated by this identity
  ./evidence/sites/contoso-marketing.json

Nothing 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.

StateMeansExit
completedEverything the slice asked for0
partialUsable evidence, incomplete coverage, with the reason0
failedNo usable artefact1
cancelledYou stopped it, and what was written is kept1

A collection that reached part of an estate produced evidence worth exactly that part:

Text
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:

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:

Text
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.