Skip to content

Add Microsoft Search vertical, result type, and site connection cmdlets#5242

Open
wobba wants to merge 7 commits intodevfrom
wobba-search-vertical-support
Open

Add Microsoft Search vertical, result type, and site connection cmdlets#5242
wobba wants to merge 7 commits intodevfrom
wobba-search-vertical-support

Conversation

@wobba
Copy link
Contributor

@wobba wobba commented Feb 13, 2026

Before creating a pull request, make sure that you have read the contribution file located at

https://github.com/pnp/powerShell/blob/dev/CONTRIBUTING.md

Type

  • Bug Fix
  • New Feature
  • Sample

Related Issues?

N/A - New feature contribution

What is in this Pull Request ?

Adds a full set of cmdlets for managing Microsoft Search verticals, result types, and site connections via the Graph Connector Service (GCS) API at gcs.office.com.

⚠️ Unsupported API Notice: The GCS API is an internal Microsoft API that is not publicly documented or officially supported. These cmdlets interact with the same endpoints used by the SharePoint admin center and Microsoft Search admin UI. While functional, the API may change without notice. A one-time WriteWarning is emitted per session when any GCS cmdlet runs, and each cmdlet's documentation includes a [!WARNING] note.

API & Permissions

These cmdlets use the Graph Connector Service (GCS) API, a first-party Microsoft API registered under app ID 56c1da01-2129-48f7-9355-af6d59d42766. The required delegated permission is:

  • ExternalConnection.ReadWrite.All (scope ID d44774bd-e26c-43b1-996d-51bb90a9078e) from the GCS API

This is NOT the standard Microsoft Graph ExternalConnection.ReadWrite.All permission — it's from the GCS-specific API registration. Users must add this permission to their Entra app registration:

az ad app permission add --id <app-id> --api 56c1da01-2129-48f7-9355-af6d59d42766 --api-permissions d44774bd-e26c-43b1-996d-51bb90a9078e=Scope
az ad app permission admin-consent --id <app-id>

New Cmdlets

Search Verticals:

  • Get-PnPSearchVertical – Retrieve search verticals (site or organization scope)
  • New-PnPSearchVertical – Create custom search verticals with SharePoint or external connector content sources
  • Set-PnPSearchVertical – Update vertical properties (display name, enabled state, query template, connector results)
  • Remove-PnPSearchVertical – Remove search verticals with -Force/-WhatIf/-Confirm support
  • Set-PnPSearchVerticalOrder – Reorder custom verticals (delete-and-recreate strategy with verification)

Search Result Types:

  • Get-PnPSearchResultType – Retrieve result types by logical ID or name
  • New-PnPSearchResultType – Create result types with rules, Adaptive Card v1.3 templates, and display properties
  • Set-PnPSearchResultType – Update result types including priority resequencing
  • Remove-PnPSearchResultType – Remove result types with -Force/-WhatIf/-Confirm support
  • New-PnPSearchResultTypeRule – Create rule objects for result type matching (7 operator types)

Site Connections:

  • Get-PnPSearchSiteConnection – Retrieve external connector site connections available for search verticals

Infrastructure

  • PnPGcsCmdlet base class with shared GCS API helpers: URL builders, retry logic with exponential backoff, PostAndGet<T> pattern (handles GCS empty-body responses), connector property validation, Adaptive Card version validation, site ID caching per connection URL
  • One-time unsupported API disclaimer via WriteWarning in PnPGcsCmdlet.BeginProcessing()
  • GCS token support via ResourceTypeName.GraphConnectorService and TokenHandler
  • Model classes: SearchVertical, SearchResultType, SearchSiteConnection with full JSON serialization
  • Enums: SearchVerticalScope (Site/Organization), SearchResultTypeRuleOperatorType (7 operators)
  • KnownSharePointManagedProperties – 430+ properties for -Validate validation against SharePoint schema
  • PowerShell formatting via PnP.PowerShell.Format.ps1xml for table output of verticals, result types, and connections

Key Design Decisions

  • -Enabled is bool (not SwitchParameter) to allow $true/$false toggle for enable/disable
  • -Payload parameter set on New/Set cmdlets for full control (clone-and-modify workflows)
  • -Validate switch on result type cmdlets fetches connector schema and validates rule/display properties
  • -IncludeConnectorResults only applies to built-in verticals (SITEALL/ALL); warns and ignores for custom verticals
  • SupportsShouldProcess on Remove cmdlets with -Force to skip confirmation
  • Site ID cached in static dictionary keyed by connection URL (single CSOM roundtrip per site per session)

