Playbook: new-cmdlet
The conventions a cmdlet in this repository must satisfy. A reference — read it before writing or
reviewing cmdlet code. For the step-by-step generation procedure see
cmdlet-scaffolder; for language-level rules see
dotnet-standards.
Pick the base class first
It determines the connection, the resource, the permission flavour and which helpers exist.
| Base class | Use for | Gives you |
|---|---|---|
PnPWebCmdlet |
Operations on the connected site/web | CurrentWeb, ClientContext |
PnPWebRetrievalsCmdlet<T> |
The above, returning objects with selectable properties | RetrievalExpressions, -Includes |
PnPSharePointCmdlet |
General SharePoint, no web context needed | ClientContext |
PnPSharePointOnlineAdminCmdlet |
Tenant admin operations | Tenant, AdminContext |
PnPGraphCmdlet |
Microsoft Graph | Connection, AccessToken, GraphRequestHelper |
PnPAzureManagementApiCmdlet |
Azure Management API | |
PnPOfficeManagementApiCmdlet |
Office 365 Management API | |
PnPGcsCmdlet, PnPTasksCmdlet |
Consumer storage, Planner/Tasks | |
PnPConnectedCmdlet |
Needs a connection, none of the above | |
BasePSCmdlet |
No connection at all |
Choosing wrong is a real defect: a tenant-admin operation on PnPWebCmdlet will use the wrong
context and fail confusingly in someone's tenant.
Shape
using System.Management.Automation;
using PnP.PowerShell.Commands.Attributes;
using PnP.PowerShell.Commands.Base;
using PnP.PowerShell.Commands.Base.PipeBinds;
namespace PnP.PowerShell.Commands.FeatureArea
{
[Cmdlet(VerbsCommon.Get, "PnPSomething")]
[OutputType(typeof(SomeType))]
[RequiredApiDelegatedPermissions("sharepoint/AllSites.Read")]
[RequiredApiApplicationPermissions("sharepoint/Sites.Read.All")]
public class GetSomething : PnPWebRetrievalsCmdlet<SomeType>
{
[Parameter(Mandatory = false, ValueFromPipeline = true, Position = 0)]
[ValidateNotNull]
public SomePipeBind Identity { get; set; }
protected override void ExecuteCmdlet()
{
// ...
WriteObject(result, true);
}
}
}
- Namespace mirrors the folder:
src/Commands/Lists/→PnP.PowerShell.Commands.Lists. - Override
ExecuteCmdlet(), neverProcessRecord()— the base class owns connection handling and error translation. - Renaming an existing cmdlet requires
[Alias("Old-PnPName")]for backward compatibility.
Permission attributes
"<resource>/<scope>". Multiple attributes are ORed; multiple scopes inside one attribute are ANDed.
[RequiredApiDelegatedPermissions("sharepoint/AllSites.FullControl", "sharepoint/User.ReadWrite.All")]
[RequiredApiApplicationPermissions("sharepoint/Sites.FullControl.All", "sharepoint/User.ReadWrite.All")]
Also available: RequiredApiDelegatedOrApplicationPermissions,
ApiNotAvailableUnderDelegatedPermissions, ApiNotAvailableUnderApplicationPermissions,
ApiPermissionsNotRequired, ApiPermissionsDependOnResource.
An unrecognised resource prefix is silently treated as SharePoint.
TokenHandler.DefineResourceTypeFromAudience defaults every audience it does not recognise to
SharePoint (only blank input becomes Unknown), so "garph/Group.Read.All" declares a SharePoint
Group.Read.All rather than failing. A string with no / at all fails the regex and is dropped.
Declare least privilege, and verify the scope against Microsoft Learn rather than from memory. See
permissions-auditor.
Common patterns
SharePoint retrievals
DefaultRetrievalExpressions = [l => l.Id, l => l.Title, l => l.RootFolder.ServerRelativeUrl];
var list = Identity.GetList(CurrentWeb);
list?.EnsureProperties(RetrievalExpressions);
WriteObject(list);
Graph
var result = GraphRequestHelper.GetResultCollection<SomeType>(this, "v1.0/groups?$select=id,displayName");
WriteObject(result, true);
Use GetResultCollection for collections — it follows @odata.nextLink. Get returns the first
page only. Match the signature of a neighbouring cmdlet in the same folder; these helpers have
evolved and older call shapes still exist in the tree.
CSOM — always ExecuteQueryRetry(), never ExecuteQuery().
Definition of done
A cmdlet is not finished until all of these exist in the same PR:
- Class — correct base class,
[Cmdlet],[OutputType], permission attributes,[Alias]if renaming. - Parameters — PipeBinds, validation attributes, deliberate
Mandatory/Position/pipeline binding;ParameterSpecifiedwhere a default is meaningful. documentation/<Verb-PnPNoun>.md— full platyPS page: front matter,## SYNOPSIS— carrying a Required Permissions block only when the cmdlet declares API permissions; a cmdlet markedApiPermissionsNotRequiredmust not have one, and none of the ten such pages does —## SYNTAX(one block per parameter set),## DESCRIPTION,## EXAMPLES(at least one,```powershellfenced, each with a sentence of explanation),## PARAMETERS**alphabetical, YAML blocks only**, including the standard-Connectionand-Verbosesections, then## RELATED LINKS. Copy the structure from a sibling page.CHANGELOG.md— a line under[Current nightly]→Added/Changed/Fixed/Removed, naming the cmdlets in backticks and linking the PR or issue.- Build clean —
dotnet build src/PnP.PowerShell.sln, no new warnings.
Removing a cmdlet means deleting its documentation page too.
"In the same PR" means the same working tree. Never commit, push, or open the PR yourself — see Human in the loop.
What an agent cannot do here
You cannot run the cmdlet. Anything calling an API you have not invoked is a draft: say which API shapes, response fields and permission scopes you inferred rather than verified, so a maintainer knows exactly what to check against a tenant.