Documentation

Full documentation for all 11 new cmdlets following existing PnP PowerShell conventions, including prerequisites, multiple examples, and parameter descriptions. Each GCS cmdlet doc includes an unsupported API warning and the specific permission/app registration instructions. Also includes minor documentation updates for existing Power Platform and Audit Log cmdlets.

wobba and others added 7 commits February 13, 2026 12:48
Standardize Required Permissions formatting across all four
SearchExternalConnection docs: remove "One of", fix tab characters,
use "or" separator. Add cross-reference to Get-PnPSearchSiteConnection
from Get-PnPSearchExternalConnection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds PnPGcsCmdlet base class with GCS API helpers (retry logic, URL
builders, shared validation, site ID caching via EnsureProperties),
token handler support for gcs.office.com, SearchVertical/SearchResultType/
SearchSiteConnection models, enums, format views, and a known SharePoint
managed properties reference for validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds Get/New/Set/Remove-PnPSearchVertical for managing custom search
verticals with support for SharePoint and external connector content
sources. Adds Set-PnPSearchVerticalOrder for reordering custom verticals
via delete/recreate. Adds Get-PnPSearchSiteConnection for retrieving
available connector site connections. Remove cmdlets support
-Force/-WhatIf/-Confirm.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds Get/New/Set/Remove-PnPSearchResultType for managing search result
types with connector and SharePoint content source support. Adds
New-PnPSearchResultTypeRule helper for building rule objects. Includes
-Validate switch for schema validation against connector properties,
SharePoint managed property checking, and Adaptive Card v1.3 validation.
Set supports -Priority with automatic resequencing. Remove cmdlets
support -Force/-WhatIf/-Confirm.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds help docs for all 11 new search cmdlets: Get/New/Set/Remove
vertical, Set vertical order, Get site connection, Get/New/Set/Remove
result type, and New result type rule. Includes prerequisites, examples,
parameter descriptions, and related links.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add GCS API permission prerequisites and token handler references
to Get-PnPFlow, Get-PnPPowerApp, Get-PnPPowerPlatformEnvironment,
and Get-PnPUnifiedAuditLog docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The GCS API is an internal Microsoft API that is not publicly documented
or officially supported. Added a one-time WriteWarning in PnPGcsCmdlet
base class and a WARNING note in all 10 GCS cmdlet docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Microsoft Search administration surface to PnP PowerShell by introducing cmdlets that manage Search verticals, modern result types, and site connections through the internal Graph Connector Service (GCS) API (gcs.office.com), plus shared infrastructure, models, formatting, and documentation updates.

Changes:

  • Introduces 11 new cmdlets for search verticals, result types, and site connections using a new PnPGcsCmdlet base class with retry/validation helpers.
  • Adds new Microsoft Search model types/enums and token/audience support for the GCS resource.
  • Adds PowerShell formatting views and comprehensive cmdlet markdown documentation (plus small doc touch-ups to existing cmdlets).

Reviewed changes

Copilot reviewed 41 out of 41 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/Commands/Search/SetSearchVerticalOrder.cs Implements reorder cmdlet using delete/recreate strategy with verification loops
src/Commands/Search/SetSearchVertical.cs Implements update cmdlet for verticals (properties vs payload parameter sets)
src/Commands/Search/SetSearchResultType.cs Implements update cmdlet for modern result types incl. priority resequencing
src/Commands/Search/RemoveSearchVertical.cs Implements remove cmdlet for verticals with -Force and ShouldProcess
src/Commands/Search/RemoveSearchResultType.cs Implements remove cmdlet for result types with -Force and ShouldProcess
src/Commands/Search/NewSearchVertical.cs Implements create cmdlet for custom verticals incl. content source resolution
src/Commands/Search/NewSearchResultTypeRule.cs Adds helper cmdlet to construct result-type rule objects
src/Commands/Search/NewSearchResultType.cs Implements create cmdlet for modern result types incl. optional validation
src/Commands/Search/GetSearchVertical.cs Implements get cmdlet for verticals (single or list)
src/Commands/Search/GetSearchSiteConnection.cs Implements get cmdlet for site connections (single or list)
src/Commands/Search/GetSearchResultType.cs Implements get cmdlet for result types (single by id/name or list)
src/Commands/Model/Graph/MicrosoftSearch/SearchVertical.cs Adds vertical payload/collection models w/ extension-data preservation
src/Commands/Model/Graph/MicrosoftSearch/SearchSiteConnection.cs Adds site connection + schema/property wrapper models
src/Commands/Model/Graph/MicrosoftSearch/SearchResultType.cs Adds result type payload/collection models w/ extension-data preservation
src/Commands/Model/Graph/MicrosoftSearch/KnownSharePointManagedProperties.cs Adds managed property allowlist used for validation warnings
src/Commands/Enums/SearchVerticalScope.cs Adds scope enum (Site vs Organization) used across new cmdlets
src/Commands/Enums/SearchResultTypeRuleOperatorType.cs Adds operator enum for rule construction helper cmdlet
src/Commands/Enums/ResourceTypeName.cs Adds Gcs resource type for access token retrieval/mapping
src/Commands/Base/TokenHandler.cs Maps gcs.office.com audience to new ResourceTypeName.Gcs
src/Commands/Base/PnPGcsCmdlet.cs New base class: disclaimer, headers, URL builders, retry and validation utilities
src/Commands/Base/GetAccessToken.cs Allows Get-PnPAccessToken -ResourceTypeName Gcs
resources/PnP.PowerShell.Format.ps1xml Adds table views for SearchVertical and SearchResultType
documentation/Set-PnPSearchVerticalOrder.md New cmdlet documentation (reorder behavior + warnings/prereqs)
documentation/Set-PnPSearchVertical.md New cmdlet documentation (update verticals)
documentation/Set-PnPSearchResultType.md New cmdlet documentation (update result types)
documentation/Set-PnPSearchExternalConnection.md Minor formatting/wording fix for permissions section
documentation/Remove-PnPSearchVertical.md New cmdlet documentation (remove verticals)
documentation/Remove-PnPSearchResultType.md New cmdlet documentation (remove result types)
documentation/Remove-PnPSearchExternalConnection.md Minor formatting/wording fix for permissions section
documentation/New-PnPSearchVertical.md New cmdlet documentation (create verticals)
documentation/New-PnPSearchResultTypeRule.md New cmdlet documentation (rule helper)
documentation/New-PnPSearchResultType.md New cmdlet documentation (create result types)
documentation/New-PnPSearchExternalConnection.md Minor formatting/wording fix for permissions section
documentation/Get-PnPUnifiedAuditLog.md Adds prerequisites section with permission/app id guidance
documentation/Get-PnPSearchVertical.md New cmdlet documentation (get verticals)
documentation/Get-PnPSearchSiteConnection.md New cmdlet documentation (get site connections)
documentation/Get-PnPSearchResultType.md New cmdlet documentation (get result types)
documentation/Get-PnPSearchExternalConnection.md Minor permissions formatting + adds related link to site connections
documentation/Get-PnPPowerPlatformEnvironment.md Adds prerequisites section (Azure Service Management API permission)
documentation/Get-PnPPowerApp.md Adds prerequisites section (Azure Service Management API permission)
documentation/Get-PnPFlow.md Adds prerequisites section (Azure Service Management API + related notes)

Comment on lines +253 to +264
try
{
GcsRequestHelper.Get<SearchVertical>(url, additionalHeaders: headers);
// Still exists - keep waiting
WriteVerbose($"Vertical '{logicalId}' still exists, waiting... ({i + 1}/{MaxVerifyAttempts})");
}
catch
{
// Error means the vertical is gone
WriteVerbose($"Verified vertical '{logicalId}' deleted");
return;
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WaitUntilDeleted() interprets any exception from the GET as confirmation of deletion. This can incorrectly “verify” deletion on transient server errors. Prefer checking for a specific NotFound/404 response (e.g., GraphException with StatusCode == NotFound) and continue waiting/retrying for other error codes.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +33
/// <summary>
/// Static cache of site IDs keyed by connection URL, persists across cmdlet invocations within the same session.
/// </summary>
private static readonly Dictionary<string, Guid> _siteIdCache = new Dictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Tracks whether the unsupported API disclaimer has been shown in this session.
/// </summary>
private static bool _apiDisclaimerShown;

Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_siteIdCache is a static Dictionary accessed without synchronization. If cmdlets run concurrently across runspaces, this can lead to race conditions or Dictionary corruption. Consider using ConcurrentDictionary or locking around reads/writes (same applies to _apiDisclaimerShown if it can be accessed concurrently).

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +99
if (ParameterSpecified(nameof(Priority)))
{
var desiredPriority = Math.Max(1, Priority.Value);
ResequenceResultTypePriorities(logicalId, desiredPriority, siteId, headers);
payload.Priority = desiredPriority;
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

desiredPriority is only clamped to a minimum of 1, but not to the maximum valid position. ResequenceResultTypePriorities() inserts at min(desiredPriority-1, others.Count) (end of list), yet the target payload is still set to the original (possibly too-large) priority, which can leave gaps/out-of-range priorities. Consider clamping desiredPriority to the list size (or using insertIdx + 1) before setting payload.Priority.

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +116
DisplayTemplate = DisplayTemplate ?? GetDefaultDisplayTemplate(),
DisplayProperties = displayProps,
DisplaySampleData = "",
LastModifiedBy = ""
};
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LastModifiedBy is described/treated elsewhere as a server-only field to omit on write (set to null). Here it’s initialized to an empty string, which will still be serialized and sent to the API. Consider setting it to null (or not setting it at all) so it’s excluded from the request body.

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +157
DeleteWithRetry(verticalUrl, headers, verifySuccess: () =>
{
// Check if the vertical is actually gone despite the 500 error
try
{
GcsRequestHelper.Get<SearchVertical>(verticalUrl, additionalHeaders: headers);
return false; // Still exists
}
catch
{
return true; // Gone — delete succeeded
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delete verifySuccess callback treats any exception from the follow-up GET as “gone”. This can produce false positives (e.g., transient 500/timeout/permission issues) and the cmdlet may proceed while the vertical still exists. Consider only treating a 404/NotFound as deleted and rethrowing/returning false for other failures so retries/verification behave correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +97
return new Dictionary<string, string>
{
{ "x-siteurl", Connection.Url }
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetGcsHeaders() unconditionally adds x-siteurl with Connection.Url. PnPConnection.Url can be null for some connection types, which would lead to sending an invalid header (or failing later in a less obvious way). Consider validating Connection?.Url and throwing a clear error if no site URL is available (especially for org-scope cmdlets that otherwise wouldn’t call GetCurrentSiteId()).

Suggested change
return new Dictionary<string, string>
{
{ "x-siteurl", Connection.Url }
var url = Connection?.Url;
if (string.IsNullOrEmpty(url))
{
throw new PSInvalidOperationException("No site URL available on the current connection. Connect to a specific SharePoint site before invoking this cmdlet.");
}
return new Dictionary<string, string>
{
{ "x-siteurl", url }

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +26
if (!Force && !ShouldProcess($"Search vertical '{Identity}' at {Scope} scope", "Remove"))
return;
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-Force bypasses ShouldProcess(), which means -WhatIf/-Confirm are ignored when -Force is used. With SupportsShouldProcess=true, the cmdlet should still call ShouldProcess even under -Force (use Force only to skip confirmation prompts, not WhatIf semantics).

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +27
if (!Force && !ShouldProcess($"Search result type '{Identity}' at {Scope} scope", "Remove"))
return;
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-Force currently skips calling ShouldProcess(), so -WhatIf and -Confirm will not be honored when -Force is present. Keep ShouldProcess in the execution path and use -Force only to bypass additional confirmation (e.g., ShouldContinue) so WhatIf remains safe.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +86
if (ParameterSpecified(nameof(IncludeConnectorResults)))
payload.IncludeConnectorResults = IncludeConnectorResults;

// Clear server-only fields that shouldn't be sent in the PUT
payload.LastModifiedBy = null;
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IncludeConnectorResults is applied unconditionally when specified. This conflicts with the stated behavior/documentation that it only applies to built-in verticals (SITEALL/ALL) and should be ignored or rejected for custom verticals. Consider checking the vertical type (from the GET payload) and either warn+ignore or throw when IncludeConnectorResults is bound for a custom vertical, so runtime behavior matches the docs/PR description.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +85
// 6. Check if order is actually different
var currentOrder = customVerticals.Keys.ToList();
var alreadyInOrder = currentOrder.SequenceEqual(Identity, StringComparer.OrdinalIgnoreCase);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentOrder is derived from customVerticals.Keys, but customVerticals is a Dictionary and its key enumeration order is not a guaranteed representation of the API’s returned order. Since the reorder optimization depends on the existing order, build currentOrder from the original allVerticals sequence (filtered to custom verticals) instead of dictionary keys.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant