Wednesday, April 29, 2026

Microsoft 365 Copilot Extensibility Complete Guide

 

Microsoft 365 Copilot Extensibility — Complete Guide

Plugins · Declarative Agents · Graph Connectors · Security · Scenarios · Cheat Sheet


Table of Contents

  1. Core Concepts — Basics
  2. Plugins — API Plugins & Message Extensions
  3. Declarative Agents
  4. Microsoft Graph Connectors
  5. Security, Governance & Responsible AI
  6. Scenario-Based Questions
  7. Cheat Sheet — Quick Reference

1. Core Concepts — Basics

What is Microsoft 365 Copilot and what does it do?

Microsoft 365 Copilot is an AI assistant integrated across Microsoft 365 apps (Teams, Outlook, Word, Excel, PowerPoint, SharePoint, Loop). It combines large language models (GPT-4 class) with the Microsoft Graph — giving it access to your organisation's data (emails, meetings, documents, chats) to generate contextually relevant responses.

Key capabilities:

  • Microsoft 365 Chat (BizChat): cross-app AI assistant — ask questions across emails, meetings, documents, chats
  • In-app Copilot: context-aware AI within specific apps — Word (draft, rewrite), Excel (analyse data), PowerPoint (create), Teams (meeting summaries)
  • Extensibility: developers extend Copilot with plugins, agents, and Graph Connectors

Key positioning: M365 Copilot = LLM + Microsoft Graph (your org data) + Microsoft 365 apps. Extensibility = adding YOUR data and YOUR actions to this system.


What are the three main extensibility mechanisms for Microsoft 365 Copilot?

Mechanism What It Does Who Builds It
Plugins Extend Copilot's ability to take actions and retrieve real-time data from external systems Developers
Declarative Agents Customised Copilot experiences with specific persona, scope, knowledge, and plugins Makers + Developers
Graph Connectors Index external data into Microsoft Graph so Copilot can search and reason over it Developers

Mental model: Graph Connectors = bring your data IN. Plugins = let Copilot take actions OUT. Declarative Agents = package it all into a focused experience.


What is the Microsoft 365 Copilot architecture and how does it process a user prompt?

User types a prompt in Teams / BizChat
        ↓
Copilot orchestrator (powered by Semantic Kernel)
  1. Understands intent via LLM
  2. Decides which skills/plugins to invoke
  3. Calls Microsoft Graph for user context
     (emails, calendar, files, chats, contacts)
  4. Calls relevant plugins for external data/actions
  5. Passes all retrieved data as grounding context to LLM
        ↓
LLM (GPT-4 class) generates a response
grounded in the user's actual organisational data
        ↓
Response rendered in Teams/Outlook/BizChat
with citations to source documents and items

Tip: The orchestrator is powered by Semantic Kernel — Microsoft's open-source AI SDK. Understanding this pipeline is essential for architect-level extensibility.


What is Teams App Manifest v1.13+ and how does it relate to Copilot extensibility?

The Teams App Manifest (now called Microsoft 365 App Manifest) defines an app's capabilities across Microsoft 365 surfaces. Version 1.13+ introduced Copilot extensibility support:

{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
  "manifestVersion": "1.17",
  "id": "unique-app-guid",
  "name": { "short": "Contoso HR", "full": "Contoso HR Assistant" },
  "copilotAgents": {
    "declarativeAgents": [{
      "id": "hrAgent",
      "file": "agents/hrAgent.json"
    }]
  },
  "plugins": [{
    "id": "hrApiPlugin",
    "file": "plugins/hrApiPlugin.json"
  }]
}

What is Teams Toolkit and how does it help with Copilot extensibility development?

Teams Toolkit is a VS Code extension and CLI providing project templates, local debugging, and deployment automation.

Capability Description
Project templates Pre-built scaffolds for API plugins, declarative agents, Graph Connectors
Local debugging Dev Tunnel for local API testing with real Copilot
Provision & deploy Automates Azure resource creation and deployment
Environment management Dev/test/prod configs with .env files
App publishing Packages and submits to Teams Admin Center

What is the difference between extending Copilot and building a standalone chatbot?

Extending M365 Copilot Standalone Chatbot
Base AI Uses M365 Copilot's LLM + Graph Bring your own LLM
User experience Within Teams/Outlook — familiar surface Separate app/interface
Org data access Built-in via Microsoft Graph Custom integration needed
Authentication Inherits M365 SSO Build your own auth
Governance Managed by Teams Admin Center Custom governance
Licence required M365 Copilot licence per user Depends on platform
Best for Enhancing existing M365 workflows Fully custom experiences

2. Plugins — API Plugins & Message Extensions

What is an API Plugin for Microsoft 365 Copilot and how does it work?

An API Plugin allows Copilot to call a REST API based on an OpenAPI (Swagger) specification. When a user asks something requiring real-time data or an external action, Copilot invokes the relevant API operation.

How it works:
1. Developer creates an OpenAPI spec for the REST API
2. Developer creates an AI plugin definition (ai-plugin.json)
   describing which operations Copilot can call + when
3. Plugin packaged in Teams app manifest (.zip)
4. Copilot's orchestrator reads plugin description + OpenAPI spec
5. When user prompt matches plugin's purpose → orchestrator calls the API
6. API response passed to LLM as grounding context
7. LLM generates response citing real-time API data

Key files:
manifest.json     ← Teams app manifest referencing the plugin
apiPlugin.json    ← AI plugin definition (name, desc, auth, api spec)
openapi.json      ← OpenAPI 3.0 spec describing REST endpoints

Tip: The quality of descriptions in the OpenAPI spec and plugin definition determines whether Copilot invokes the plugin correctly. Poor descriptions = plugin never triggered or triggered for wrong queries.


What is an AI Plugin definition file (ai-plugin.json) and what does it contain?

{
  "schema_version": "v2.1",
  "name_for_human": "Contoso HR Plugin",
  "name_for_model": "ContosoHR",
  "description_for_human": "Access HR data including leave balances and policies",
  "description_for_model": "Use this plugin to retrieve employee leave balances, HR policies, and submit leave requests. Call this when users ask about their leave, time off, holidays, annual leave, sick leave, or HR policies.",
  "auth": {
    "type": "OAuthPluginVault",
    "reference_id": "${{OAUTH_CONNECTION_NAME}}"
  },
  "api": {
    "type": "openapi",
    "url": "https://hrapi.contoso.com/openapi.json"
  },
  "logo_url": "https://hrapi.contoso.com/logo.png",
  "contact_email": "support@contoso.com",
  "functions": [
    {
      "name": "getLeaveBalance",
      "description": "Get the current leave balance for the authenticated employee. Use when user asks about remaining leave days, annual leave, sick leave, or time off balance."
    },
    {
      "name": "submitLeaveRequest",
      "description": "Submit a leave request for the employee. Use when user wants to apply for leave, book time off, or request annual/sick/personal leave."
    }
  ]
}

Warning: The description_for_model is what the Copilot orchestrator reads to decide when to invoke the plugin. Be very specific and include example scenarios. Vague descriptions cause the plugin to be ignored or misused.


What authentication options are available for API Plugins?

Auth Type Description Best For
None (anonymous) No authentication Public, non-sensitive APIs
API Key Static API key in header/query string Simple external APIs
OAuth 2.0 (OAuthPluginVault) User authenticates via OAuth. Token stored in Teams token vault. External systems with OAuth support
Microsoft Entra ID SSO Seamless SSO — user's M365 identity used automatically. No login prompt. Internal enterprise APIs

Tip: OAuthPluginVault with Entra ID is the recommended enterprise pattern — users get seamless SSO and the API receives a proper OAuth token scoped to the user's identity. No shared credentials.


What is a Message Extension plugin and how does it differ from an API plugin?

Message Extension plugin: a Teams Message Extension (search/action command) also surfaced as a Copilot plugin. Existing Teams apps can become Copilot plugins with minimal changes.

API plugin: built purely for Copilot, based on OpenAPI spec. No Teams UI surface — only callable by Copilot's orchestrator.

Message Extension Plugin API Plugin
Works in Teams compose box AND Copilot Copilot only
Backend Bot Framework handler Any REST API
Best for Search and insert records into Teams messages Read/write operations, complex workflows
Code required Yes (Bot Framework) No (just OpenAPI spec + api-plugin.json)

What are Adaptive Cards in the context of Copilot plugins?

Adaptive Cards are JSON-based UI templates that Copilot renders as rich structured responses when a plugin returns data.

{
  "type": "AdaptiveCard",
  "version": "1.5",
  "body": [
    {
      "type": "TextBlock",
      "text": "Your Leave Balance",
      "weight": "Bolder",
      "size": "Large"
    },
    {
      "type": "FactSet",
      "facts": [
        { "title": "Annual Leave", "value": "12 days remaining" },
        { "title": "Sick Leave",   "value": "5 days remaining" },
        { "title": "Personal",     "value": "2 days remaining" }
      ]
    }
  ],
  "actions": [
    {
      "type": "Action.Execute",
      "title": "Apply for Leave",
      "verb": "submitLeaveRequest"
    }
  ]
}

Tip: Always design Adaptive Card responses for data-heavy plugin results. Structured cards are far more readable than plain text responses — and they support actionable buttons for follow-up operations.


How do you write effective OpenAPI descriptions for Copilot plugins?

The Copilot orchestrator reads operation descriptions to decide when and how to invoke plugin functions.

# POOR description (will be ignored or mis-triggered):
operationId: getItems
summary: Get items

# GOOD description (Copilot knows exactly when to use this):
operationId: getEmployeeLeaveBalance
summary: Get an employee's current leave balance and entitlements
description: >
  Returns the authenticated employee's current leave balances for all
  leave types including annual leave, sick leave, personal leave, and
  parental leave. Use this operation when the user asks about:
  - How many days of leave they have left
  - Their annual/sick/personal leave balance
  - Time off entitlements
  - Remaining holidays
parameters:
  - name: leaveType
    description: >
      Optional. Filter by leave type: 'annual', 'sick', 'personal',
      'parental'. Omit to return all leave types.

3. Declarative Agents

What is a Declarative Agent and how does it differ from Microsoft 365 Copilot?

Microsoft 365 Copilot (general): broad AI assistant across all M365 data. General-purpose, answers any work question. No persona customisation.

Declarative Agent: a focused, scoped Copilot experience with:

  1. Custom persona: name, description, avatar — "Contoso HR Assistant", "IT Helpdesk Bot"
  2. Scoped knowledge: only specific SharePoint sites, OneDrive folders, or Graph Connector data
  3. Custom instructions: system prompt defining tone, behaviour, what to answer/refuse
  4. Specific plugins: only the plugins relevant to this agent's purpose
  5. Conversation starters: suggested prompts shown to users on first open

Tip: Declarative Agents are the answer to "build a custom Copilot for our HR team." They are not a separate AI model — they are a configured, scoped view of M365 Copilot.


What is the declarative agent manifest file and what does it contain?

{
  "$schema": "https://developer.microsoft.com/json-schemas/copilot/declarative-agent/v1.4/schema.json",
  "version": "v1.4",
  "name": "Contoso HR Assistant",
  "description": "Your personal HR assistant for leave, policies, and benefits.",
  "instructions": "You are an HR assistant for Contoso. Only answer HR-related questions about leave policies, employee benefits, and HR procedures. If asked about non-HR topics, politely redirect. Always be professional and empathetic. Never share other employees' personal information.",
  "conversation_starters": [
    {
      "title": "Check leave balance",
      "text": "How many days of annual leave do I have remaining?"
    },
    {
      "title": "Submit leave request",
      "text": "I want to apply for annual leave next week"
    },
    {
      "title": "Find HR policy",
      "text": "What is the remote working policy?"
    }
  ],
  "capabilities": [
    {
      "name": "OneDriveAndSharePoint",
      "items_by_sharepoint_ids": [
        {
          "site_id": "contoso.sharepoint.com,abc123,...",
          "web_id": "...",
          "list_id": "..."
        }
      ]
    },
    {
      "name": "GraphConnectors",
      "connections": [{ "connection_id": "hrdocuments" }]
    }
  ],
  "actions": [
    { "id": "hrPlugin", "file": "plugins/hrApiPlugin.json" }
  ]
}

What knowledge sources can a Declarative Agent use?

Source Description
SharePoint sites and libraries Specific sites, libraries, or folders scoped to the agent
OneDrive files Specific folders or files
Microsoft Graph Connectors External data indexed into Graph (ServiceNow, SAP, Confluence, custom DBs)
Web search Public internet via Bing (can be enabled/disabled)
Plugins Real-time data and actions via API plugins

Warning: Knowledge source scoping is a security control — an agent scoped to HR SharePoint sites cannot access Finance sites, even if the user has permission. The agent's scope is a strict constraint on what Copilot searches.


How do you write effective instructions for a Declarative Agent?

The instructions field is the system prompt defining persona, scope, behaviour, and constraints.

Effective instruction structure:

1. PERSONA: "You are [name], the [role] for [organisation]."

2. SCOPE: "Only answer questions about [domain]. If asked about
   [out-of-scope topic], say [specific redirect message]."

3. TONE: "Always be [professional/friendly/concise]. Use
   [plain language / technical language appropriate for audience]."

4. DATA CONSTRAINTS: "Never reveal other employees' [personal data].
   Only show the current user's own [records]."

5. PLUGIN GUIDANCE: "When asked about [specific topic], always use
   the [plugin name] to retrieve real-time data rather than relying
   on documents."

6. ESCALATION: "For questions you cannot answer, direct users to
   [contact/resource]."

Example — IT Helpdesk Agent:
"You are ITBot, the IT Helpdesk assistant for Contoso. Only answer
questions about IT support, software, hardware, network connectivity,
and access requests. For HR questions, direct users to the HR
Assistant. Always check the user's open tickets using the
ServiceNow plugin before suggesting solutions. Never close a ticket
without explicit user confirmation."

Tip: Treat instructions like a detailed job description. The more specific and clear, the better the agent behaves. Vague instructions lead to unpredictable responses and scope creep.


What is Copilot Studio and how does it relate to Declarative Agents?

Copilot Studio provides a low-code UI for building and publishing Declarative Agents without writing JSON files.

Copilot Studio for M365 Copilot agents:

  1. Create and configure declarative agents visually
  2. Add plugins from the catalogue or connect to custom APIs
  3. Add knowledge sources (SharePoint sites, web URLs, uploaded files)
  4. Test the agent in the test panel
  5. Publish to Microsoft 365 Copilot — appears in the Copilot agent store
  6. Share with specific users or deploy org-wide via Teams Admin Center
Copilot Studio Teams Toolkit + JSON
Audience Business users, makers Developers
Authoring GUI-based, low-code Code-first, JSON manifests
Source control Limited Full Git/DevOps support
Best for Quick deployment, business-owned agents Complex agents, ALM, pro-code

4. Microsoft Graph Connectors

What is a Microsoft Graph Connector and what problem does it solve?

A Microsoft Graph Connector indexes external data into the Microsoft Graph — making it searchable by Microsoft 365 Search, Microsoft 365 Copilot, and other Graph-aware services.

The problem it solves: organisations have valuable data in non-Microsoft systems (ServiceNow, Confluence, SAP, Salesforce, SQL databases, intranets). Without a connector, Copilot cannot access this data. A Graph Connector brings it into the Microsoft Search and Copilot ecosystem without migrating data to SharePoint.

Data flow:
ServiceNow / Confluence / SAP / Custom DB
        ↓ (Graph Connector indexes items via Graph API)
Microsoft Graph — external items index
        ↓
Microsoft 365 Search + Copilot can find and cite it
        ↓
User: "What is the status of IT ticket #12345?"
Copilot: retrieves from ServiceNow index → answers with citation

Tip: Graph Connectors are what makes Copilot truly enterprise-ready — they eliminate "Copilot doesn't know about our systems" by bringing external data into Graph's searchable index.


What are the components of a Graph Connector solution?

Component Description API
Connection Defines the connector — name, description POST /external/connections
Schema Structure of external items — property types, searchability PATCH /external/connections/{id}/schema
External items The actual data records pushed into the index PUT /external/connections/{id}/items/{itemId}
Result type How search results are displayed (Adaptive Card template) Admin Center configuration
Crawler/sync agent Your app that reads external system and pushes to Graph Custom application
// 1. Create connection
POST https://graph.microsoft.com/v1.0/external/connections
{
  "id": "contosohr",
  "name": "Contoso HR Documents",
  "description": "HR policies, procedures, and employee handbook"
}

// 2. Define schema
PATCH https://graph.microsoft.com/v1.0/external/connections/contosohr/schema
{
  "baseType": "microsoft.graph.externalItem",
  "properties": [
    { "name": "title", "type": "String", "isSearchable": true,
      "isRetrievable": true, "labels": ["title"] },
    { "name": "content", "type": "String", "isSearchable": true,
      "isRetrievable": true, "labels": ["body"] },
    { "name": "url", "type": "String", "isRetrievable": true,
      "labels": ["url"] },
    { "name": "lastModified", "type": "DateTime",
      "isRetrievable": true, "labels": ["lastModifiedDateTime"] },
    { "name": "category", "type": "String", "isSearchable": true,
      "isRefinable": true, "isRetrievable": true }
  ]
}

// 3. Push an external item
PUT https://graph.microsoft.com/v1.0/external/connections/contosohr/items/policy_001
{
  "acl": [
    { "type": "everyone", "value": "everyone", "accessType": "grant" }
  ],
  "properties": {
    "title": "Remote Working Policy 2025",
    "content": "Employees may work remotely up to 3 days per week...",
    "url": "https://intranet.contoso.com/hr/policies/remote-working",
    "lastModified": "2025-01-15T10:00:00Z",
    "category": "Work Arrangements"
  },
  "content": {
    "value": "Full policy text for search indexing...",
    "type": "text"
  }
}

What is ACL in Graph Connectors and why is it critical?

The ACL (Access Control List) on each external item defines who can see it in search results and Copilot responses.

"acl": [
  // Everyone in the org can see:
  { "type": "everyone", "value": "everyone",
    "accessType": "grant" },

  // Specific Entra ID user can see:
  { "type": "user",
    "value": "entra-user-object-id",
    "accessType": "grant" },

  // Members of an Azure AD group can see:
  { "type": "group",
    "value": "aad-group-object-id",
    "accessType": "grant" },

  // Deny a specific user (overrides grants):
  { "type": "user",
    "value": "blocked-user-object-id",
    "accessType": "deny" }
]

Critical: ACL is the security boundary for Graph Connector data. If ACLs are misconfigured (e.g., everyone can see confidential HR records), Copilot will expose that data to all users. Always map source system permissions to Entra ID identities — never default to "everyone" for sensitive data.


What are pre-built Graph Connectors and when would you build custom?

Pre-built connectors (available in M365 Admin Center → Search → Data Sources): ServiceNow, Confluence, Jira, Salesforce, SAP, Azure DevOps, GitHub, MediaWiki, and more.

Build custom when:

  • No pre-built connector exists for your system
  • Pre-built connector doesn't index the specific data/schema you need
  • Custom ACL mapping logic is required for your org's permission model
  • You need to transform or enrich data before indexing
  • You need incremental sync with custom change detection

Tip: Always check the pre-built catalogue first. ServiceNow, Confluence, and Jira connectors cover the majority of enterprise use cases. Custom connectors are for unique or proprietary systems.


5. Security, Governance & Responsible AI

How does Microsoft 365 Copilot enforce data security and permissions?

  1. Microsoft Graph respects existing permissions: Copilot only retrieves data the current user has permission to access in SharePoint, OneDrive, Exchange, and Teams. It cannot elevate permissions.
  2. Graph Connector ACLs: external items only shown to users whose Entra ID identity matches the item's ACL
  3. Plugin authentication: OAuth plugins call external API as the current user — not a service account. Users only see their own data.
  4. Tenant isolation: Copilot data never crosses tenant boundaries. Org data is never used to train or improve the LLM.
  5. Microsoft Purview integration: sensitivity labels on documents are respected — confidential documents are not surfaced to users without appropriate permissions

Key principle: Copilot can only show what the user could find themselves. It is a search and reasoning layer on top of existing permissions — not a permission bypass.


What admin controls exist for governing Copilot extensibility?

Control Location Purpose
App approval Teams Admin Center → Manage apps Allow/block specific plugins and agents
App permission policies Teams Admin Center Control which users can access which Copilot apps
App setup policies Teams Admin Center Pre-install agents for users automatically
Copilot settings M365 Admin Center → Copilot Tenant-wide: web search, connected experiences
Purview audit Microsoft Purview Audit Copilot interactions, retention policies
Graph Connector admin M365 Admin Center → Search Enable/disable connections, view indexed items

Warning: Any plugin or agent published to the organisation must be approved in Teams Admin Center before users can access it. Unapproved apps are blocked by default.


What is Responsible AI and how does it apply to Copilot extensibility?

Microsoft's Responsible AI principles applied to extensibility:

Principle Developer Responsibility
Transparency Be honest about what the agent can/cannot do. Don't design agents that deceive users about their AI nature.
Fairness Don't design agents that return different quality responses based on protected characteristics.
Privacy Don't log user prompts in violation of privacy policies. Use minimum data needed. Respect sensitivity labels.
Reliability Handle plugin errors gracefully. Never let a failed API call result in a misleading response.
Human oversight For high-stakes actions (approvals, purchases, deletions), require explicit confirmation via Adaptive Card before executing.
Accountability Maintain audit logs of agent actions. Ensure humans can review and override agent decisions.

6. Scenario-Based Questions

Scenario: Build a Copilot plugin that lets employees check their ServiceNow IT tickets.

  1. Approach: API Plugin (not message extension) — structured data, Copilot-only needed
  2. ServiceNow API: use Table API. OpenAPI spec for:
    • GET /api/now/table/incident?caller_id={userId} — get user's tickets
    • GET /api/now/table/incident/{id} — get ticket detail
  3. Authentication: OAuth 2.0 with Entra ID SSO — users authenticate once
  4. Critical — OpenAPI descriptions:
    operationId: getMyTicketsdescription: "Retrieves all IT support tickets raised by the current  employee. Use when user asks about their tickets, incidents, IT  requests, open issues, support cases, or IT problems."
    
  5. Adaptive Card: ticket number, title, status, priority, last updated + "View in ServiceNow" deep-link button
  6. ai-plugin.json: detailed description_for_model with example user queries
  7. Deploy: Teams Toolkit → package → Teams Admin Center → admin approves

Key insight: Description quality determines plugin invocation. Poor descriptions = Copilot never uses the plugin even when it should.


Scenario: Build a Declarative Agent for the Sales team with CRM and product data.

Agent configuration:

  • Name: "Contoso Sales Assistant"
  • Instructions: "You are a Sales Assistant for Contoso. Help sales reps with product information, pricing, opportunity management, and competitive analysis. Always reference official pricing from the pricing API. Only access the current user's opportunities from Salesforce."

Knowledge sources:

  • SharePoint: Product catalogue site, Sales playbooks, Pricing documents
  • Graph Connector: Salesforce CRM (opportunities, accounts, contacts — indexed nightly)
  • OneDrive: Sales team shared folder

Plugins:

  • Salesforce API plugin: get/update opportunities, log activities
  • Pricing API plugin: real-time pricing for product configurations

Conversation starters:

  • "What are my open opportunities this quarter?"
  • "What is the pricing for Product X with Enterprise support?"
  • "Help me prepare talking points for my meeting with [Company]"

Deploy: Copilot Studio → publish → share with Sales Azure AD group


Scenario: Your organisation has a legacy intranet with thousands of HR policy documents. How do you make them available to Copilot?

Solution: Custom Graph Connector

  1. Build a sync agent: Azure Function or .NET app that reads the intranet's content via its API or crawl
  2. Create connection: POST /external/connections with id: "hrintranet", descriptive name and description
  3. Define schema: properties for Title, Content, URL, Author, LastModified, PolicyCategory — mark Content and Title as isSearchable: true, set appropriate labels
  4. Map permissions: if HR policies are org-wide, use ACL type "everyone". For restricted policies (e.g., management-only), map intranet groups to Entra ID group object IDs
  5. Push items: for each document, PUT /external/connections/hrintranet/items/{id} with properties and content
  6. Schedule incremental sync: track lastCrawledDateTime, only push items modified since last sync
  7. Configure result type: Adaptive Card template for how results appear in search
  8. Add to HR Declarative Agent: reference "connection_id": "hrintranet" in agent capabilities
  9. Test: ask Copilot "What is the parental leave policy?" — should cite intranet document with link

Scenario: How do you handle a plugin write operation with user confirmation before executing?

Requirement: Plugin submits a leave request. User must confirm before the request is submitted — Copilot should not submit automatically on a single ambiguous prompt.

Two-step API design:

# Step 1 — Preview (safe, no side effects):
GET /leaveRequest/preview?type={leaveType}&start={date}&end={date}
Returns: { dates, days, remainingBalance, approver, confirmation_id }

# Step 2 — Submit (only called after user confirms):
POST /leaveRequest/submit
Body: { confirmation_id }

Adaptive Card confirmation returned from preview:

{
  "type": "AdaptiveCard",
  "body": [
    { "type": "TextBlock", "text": "Leave Request Preview", "weight": "Bolder" },
    { "type": "FactSet", "facts": [
      { "title": "Type", "value": "Annual Leave" },
      { "title": "Dates", "value": "15 Jan – 19 Jan 2026" },
      { "title": "Days", "value": "5 days" },
      { "title": "Balance after", "value": "7 days remaining" }
    ]}
  ],
  "actions": [
    { "type": "Action.Execute", "title": "✓ Confirm & Submit",
      "verb": "submitLeave" },
    { "type": "Action.Execute", "title": "✗ Cancel",
      "verb": "cancelLeave" }
  ]
}

Plugin instructions in ai-plugin.json:

"Always call the preview endpoint first and display the confirmation card. Only call the submit endpoint when the user explicitly clicks Confirm in the card."

Responsible AI principle: The two-step preview-then-confirm pattern is the standard for any write operation in Copilot plugins. It gives users agency and prevents accidental actions from ambiguous prompts.


Scenario: How do you ensure a Declarative Agent doesn't leak confidential Finance data to HR users?

  1. Scope knowledge sources strictly: only add HR SharePoint sites to the HR agent — never Finance sites, even if the admin has access to both
  2. Use Graph Connector ACLs: HR-specific connector items have ACL entries for the HR security group only — Finance documents have Finance group ACL. Copilot automatically enforces these.
  3. Plugin authentication: use OAuth SSO — the plugin calls the HR API as the current user. The API enforces its own role-based access. Copilot cannot bypass API-level security.
  4. Instructions boundary: "Only answer questions about HR. If asked about financial data, budgets, or Finance matters, say 'I only have access to HR information.'"
  5. Test with non-HR users: verify Finance users cannot retrieve HR-restricted content through the agent

7. Cheat Sheet — Quick Reference

Extensibility Mechanisms at a Glance

API Plugin
→ OpenAPI spec + ai-plugin.json + manifest.json
→ Copilot calls your REST API for real-time data/actions
→ Auth: None / API Key / OAuth / Entra SSO
→ Response: plain text or Adaptive Cards

Message Extension Plugin
→ Existing Teams app (composeExtension) surfaced in Copilot
→ Works in Teams compose box AND Copilot
→ Uses Bot Framework
→ Good for search + insert into Teams messages

Declarative Agent
→ agents/myAgent.json + manifest.json
→ Scoped Copilot with custom persona + knowledge + plugins
→ No custom AI model — configured view of M365 Copilot
→ Authoring: Teams Toolkit (code) or Copilot Studio (low-code)

Graph Connector
→ Custom app calling Graph API to index external items
→ Creates: connection → schema → push items
→ Items appear in M365 Search and Copilot responses
→ ACL per item controls who can see it

AI Plugin Definition — Key Fields

{
  "name_for_model": "ShortNoSpacesName",
  "description_for_model": "DETAILED description of what this plugin does,
    when to use it, example scenarios. This is what Copilot reads.",
  "auth": {
    "type": "OAuthPluginVault | ApiKeyAuth | None",
    "reference_id": "${{OAUTH_CONNECTION}}"
  },
  "functions": [{
    "name": "operationId from OpenAPI spec",
    "description": "SPECIFIC description of this function with
      example user queries that should trigger it"
  }]
}

Declarative Agent — Key Fields

{
  "name": "Display name shown to users",
  "instructions": "System prompt — persona, scope, tone, data rules",
  "conversation_starters": [
    { "title": "Short title", "text": "Full prompt to send" }
  ],
  "capabilities": [
    {
      "name": "OneDriveAndSharePoint",
      "items_by_sharepoint_ids": [{ "site_id": "..." }]
    },
    {
      "name": "GraphConnectors",
      "connections": [{ "connection_id": "myconnection" }]
    }
  ],
  "actions": [
    { "id": "myPlugin", "file": "plugins/plugin.json" }
  ]
}

Graph Connector — API Flow

1. Create connection
POST /v1.0/external/connections
{ "id": "myconn", "name": "...", "description": "..." }

2. Register schema (async — poll until Completed)
PATCH /v1.0/external/connections/myconn/schema
{ "baseType": "microsoft.graph.externalItem",
  "properties": [{ "name": "title", "type": "String",
    "isSearchable": true, "labels": ["title"] }] }

3. Push items
PUT /v1.0/external/connections/myconn/items/{itemId}
{ "acl": [...],
  "properties": { "title": "...", "content": "..." },
  "content": { "value": "...", "type": "text" } }

4. Update items (same PUT — upsert semantics)
5. Delete items
DELETE /v1.0/external/connections/myconn/items/{itemId}

Schema property labels (map to Microsoft semantics):
title, url, iconUrl, createdBy, lastModifiedBy,
createdDateTime, lastModifiedDateTime, fileName, fileExtension, body

Deployment Path

Development:
Teams Toolkit → scaffold project → code → local debug with Dev Tunnel

Packaging:
Teams Toolkit → Provision → Deploy → Package

Internal deployment:
Upload .zip to Teams Admin Center → Admin approves → Users access

Public (Teams Store):
Microsoft Partner Center → Submit for validation → Store listing

Top 10 Tips

  1. Three mechanisms, one sentence each — "Graph Connectors bring data IN. Plugins let Copilot act OUT. Declarative Agents package it into a focused experience." Start every architecture question here.
  2. description_for_model quality = plugin success — the orchestrator reads this to decide when to invoke the plugin. Vague descriptions = plugin never used. This is the #1 developer mistake.
  3. Copilot cannot bypass permissions — it only surfaces what the user could already access. Copilot is a reasoning layer, not a permission bypass. Security questions always return to this principle.
  4. ACL misconfiguration is the #1 Graph Connector risk — defaulting to "everyone" on sensitive items exposes confidential data to all users through Copilot queries.
  5. Two-step confirm pattern for write operations — preview first, submit only on explicit user confirmation. Responsible AI standard for any destructive or irreversible action.
  6. Declarative Agents ≠ new AI model — they are a configured, scoped view of M365 Copilot. Many candidates think they require a custom LLM — they do not.
  7. Teams Admin Center approval is mandatory — no plugin or agent reaches users without admin approval. This is the governance gate — always mention it in deployment discussions.
  8. OAuthPluginVault with Entra SSO is the correct enterprise auth answer. Shared service account credentials in plugins are a security anti-pattern.
  9. Copilot Studio vs Teams Toolkit — Copilot Studio for makers and business users; Teams Toolkit for developers needing source control and ALM. Both produce the same underlying manifest JSON.
  10. Schema property labels in Graph Connectors map external properties to Microsoft's semantic understanding — labels: ["title"], labels: ["body"] etc. Correct labelling dramatically improves Copilot's ability to summarise and cite your external content.


Tuesday, April 28, 2026

SharePoint Framework (SPFx) Complete Guide

 

SharePoint Framework (SPFx) — Complete Guide

Core Concepts · Web Parts · Extensions · API & Graph · Deployment & ALM · Scenarios · Cheat Sheet


Table of Contents

  1. Core Concepts — Basics
  2. Web Parts — Deep Dive
  3. SPFx Extensions
  4. API Calls, Graph & Authentication
  5. Deployment, Packaging & ALM
  6. Performance & Best Practices
  7. Scenario-Based Questions
  8. Cheat Sheet — Quick Reference

1. Core Concepts — Basics

What is SharePoint Framework (SPFx) and why was it introduced?

SPFx is the recommended development model for extending SharePoint Online, Microsoft Teams, Microsoft Viva, and Outlook. Introduced in 2016, it replaced older extension models (Farm Solutions, Sandboxed Solutions, Script Editor web parts) with a modern, client-side, TypeScript-based framework.

Why SPFx was introduced:

  1. Runs entirely in the browser — no server-side code execution required
  2. Uses modern web standards — TypeScript, React, webpack, Node.js toolchain
  3. Works in SharePoint Online, SharePoint 2019/SE on-premises, Teams, Viva Connections, and Outlook
  4. Supports all SharePoint security and permissions natively
  5. Fully supported by Microsoft — unlike Script Editor/Content Editor web parts

Key positioning: SPFx replaced the "full trust" server-side model with a "least privilege" client-side model. This is what makes it cloud-compatible and Teams/Viva-ready.


What can you build with SPFx?

Component Description
Client-side web parts Interactive UI components placed on SharePoint pages and Teams tabs
Application Customizer Inject custom header/footer HTML across all pages in a site
Field Customizer Customise how a column value is rendered in a list view
Command Set Add custom buttons to list/library toolbar and context menu
Adaptive Card Extensions (ACE) Cards for Microsoft Viva Connections dashboard
Form customiser Replace default SharePoint new/edit/display forms with custom React forms

What is the SPFx development toolchain and what does each component do?

Node.js       → JavaScript runtime — required for the build toolchain
npm / yarn    → package manager — install dependencies
Yeoman (yo)   → project scaffolding generator
  @microsoft/generator-sharepoint → SPFx-specific Yeoman generator
gulp          → task runner — build, bundle, package, serve
webpack       → module bundler — bundles TypeScript/React into JS
TypeScript    → typed superset of JavaScript — primary language
React         → UI component library (default, optional)
Fluent UI     → Microsoft design system — UI components for M365 look/feel
PnPjs         → community library for SharePoint/Graph REST API calls

Tip: SPFx has specific Node.js version compatibility requirements per SPFx version. Always check the SPFx compatibility matrix before setting up a new dev environment. Using the wrong Node.js version causes cryptic build errors.


What is the SPFx project structure and what are the key files?

my-spfx-solution/
├── config/
│   ├── config.json              ← bundle config, external scripts
│   ├── package-solution.json    ← solution metadata, version, permissions
│   ├── serve.json               ← local workbench serve config
│   └── write-manifests.json     ← CDN URL for asset hosting
├── src/
│   └── webparts/
│       └── myWebPart/
│           ├── MyWebPartWebPart.ts           ← main web part class
│           ├── MyWebPartWebPart.manifest.json ← component metadata, GUID
│           └── components/
│               ├── MyWebPart.tsx             ← React component
│               └── MyWebPart.module.scss     ← CSS modules
├── sharepoint/
│   └── assets/                  ← provisioning XML, list schemas
├── .yo-rc.json                  ← Yeoman config, SPFx version
├── package.json                 ← npm dependencies
├── tsconfig.json                ← TypeScript config
└── gulpfile.js                  ← gulp tasks

What are the key SPFx CLI commands every developer must know?

# Project setup
yo @microsoft/sharepoint          # scaffold new project
npm install                       # install dependencies

# Development
gulp serve                        # local workbench
gulp serve --nobrowser            # serve without opening browser

# Building
gulp build                        # compile TypeScript, validate
gulp bundle                       # webpack bundle (debug)
gulp bundle --ship                # webpack bundle (production/minified)
gulp package-solution             # create .sppkg file (debug)
gulp package-solution --ship      # create .sppkg file (production)

# Upgrade Node/SPFx compatibility check
spfx doctor                       # check environment compatibility

What is the difference between SPFx and the older SharePoint extension models?

Model Runs Trust Cloud-compatible Status
Farm Solutions Server-side Full trust No Deprecated
Sandboxed Solutions Server-side Partial trust No Deprecated
Script Editor web part Client-side No governance Technically yes Discouraged
SPFx Client-side Least privilege Yes Recommended

2. Web Parts — Deep Dive

What is the anatomy of an SPFx web part class?

import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IPropertyPaneConfiguration,
         PropertyPaneTextField } from '@microsoft/sp-property-pane';
import * as React from 'react';
import * as ReactDom from 'react-dom';
import MyComponent from './components/MyComponent';

export interface IMyWebPartProps {
  description: string;
  listName: string;
}

export default class MyWebPart
  extends BaseClientSideWebPart<IMyWebPartProps> {

  public render(): void {
    // Mount React component
    const element = React.createElement(MyComponent, {
      description: this.properties.description,
      listName: this.properties.listName,
      context: this.context    // ← pass SP context to React
    });
    ReactDom.render(element, this.domElement);
  }

  protected onDispose(): void {
    ReactDom.unmountComponentAtNode(this.domElement);
  }

  // Lifecycle hooks
  protected async onInit(): Promise<void> {
    // initialise PnPjs, services, etc.
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [{
        header: { description: 'Web Part Settings' },
        groups: [{
          groupName: 'Configuration',
          groupFields: [
            PropertyPaneTextField('description', { label: 'Description' }),
            PropertyPaneTextField('listName', { label: 'List Name' })
          ]
        }]
      }]
    };
  }
}

What is the Web Part context and what does it expose?

The web part context (this.context) is the gateway to SharePoint and Microsoft 365 platform services:

this.context.pageContext
  .web.absoluteUrl      // current site URL
  .web.title            // site title
  .user.displayName     // current user's display name
  .user.email           // current user's email
  .user.loginName       // current user's login name (i:0#.f|membership|...)
  .site.id              // site collection GUID
  .list.id              // current list GUID (if on list page)
  .list.title           // current list title (if on list page)

this.context.spHttpClient         // make SharePoint REST API calls
this.context.msGraphClientFactory // get MS Graph v3 client
this.context.aadHttpClientFactory // call Azure AD-secured APIs
this.context.httpClient           // call anonymous/public APIs
this.context.serviceScope         // DI container
this.context.propertyPane         // open/refresh the property pane
this.context.sdks.microsoftTeams  // Teams context (if running in Teams)

Tip: Never use window.location or hardcode site URLs. Always use this.context.pageContext.web.absoluteUrl for the current site URL.


What is the Property Pane and what control types are available?

The Property Pane is the configuration panel that opens when an editor clicks "Edit" on a web part.

Built-in property pane controls:

Control Description
PropertyPaneTextField Single or multi-line text input
PropertyPaneCheckbox Boolean toggle
PropertyPaneDropdown Select from a list of options
PropertyPaneToggle On/off switch
PropertyPaneSlider Numeric range slider
PropertyPaneChoiceGroup Radio button group
PropertyPaneLink Clickable link
PropertyPaneLabel Descriptive read-only label
PropertyPaneHorizontalRule Visual separator

PnP property pane controls (@pnp/spfx-property-controls):

Control Description
PropertyFieldListPicker Pick a SharePoint list
PropertyFieldPeoplePicker Pick SharePoint users/groups
PropertyFieldTermPicker Pick managed metadata terms
PropertyFieldColorPicker Colour selector
PropertyFieldDateTimePicker Date/time input

What is the difference between reactive and non-reactive property panes?

Reactive (default): web part re-renders immediately every time a property pane value changes — even before the user confirms.

Non-reactive: web part only re-renders when the user clicks "Apply". Better for properties that trigger expensive API calls on change.

protected get disableReactivePropertyChanges(): boolean {
  return true;  // web part only updates on Apply button click
}

Tip: Use non-reactive when property changes trigger API calls (e.g., selecting a list name loads its columns). Reactive mode fires an API call on every keystroke — expensive and poor UX.


What is the SPFx web part manifest and what does it contain?

{
  "$schema": "https://developer.microsoft.com/json-schemas/...",
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "alias": "MyWebPart",
  "componentType": "WebPart",
  "version": "0.0.1",
  "manifestVersion": 2,
  "requiresCustomScript": false,
  "supportedHosts": [
    "SharePointWebPart",
    "TeamsPersonalApp",
    "TeamsTab",
    "SharePointFullPage"
  ],
  "supportsThemeVariants": true,
  "preconfiguredEntries": [{
    "groupId": "5c03119e-3074-46fd-976b-c60198311f70",
    "group": { "default": "Other" },
    "title": { "default": "My Web Part" },
    "description": { "default": "My web part description" },
    "officeFabricIconFontName": "Page",
    "properties": {
      "description": "My Web Part"
    }
  }]
}

Critical: Never change the web part GUID (id) after it has been deployed to production. The GUID identifies the component on pages — changing it breaks all existing page instances.


3. SPFx Extensions

What is an Application Customizer and how does it work?

An Application Customizer injects custom HTML into placeholder regions (Top = header, Bottom = footer) on every SharePoint page where it is activated.

import { BaseApplicationCustomizer,
         PlaceholderContent, PlaceholderName }
  from '@microsoft/sp-application-base';

export default class HeaderFooterCustomizer
  extends BaseApplicationCustomizer<IHeaderFooterProps> {

  private _headerPlaceholder: PlaceholderContent | undefined;

  public onInit(): Promise<void> {
    this.context.placeholderProvider.changedEvent
      .add(this, this._renderPlaceholders);
    this._renderPlaceholders();
    return Promise.resolve();
  }

  private _renderPlaceholders(): void {
    if (!this._headerPlaceholder) {
      this._headerPlaceholder =
        this.context.placeholderProvider
          .tryCreateContent(PlaceholderName.Top, {
            onDispose: this._onDispose
          });
    }
    if (this._headerPlaceholder?.domElement) {
      this._headerPlaceholder.domElement.innerHTML =
        `<div class="custom-header">Company Banner</div>`;
      // Or mount a React component for richer UI
    }
  }
}

Tip: Application Customizers deployed to the Tenant App Catalog with skipFeatureDeployment: true and activated via the "Tenant Wide Extensions" list apply to ALL SharePoint sites in the tenant in one operation.


What is a Field Customizer and when would you use it?

A Field Customizer replaces the default rendering of a column value in a list view with custom HTML/React.

import { BaseFieldCustomizer,
         IFieldCustomizerCellEventParameters }
  from '@microsoft/sp-listview-extensibility';

export default class StatusFieldCustomizer
  extends BaseFieldCustomizer<IStatusProps> {

  public onRenderCell(
    event: IFieldCustomizerCellEventParameters): void {

    const status: string = event.fieldValue;
    const color: string =
      status === 'Approved' ? 'green' :
      status === 'Rejected' ? 'red' : 'orange';

    event.domElement.innerHTML =
      `<span style="color:${color};font-weight:bold;
        padding:4px 8px;border-radius:4px;
        background:${color}20">${status}</span>`;
  }

  public onDisposeCell(
    event: IFieldCustomizerCellEventParameters): void {
    ReactDom.unmountComponentAtNode(event.domElement);
  }
}

Use cases: colour-coded status badges, progress bars, rating stars, clickable action buttons, formatted currency/date display.


What is a Command Set extension and how do you add custom buttons to a list?

A Command Set adds custom buttons to the SharePoint list/library toolbar and context menu.

import { BaseListViewCommandSet, Command,
         IListViewCommandSetExecuteEventParameters,
         IListViewCommandSetListViewUpdatedParameters }
  from '@microsoft/sp-listview-extensibility';

export default class ApprovalCommandSet
  extends BaseListViewCommandSet<ICommandProps> {

  public onListViewUpdated(
    event: IListViewCommandSetListViewUpdatedParameters): void {
    const approveCmd: Command = this.tryGetCommand('APPROVE_ITEM');
    if (approveCmd) {
      // Show button only when exactly one item selected
      approveCmd.visible = event.selectedRows.length === 1;
    }
  }

  public onExecute(
    event: IListViewCommandSetExecuteEventParameters): void {
    switch (event.itemId) {
      case 'APPROVE_ITEM':
        const itemId = event.selectedRows[0].getValueByName('ID');
        this._approveItem(Number(itemId));
        break;
    }
  }

  private async _approveItem(id: number): Promise<void> {
    const sp = spfi().using(SPFx(this.context));
    await sp.web.lists.getByTitle('Requests').items.getById(id)
      .update({ Status: 'Approved' });
    this.context.listView.refresh();
  }
}

What are Adaptive Card Extensions (ACE) and what are they used for?

Adaptive Card Extensions are SPFx components that render as cards on the Microsoft Viva Connections dashboard.

Two views:

  • Card view: compact card on the dashboard — title, description, icon, primary/secondary button
  • Quick view: richer panel that opens on card click — detailed data, forms, actions using Adaptive Card JSON

Common use cases:

  • Pending approvals count with approve/reject actions
  • Today's lunch menu or announcements
  • IT ticket submission form
  • Time-off balance with request button
  • Key KPI metrics tile

Tip: ACEs are the bridge between SPFx development and Viva Connections. If asked about Viva extensibility — the answer is ACE + SPFx.


What is a Form Customiser in SPFx?

A Form Customiser replaces the default SharePoint list new/edit/display forms with a completely custom React-based form. Instead of the standard SharePoint form UI, users see your custom UI.

Use cases:

  • Multi-step wizard forms
  • Custom validation with complex business rules
  • Branded forms matching company design system
  • Forms with dynamic fields based on previous selections
  • Integration with external systems at form submission

4. API Calls, Graph & Authentication

What are the different ways to call APIs from SPFx?

Client Use For Auth Method
SPHttpClient SharePoint REST API SharePoint session cookie (automatic)
MSGraphClientV3 Microsoft Graph API MSAL OAuth (automatic via SPFx)
AadHttpClient Azure AD-secured custom APIs OAuth token (automatic via SPFx)
HttpClient Anonymous/public APIs None
PnPjs SharePoint REST + Graph (wrapper) Inherits from SPFx context

How do you call the SharePoint REST API from an SPFx web part?

import { SPHttpClient, SPHttpClientResponse }
  from '@microsoft/sp-http';

// GET — read list items with OData
const response: SPHttpClientResponse =
  await this.context.spHttpClient.get(
    `${this.context.pageContext.web.absoluteUrl}` +
    `/_api/web/lists/getbytitle('Tasks')/items` +
    `?$select=Title,Status,AssignedTo/Title` +
    `&$expand=AssignedTo` +
    `&$filter=Status eq 'In Progress'` +
    `&$orderby=Created desc` +
    `&$top=10`,
    SPHttpClient.configurations.v1
  );
const data = await response.json();
const items = data.value;

// POST — create a new list item
await this.context.spHttpClient.post(
  `${this.context.pageContext.web.absoluteUrl}` +
  `/_api/web/lists/getbytitle('Tasks')/items`,
  SPHttpClient.configurations.v1,
  {
    headers: {
      'Accept': 'application/json;odata=nometadata',
      'Content-Type': 'application/json;odata=verbose'
    },
    body: JSON.stringify({
      '__metadata': { 'type': 'SP.Data.TasksListItem' },
      'Title': 'New Task',
      'Status': 'Not Started'
    })
  }
);

How do you call Microsoft Graph API from SPFx?

// Step 1 — declare permissions in package-solution.json:
"webApiPermissionRequests": [
  { "resource": "Microsoft Graph", "scope": "User.Read" },
  { "resource": "Microsoft Graph", "scope": "Sites.Read.All" },
  { "resource": "Microsoft Graph", "scope": "Mail.Read" }
]
Step 2 — admin approves:
SharePoint Admin Center → Advanced → API Access → approve pending requests
// Step 3 — use MSGraphClientV3 in web part:
import { MSGraphClientV3 } from '@microsoft/sp-http';

const client: MSGraphClientV3 =
  await this.context.msGraphClientFactory.getClient('3');

// Get current user's profile
const me = await client.api('/me').get();

// Get SharePoint sites
const sites = await client.api('/sites')
  .filter("siteCollection/root ne null")
  .select('displayName,webUrl')
  .top(50)
  .get();

// Get current user's manager
const manager = await client.api('/me/manager').get();

// Send email on behalf of user
await client.api('/me/sendMail').post({
  message: {
    subject: 'Notification from SPFx',
    body: { contentType: 'Text', content: 'Hello from SPFx' },
    toRecipients: [
      { emailAddress: { address: 'recipient@company.com' } }
    ]
  }
});

Warning: Graph API permissions in SPFx are tenant-wide — any SPFx solution can use approved permissions. Never approve overly broad scopes (User.ReadWrite.All, Sites.FullControl.All). Use least privilege.


What is PnPjs and why is it preferred over raw SPHttpClient?

PnPjs (@pnp/sp, @pnp/graph) wraps SharePoint REST and Microsoft Graph APIs with a fluent, strongly-typed, chainable interface.

// Setup in onInit():
import { spfi, SPFx } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/site-users";

const sp = spfi().using(SPFx(this.context));

// Read list items (concise vs verbose raw HTTP):
const items = await sp.web
  .lists.getByTitle('Tasks')
  .items
  .select('Title', 'Status', 'Author/Title')
  .expand('Author')
  .filter("Status eq 'Active'")
  .orderBy('Created', false)
  .top(50)();

// Create item:
await sp.web.lists.getByTitle('Tasks').items.add({
  Title: 'New Task',
  Status: 'Not Started'
});

// Update item:
await sp.web.lists.getByTitle('Tasks').items.getById(itemId).update({
  Status: 'Completed'
});

// Parallel requests (no extra code):
const [user, listItems, siteTitle] = await Promise.all([
  sp.web.currentUser(),
  sp.web.lists.getByTitle('Tasks').items.top(10)(),
  sp.web.select('Title')()
]);

Tip: PnPjs v3+ uses tree-shaking-friendly imports — only import what you use to keep bundle size minimal. Always initialise PnPjs with SPFx context in the web part's onInit().


How do you call an Azure AD-secured internal API from SPFx?

// Declare in package-solution.json:
"webApiPermissionRequests": [{
  "resource": "MyInternalAPI",   // App registration display name
  "scope": "Data.Read"
}]

// After admin approval:
import { AadHttpClient } from '@microsoft/sp-http';

const client: AadHttpClient =
  await this.context.aadHttpClientFactory
    .getClient('api://myinternalapi-client-id');

const response = await client.get(
  'https://myapi.company.com/api/data',
  AadHttpClient.configurations.v1
);
const data = await response.json();

Tip: SPFx handles OAuth token acquisition transparently — no MSAL code needed. The framework gets and caches the token automatically.


5. Deployment, Packaging & ALM

What is the App Catalog and what are the two types?

Tenant App Catalog: one per SharePoint tenant. SPFx solutions deployed here can be made available to all sites. Managed by SharePoint/Global Admins. Supports tenant-wide deployment.

Site Collection App Catalog: one per site collection, enabled by admin. Allows site collection owners to deploy SPFx solutions scoped to their site only — without requiring tenant admin involvement.

// Enable tenant-wide deployment in package-solution.json:
{
  "solution": {
    "name": "my-solution",
    "id": "...",
    "version": "1.0.0.0",
    "skipFeatureDeployment": true   // ← enables tenant-wide deployment
  }
}

Tip: skipFeatureDeployment: true + Application Customizer + Tenant Wide Extensions list = zero-touch org-wide deployment pattern.


What is the difference between debug and ship bundles?

# Debug bundle (for development):
gulp bundle                    # non-minified, source maps, large file
gulp package-solution          # creates debug .sppkg

# Production bundle (for deployment):
gulp bundle --ship             # minified, tree-shaken, small file
gulp package-solution --ship   # creates production .sppkg

Critical: Always use --ship for production deployments. A debug bundle can be 3–5x larger than the ship bundle, causing slow page loads for all users.


What is CDN hosting for SPFx assets and why is it important?

SPFx JavaScript bundles and assets need to be hosted at a publicly accessible URL.

Option Description Cost
SharePoint CDN Assets served from SharePoint document library via Azure CDN Free (included)
Azure Blob + CDN Assets in Azure Blob Storage behind Azure CDN Azure costs
Embedded in .sppkg Assets bundled inside the .sppkg file Free but slower
# Enable Office 365 CDN:
Set-SPOTenantCdnEnabled -CdnType Public -Enable $true

# Configure CDN URL in write-manifests.json:
{
  "cdnBasePath": "https://yourtenant.sharepoint.com/sites/appcatalog/CDNFiles"
}

How do you version and upgrade an SPFx solution in production?

  1. Update version in package-solution.json (e.g., 1.0.0.0 → 1.1.0.0)
  2. Update version in the web part manifest (.manifest.json)
  3. Build and package: gulp bundle --ship && gulp package-solution --ship
  4. Upload new .sppkg to App Catalog — SharePoint detects the version change
  5. If skipFeatureDeployment: true: update propagates to all sites automatically
  6. If skipFeatureDeployment: false: site admins must go to Site Contents → find app → click Upgrade

Warning: The web part GUID must stay the same across all versions. Only the version number changes — changing the GUID creates a new web part and breaks all existing page instances.


How do you deploy extensions (Application Customizer) tenant-wide?

Approach 1 — Tenant Wide Extensions list (recommended):

  1. Upload .sppkg to Tenant App Catalog with skipFeatureDeployment: true
  2. Check "Make this solution available to all sites"
  3. Navigate to App Catalog site → "Tenant Wide Extensions" list
  4. Add new item:
    • Title: "Company Header"
    • Component ID: (Application Customizer GUID from manifest)
    • Component Properties: {"headerMessage":"Welcome"}
    • Location: ClientSideExtension.ApplicationCustomizer

Approach 2 — PnP PowerShell (for automation):

Add-PnPCustomAction -Name "CompanyHeader" -Title "Company Header" `
  -Location "ClientSideExtension.ApplicationCustomizer" `
  -ClientSideComponentId "a1b2c3d4-..." `
  -ClientSideComponentProperties '{"message":"Welcome"}' `
  -Scope Site

6. Performance & Best Practices

What are the key SPFx performance best practices?

Bundle size:

  • Always use --ship for production — minified and tree-shaken
  • Use tree-shaking-friendly imports (PnPjs v3, Fluent UI v8+)
  • Configure externals in config.json for libraries already loaded by SharePoint (React, ReactDOM, Fluent UI)
  • Avoid importing entire libraries — import only what you use

Data loading:

  • Use onInit() for one-time initialisation, not render-blocking data loads
  • Cache data in component state — avoid re-fetching on every render
  • Use Promise.all / PnPjs batching for parallel requests
  • Implement loading states with Fluent UI Spinner

CSS:

  • Use CSS Modules (.module.scss) to scope styles — prevents class name conflicts with other web parts on the page
  • Never use global CSS — it bleeds into SharePoint UI
  • Use Fluent UI theme variables instead of hardcoded colours — supports SharePoint theme changes

React:

  • Use functional components with hooks (not class components)
  • Memoize expensive calculations with useMemo
  • Avoid re-renders with useCallback for event handlers
  • Use React.lazy for code splitting large components

What are the SPFx externals and why are they important?

Externals in config/config.json tell webpack not to bundle specific libraries — instead loading them from the SharePoint page's global scope (which already has React, ReactDOM, and Fluent UI loaded).

// config/config.json:
"externals": {
  "react": "React",
  "react-dom": "ReactDom",
  "@fluentui/react": {
    "path": "https://res.cdn.office.net/files/fabric-react-8.29.0/office-ui-fabric-react.js",
    "globalName": "Fabric",
    "globalDependencies": ["react", "react-dom"]
  }
}

Why important: Without externals, React and Fluent UI are bundled into your web part JS file — adding ~200-500KB to the bundle. With externals, your bundle only contains your code — dramatically smaller and faster to load.


7. Scenario-Based Questions

Scenario: Build an SPFx web part that displays list items with filtering and paging.

Architecture:

  1. Property pane: PropertyFieldListPicker for list selection, PropertyPaneDropdown for filter column, PropertyPaneSlider for page size
  2. React component with useState/useEffect hooks:
const [items, setItems] = useState<IListItem[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [currentPage, setCurrentPage] = useState<number>(1);
const [error, setError] = useState<string>('');

useEffect(() => {
  const loadItems = async () => {
    setLoading(true);
    try {
      const sp = spfi().using(SPFx(props.context));
      const result = await sp.web
        .lists.getByTitle(props.listName)
        .items
        .select('Title', 'Status', 'AssignedTo/Title')
        .expand('AssignedTo')
        .filter(props.filterValue ? `Status eq '${props.filterValue}'` : '')
        .orderBy('Created', false)
        .top(props.pageSize)
        .skip((currentPage - 1) * props.pageSize)();
      setItems(result);
    } catch (err) {
      setError(`Failed to load items: ${err.message}`);
    } finally {
      setLoading(false);
    }
  };
  loadItems();
}, [currentPage, props.filterValue, props.listName]);
  1. Render: Fluent UI DetailsList for grid, Spinner for loading, MessageBar for errors, custom pagination controls

Scenario: Deploy a company-wide custom header to all SharePoint sites without touching each site.

  1. Build an Application Customizer that injects custom HTML into the Top placeholder
  2. Set skipFeatureDeployment: true in package-solution.json
  3. Build: gulp bundle --ship && gulp package-solution --ship
  4. Upload .sppkg to Tenant App Catalog → check "Make this solution available to all sites"
  5. Add entry to Tenant Wide Extensions list in App Catalog site with the customizer's GUID
  6. Result: all SharePoint sites in the tenant show the custom header — zero per-site action needed

Scenario: An SPFx web part needs to call an internal Azure API secured with Azure AD.

// 1. Register API in Azure AD — expose scope: api://myapi/Data.Read

// 2. package-solution.json:
"webApiPermissionRequests": [{
  "resource": "MyInternalAPI",
  "scope": "Data.Read"
}]

// 3. Admin approves in SharePoint Admin Center → Advanced → API Access

// 4. Call the API in the web part:
const client: AadHttpClient =
  await this.context.aadHttpClientFactory
    .getClient('api://myapi-app-registration-client-id');

const response = await client.get(
  'https://myapi.company.com/api/employees',
  AadHttpClient.configurations.v1
);
const employees = await response.json();

Scenario: How do you make an SPFx web part available as a Microsoft Teams tab?

// 1. Add Teams to supportedHosts in manifest:
"supportedHosts": [
  "SharePointWebPart",
  "TeamsTab",
  "TeamsPersonalApp"
]
// 2. Handle Teams context in web part:
public async onInit(): Promise<void> {
  if (this.context.sdks.microsoftTeams) {
    const teamsContext = this.context.sdks.microsoftTeams.context;
    console.log('Team ID:', teamsContext.team?.internalId);
    console.log('Channel ID:', teamsContext.channel?.id);
    console.log('User:', teamsContext.user?.userPrincipalName);
  }
}
3. Deploy to App Catalog with tenant-wide deployment
4. In App Catalog → find app → click "Sync to Teams"
   (auto-creates Teams app manifest from SPFx solution manifest)
5. In Teams → channel → + Add tab → find your app → configure

Scenario: How do you implement dark/light theme support in an SPFx web part?

// Use Fluent UI theme tokens in CSS Modules:
// MyWebPart.module.scss
.container {
  background: "[theme:bodyBackground, default:#ffffff]";
  color: "[theme:bodyText, default:#000000]";
  border: 1px solid "[theme:neutralLight, default:#edebe9]";
}

// In manifest — enable theme variants:
"supportsThemeVariants": true

// In web part class:
import { ThemeProvider, ThemeChangedEventArgs,
         IReadonlyTheme } from '@microsoft/sp-component-base';

protected onInit(): Promise<void> {
  const themeProvider = this.context.serviceScope
    .consume(ThemeProvider.serviceKey);
  this._themeVariant = themeProvider.tryGetTheme();
  themeProvider.themeChangedEvent.add(this,
    this._handleThemeChanged);
  return super.onInit();
}

private _handleThemeChanged(args: ThemeChangedEventArgs): void {
  this._themeVariant = args.theme;
  this.render();
}

8. Cheat Sheet — Quick Reference

Key Commands

# Scaffold
yo @microsoft/sharepoint

# Development
gulp serve --nobrowser

# Production build
gulp bundle --ship
gulp package-solution --ship

# Deploy
Upload .sppkg to App Catalog
Approve API permissions if needed

# Check environment
spfx doctor

API Client Quick Reference

// SharePoint REST:
await this.context.spHttpClient.get(url, SPHttpClient.configurations.v1)

// Microsoft Graph:
const client = await this.context.msGraphClientFactory.getClient('3');
await client.api('/me').get();

// Azure AD-secured API:
const client = await this.context.aadHttpClientFactory
  .getClient('api://your-app-id');
await client.get(url, AadHttpClient.configurations.v1);

// PnPjs (recommended wrapper):
const sp = spfi().using(SPFx(this.context));
await sp.web.lists.getByTitle('Name').items.top(10)();

Extension Types Reference

Application Customizer
  → Injects HTML into Top/Bottom placeholders
  → Runs on every page in activated site/tenant
  → Activated via site CustomActions or Tenant Wide Extensions list
  → Use for: global header, footer, notification banners, chat widgets

Field Customizer
  → Replaces column value rendering in list views
  → Bound to a specific column in a specific list
  → Use for: status badges, progress bars, icon columns, linked text

Command Set
  → Adds buttons to list toolbar and context menu
  → Can show/hide buttons based on selection count or item values
  → Use for: approve/reject actions, export, send for review

Adaptive Card Extension (ACE)
  → Cards on Viva Connections dashboard
  → Card view (compact) + Quick view (detail panel)
  → Use for: approvals, metrics, announcements, forms

Form Customiser
  → Replaces SharePoint default new/edit/display forms
  → Full React component — complete UI control
  → Use for: multi-step forms, complex validation, branded forms

Package-solution.json Key Settings

{
  "solution": {
    "name": "my-solution-client-side-solution",
    "id": "unique-guid-never-change",
    "version": "1.0.0.0",
    "includeClientSideAssets": true,    // embed assets in .sppkg
    "skipFeatureDeployment": true,       // enable tenant-wide deployment
    "isDomainIsolated": false,           // set true for isolated web parts
    "developer": {
      "name": "Your Company",
      "websiteUrl": "https://yourcompany.com"
    },
    "metadata": {
      "shortDescription": { "default": "Solution description" },
      "longDescription": { "default": "Long description" },
      "screenshotPaths": [],
      "videoUrl": "",
      "categories": []
    }
  },
  "webApiPermissionRequests": [
    { "resource": "Microsoft Graph", "scope": "User.Read" },
    { "resource": "Microsoft Graph", "scope": "Sites.Read.All" }
  ],
  "paths": {
    "zippedPackage": "solution/my-solution.sppkg"
  }
}

Deployment Checklist

Pre-deployment:
☐ Use --ship flag for bundle and package
☐ Verify bundle size is acceptable (<500KB ideal)
☐ Test in SharePoint workbench against real site
☐ Test in Teams if supportedHosts includes Teams
☐ Verify property pane controls work correctly
☐ Check theme support (light/dark/high contrast)
☐ Review Graph/API permissions — least privilege

Deployment:
☐ Upload .sppkg to correct App Catalog (tenant or site)
☐ Check "Make available to all sites" if tenant-wide
☐ Approve API permissions in Admin Center if needed
☐ Test in staging/UAT before production

Post-deployment:
☐ Verify web part appears in web part picker
☐ Test all property pane settings
☐ Verify API calls return correct data
☐ Check browser console for errors
☐ Test on mobile (SharePoint mobile app)

Top 10 Tips

  1. Never change the web part GUID after deployment — it breaks all existing page instances. Only the version number changes between releases.
  2. Always use --ship for production — debug bundles are 3–5x larger. Forgetting --ship is the most common deployment mistake.
  3. this.context.pageContext.web.absoluteUrl — never hardcode site URLs. Always use the context for the current site URL.
  4. PnPjs over raw SPHttpClient — more concise, typed, handles errors better, supports batching. Know how to initialise it with SPFx context.
  5. Graph API permissions are tenant-wide — once approved, any SPFx solution in the tenant can use them. Always use least privilege scopes.
  6. skipFeatureDeployment: true + Tenant Wide Extensions — the canonical pattern for org-wide Application Customizer deployment. Know this by heart.
  7. Reactive vs non-reactive property pane — non-reactive is needed when property changes trigger API calls. Reactive fires on every keystroke.
  8. Service Scope for DI — how SPFx enables testability and service reuse. Know how to define a service key and consume it.
  9. CSS Modules — always scope styles with .module.scss. Global CSS bleeds into SharePoint UI and other web parts on the page.
  10. Teams integration — add TeamsTab to supportedHosts, handle this.context.sdks.microsoftTeams, use "Sync to Teams" in App Catalog. Three steps, always the same pattern.


Monday, April 27, 2026

Power Platform Governance & COE Complete Guide

 

Power Platform Governance & COE — Complete Guide

Governance Fundamentals · DLP Policies · Environment Strategy · COE Starter Kit · ALM · Pipelines · Scenarios · Cheat Sheet


Table of Contents

  1. Governance Fundamentals
  2. Data Loss Prevention (DLP) Policies
  3. Environment Strategy & Lifecycle
  4. COE Starter Kit & Adoption
  5. ALM & Deployment Pipelines
  6. Monitoring, Auditing & Compliance
  7. Scenario-Based Questions
  8. Cheat Sheet — Quick Reference

1. Governance Fundamentals

What is Power Platform governance and why does it matter?

Power Platform governance is the set of policies, processes, and tools that ensure the platform is used securely, efficiently, and in alignment with organisational standards.

Without governance:

  • Users create apps connecting to sensitive data without security reviews
  • Connectors expose corporate data to external services (consumer apps, personal accounts)
  • Hundreds of unmanaged environments consume capacity and storage with no oversight
  • Critical business processes run on flows owned by individuals who leave the organisation
  • Compliance violations occur when personal data flows to unsanctioned services

Key insight: The governance challenge is unique to Power Platform — it is a self-service platform by design. The goal is to enable innovation while preventing data leakage, shadow IT, and orphaned resources.


What are the three pillars of Power Platform governance?

Pillar Focus Areas
Security & Compliance DLP policies, connector restrictions, Azure AD conditional access, tenant isolation, data residency, sensitivity labels
Environment Management Environment strategy (dev/test/prod), capacity planning, lifecycle (creation, monitoring, cleanup), access control
Adoption & Enablement COE governance, training programmes, maker communities, app catalogues, support models, usage analytics

Tip: Governance without adoption = platform stagnation. Adoption without governance = security risk. Balancing the two is the central tension in Power Platform architecture.


What admin roles exist for Power Platform and what can each do?

Role Scope Use Case
Global Administrator Full M365 + Power Platform Too broad — avoid for regular admin
Power Platform Administrator All environments, DLP, capacity, connectors, tenant settings Primary Power Platform admin role
Dynamics 365 Administrator D365 environments specifically D365-focused admin
Environment Administrator One specific environment Delegated env-level management
System Administrator (Dataverse role) Full access within one Dataverse environment Customise, configure, manage data

Warning: Never assign Global Administrator for Power Platform administration. Use Power Platform Administrator — it has the right scope without unnecessary M365 access.


What is the Power Platform Admin Center and what can you manage from it?

The Power Platform Admin Center (admin.powerplatform.microsoft.com) is the central management portal.

Capability Description
Environments Create, configure, copy, reset, delete environments. Manage capacity and storage.
DLP policies Create and manage Data Loss Prevention policies across tenant and environments.
Analytics Usage reports for Power Apps, Power Automate, Copilot Studio across the tenant.
Capacity Monitor Dataverse storage, file storage, log storage per environment.
Connectors View and manage custom connectors across the tenant.
Tenant settings Control who can create environments, AI features, sharing settings.
Support Raise and manage Microsoft support tickets.

What are the most important tenant-level settings for governance?

  1. Who can create production/sandbox environments — restrict to admins only. Prevents environment sprawl.
  2. Who can create trial environments — balance exploration vs sprawl.
  3. Power Apps/Power Automate for M365 users — enable/disable for specific Azure AD groups.
  4. AI features (Copilot) — enable/disable generative AI features tenant-wide.
  5. Sharing canvas apps — restrict who users can share apps with (organisation, specific groups, admins only).
  6. Weekly digest emails to makers — notify makers when their resources are flagged.

Tip: Restricting environment creation to admins only is the single most impactful governance setting — it prevents the #1 governance problem: uncontrolled environment sprawl.


2. Data Loss Prevention (DLP) Policies

What are DLP policies in Power Platform and how do they work?

DLP policies control which connectors can be used together in a flow or app. They prevent sensitive business data from being exfiltrated to unsanctioned services by enforcing connector grouping rules.

Three connector groups:

Group Description Examples
Business Approved connectors for business data SharePoint, Dataverse, Teams, Outlook, SQL Server, Azure
Non-business (Personal) Consumer/personal connectors Twitter, Facebook, Gmail, personal OneDrive, Dropbox
Blocked Cannot be used at all in this environment High-risk or unvetted connectors

A flow or app can only use connectors from one group — Business OR Non-business, never both together. This prevents a flow from reading corporate SharePoint data and writing it to a personal Gmail.

Critical: An environment with no DLP policy allows any combination of connectors — corporate data can flow freely to consumer services. Always apply DLP before allowing maker access to an environment.


What is the difference between tenant-scoped and environment-scoped DLP policies?

Tenant-scoped DLP policy: applies to ALL environments in the tenant (or all except explicitly excluded ones). Set by Power Platform Administrator. Cannot be overridden by environment admins. Used for baseline security.

Environment-scoped DLP policy: applies to one or more specific environments. Can be set by Power Platform Admins or Environment Admins. Can be more restrictive than the tenant policy but never more permissive — tenant policy always wins.

Tenant policy (baseline):         Environment policy (stricter):
Business: SharePoint, Teams        Business: SharePoint only
Non-business: Gmail, Twitter       Blocked: Teams, Gmail, Twitter

Effective result for that environment:
= most restrictive combination of both policies
→ SharePoint in Business, Teams blocked, Gmail blocked

Tip: Always create a baseline tenant-wide DLP policy first. Then add environment-specific policies for additional restrictions in sensitive or production environments.


What are connector action controls in DLP policies?

Connector action controls allow admins to permit or block specific actions within a connector — rather than blocking the entire connector. This provides fine-grained control.

SharePoint connector action controls:
Allow: Get items, Get item, Create item, Update item
Block: Delete item, Delete attachment

HTTP connector action controls:
Allow: GET requests only
Block: POST, PUT, DELETE requests
→ Allows reading external APIs but prevents writing data out

Dataverse connector action controls:
Allow: List rows, Get row, Create row, Update row
Block: Delete row, Execute action, Perform bulk operations

Tip: Connector action controls are a governance maturity milestone — they allow enabling a connector for legitimate use while preventing its abuse. Much more nuanced than all-or-nothing connector blocking.


What are endpoint filtering rules in DLP policies?

Endpoint filtering rules restrict which specific URLs or hosts the HTTP connector, custom connectors, or certain other connectors can connect to.

HTTP connector endpoint rules:
Allow list:
  *.internal-company-api.com
  https://api.approvedsystem.com
Deny list (default):
  * (all other endpoints blocked)

Use case: allow Power Automate to call only internal
APIs — block calls to external/consumer services
via HTTP action even with custom endpoints

How do DLP policies interact with existing flows when a new policy is applied?

  1. When a DLP policy is created or changed, existing flows are evaluated against the new policy
  2. Flows that violate the new policy are suspended — they stop running immediately
  3. The flow owner receives an email notification that their flow has been suspended
  4. The flow remains suspended until: the policy is changed to permit the connectors, OR the flow is modified to comply
  5. Power Platform Admin Center shows which flows are suspended and which policy caused it

Warning: Applying a new restrictive DLP policy to an existing environment can immediately suspend production flows. Always audit existing flows before applying a new policy — use the COE Starter Kit inventory to identify potentially affected flows first.


3. Environment Strategy & Lifecycle

What is environment strategy in Power Platform and what are the recommended patterns?

Environment strategy defines how environments are structured, purposed, and managed across the organisation.

Recommended environment tiers:

Tier Purpose Access DLP
Default Personal M365 productivity All licensed users (restricted) Strict — M365 connectors only
Developer Individual maker experimentation One maker only Restrictive
Shared sandbox Project development & testing Project team Standard business connectors
Test/UAT Pre-production validation QA team + business owners Production-equivalent
Production Live business applications End users (read), admins Most restrictive
COE/Admin Governance tools and admin flows COE team only Permissive (for admin connectors)

How do you prevent environment sprawl in a large organisation?

  1. Restrict environment creation — set "Who can create production and sandbox environments" to admins only in tenant settings
  2. Environment request process — implement a formal request flow (Power Automate approval). COE Starter Kit includes this out of the box.
  3. Environment lifecycle policies — auto-identify environments with no activity for 90 days via COE analytics → notify owners → delete if no response
  4. Trial environment controls — trial environments auto-expire after 30 days. Ensure cleanup is monitored.
  5. Regular audits — monthly review of all environments via COE Starter Kit inventory

Tip: Environment sprawl is the #1 governance problem in large tenants. Microsoft reports that uncontrolled tenants can have 1000+ environments — most unused. Proactive controls prevent this from the start.


What is the Default environment and what special considerations does it have?

Characteristic Detail
Existence Every tenant has exactly one — cannot be deleted
Membership All licensed users automatically added as Environment Makers
M365 flows Teams, Outlook, SharePoint-triggered flows run here by default
Risk level Highest risk — anyone can create apps and flows here

Governance recommendations:

  1. Apply strict DLP — allow only M365/productivity connectors, block all premium and personal connectors
  2. Remove Environment Maker role from all users — grant only to approved individuals
  3. Rename to signal its purpose: "Default — Personal Productivity Only"
  4. Use COE Starter Kit to monitor and clean up unused resources regularly

Critical: The Default environment is the highest-risk environment in any tenant. Without governance it becomes a dumping ground for production apps with no oversight.


How do you handle environment capacity and storage management?

Three Dataverse storage types:

Type Contains
Database storage Dataverse table data, relationships, metadata
File storage Attachments, images, file columns
Log storage Audit logs, plugin trace logs

Capacity management practices:

  1. Monitor via Admin Center → Capacity → Summary
  2. Set storage alerts — get notified before hitting capacity limits
  3. Regularly purge audit logs older than the retention period
  4. Run Dataverse bulk delete jobs for old/inactive records
  5. Use Azure Blob or SharePoint for file storage instead of Dataverse file columns where possible
  6. Archive inactive environments' Dataverse data before deletion

4. COE Starter Kit & Adoption

What is the COE Starter Kit and what does it provide?

The Centre of Excellence (COE) Starter Kit is a free, open-source collection of Power Platform components published by Microsoft that helps organisations govern, monitor, and nurture their Power Platform adoption.

Key modules:

Module Description
Core components Inventory of all apps, flows, connectors, environments, and makers across the tenant. Synced daily via admin APIs.
Governance components Compliance processes — maker compliance emails, app archival, orphaned resource cleanup, environment request flows.
Nurture components Adoption tools — maker onboarding, training resources, app showcase/gallery, hackathon management, community tools.
Audit & compliance Risk assessment of apps/flows, DLP violation reporting, sensitivity analysis.
Power BI dashboard Tenant-wide analytics — top makers, most used apps, connector usage, risk scores, environment health.

Tip: The COE Starter Kit transforms Power Platform administration from reactive firefighting into proactive governance. It is the expected answer to "how do you govern Power Platform at scale."


What does the COE Core inventory collect and how does it work?

A scheduled cloud flow runs daily using the Power Platform Admin connector and Management APIs:

API calls:
Get Environments           → all environments in tenant
Get Apps as Admin          → all canvas and model-driven apps
Get Flows as Admin         → all cloud flows
Get Connectors as Admin    → all connectors used
Get Makers                 → all users who have created resources

Stored in Dataverse tables:
admin_Environment          → all environments
admin_PowerApp             → all canvas and model-driven apps
admin_Flow                 → all cloud flows
admin_Connector            → all connectors used
admin_Maker                → all users who have created resources
admin_PowerAppConnector    → connector usage per app
admin_FlowConnector        → connector usage per flow

Warning: The COE inventory sync requires a service account with Power Platform Administrator role. Use a dedicated service account — never a personal admin account (single point of failure risk).


What is the compliance process in the COE Governance module?

The governance module automates a compliance conversation with makers about their apps and flows:

  1. Admin emails makers of apps/flows that haven't been reviewed — asking for business justification, data classification, and owner confirmation
  2. Maker fills in a compliance form: app purpose, data sensitivity level, business owner, support contact
  3. If no response within the grace period: app/flow is quarantined (shared access removed)
  4. If still no response: app/flow is deleted (with backup)
  5. Compliant resources are tagged and exempted from future compliance sweeps

Tip: This automated compliance process cleans up years of accumulated shadow IT without manual effort. The grace period approach avoids disrupting legitimate users while enforcing governance.


What does a Power Platform Centre of Excellence team look like?

Three layers of a mature COE:

Layer Composition Responsibilities
Central COE team 2–5 people (Architect, Admin Lead, Enablement Lead) Platform strategy, DLP, environment strategy, security standards, COE Starter Kit
Champions network Power users/citizen developers in each BU First-line support, evangelism, quality gatekeeping for department apps
Professional developers IT/dev team Complex solutions requiring pro-code extensions (PCF, plugins, custom APIs)

Key principle: The COE's role is to enable — not gatekeep. The goal is a "managed self-service" model where makers can build freely within guardrails, not a bottleneck where everything goes through central IT.


What is the maker onboarding process in a governed environment?

  1. User requests maker access via a Power Automate approval flow
  2. Manager approves — confirms the maker needs to build on the platform
  3. Automated onboarding: assigned Environment Maker role in appropriate dev environment, added to makers Azure AD group, added to maker community Teams channel
  4. Welcome email with training resources, DLP policy overview, coding standards, ALM process documentation
  5. Mandatory Power Platform fundamentals training before first app published to production
  6. COE inventory automatically tracks the new maker and their subsequent resource creation

5. ALM & Deployment Pipelines

What is ALM for Power Platform and what does it involve?

ALM (Application Lifecycle Management) covers the processes and tools for developing, testing, and deploying Power Platform solutions in a controlled, repeatable way.

Core ALM elements:

Element Description
Source control Solution files stored in Git (Azure DevOps or GitHub) — tracked, versioned, peer-reviewed
Solution packaging All components in a managed solution (apps, flows, tables, connection references, env variables)
CI/CD pipelines Automated export → unpack → commit → build → deploy
Environment promotion Dev → Test/UAT → Production with gated deployments
Connection references Environment-agnostic connection pointers
Environment variables Environment-specific configuration values

What is the Power Platform Build Tools and how is it used in CI/CD?

Power Platform Build Tools is an Azure DevOps extension (and GitHub Actions equivalent) providing pipeline tasks for automating Power Platform ALM.

# Typical CI pipeline (on commit):
- task: PowerPlatformToolInstaller@2
- task: PowerPlatformExportSolution@2
    inputs:
      authenticationType: 'PowerPlatformSPN'
      Environment: '$(DevEnvironmentUrl)'
      SolutionName: 'MyAppSolution'
      SolutionOutputFile: '$(Build.ArtifactStagingDirectory)/solution.zip'
- task: PowerPlatformUnpackSolution@2
    inputs:
      SolutionInputFile: '$(Build.ArtifactStagingDirectory)/solution.zip'
      SolutionTargetFolder: '$(Build.SourcesDirectory)/solutions/MyAppSolution'
- task: PowerPlatformChecker@2
    inputs:
      SolutionInputFile: '$(Build.ArtifactStagingDirectory)/solution.zip'

# Typical CD pipeline (to Production):
- task: PowerPlatformPackSolution@2
- task: PowerPlatformImportSolution@2
    inputs:
      Environment: '$(ProdEnvironmentUrl)'
      SolutionInputFile: '$(Pipeline.Workspace)/solution.zip'

What are Power Platform Pipelines (in-product) vs Azure DevOps pipelines?

Power Platform Pipelines Azure DevOps / GitHub Actions
Setup Native in Admin Center External DevOps tool required
Audience Makers, citizen developers Professional developers/DevOps
Customisation Limited Fully customisable
Testing Manual Automated test support
Best for Simple solutions, maker-led deployments Complex solutions, pro-code components, enterprise DevOps

Tip: Most mature enterprise teams use Azure DevOps for complex solutions and Power Platform Pipelines for simpler maker-built apps. Both can coexist in the same tenant.


What is Solution Checker and why should it be part of every deployment pipeline?

Solution checker analyses a Power Platform solution against best practice rules and returns issues by severity (Critical, High, Medium, Low, Informational).

What it checks:

  • Plugin and custom workflow code quality (deprecated APIs, missing error handling)
  • JavaScript web resource issues (deprecated client API usage)
  • Canvas app formula issues (delegation warnings, performance patterns)
  • Solution structure issues (missing dependencies, invalid references)
# Run via CLI in pipeline:
pac solution check --path ./solution.zip \
  --outputDirectory ./checker-results \
  --rulesetId 0ad12346-e108-40b8-a956-9a373e549abb

# Gate deployment:
# Critical issues > 0 → fail pipeline, block deployment
# High issues > threshold → require manual approval

Tip: Solution checker as a mandatory CI quality gate is the difference between a governed and ungoverned ALM process. It catches technical debt before it reaches production.


What are connection references and environment variables and why are they critical for ALM?

Connection references: solution components acting as pointers to connections. Flows reference a Connection Reference instead of a hardcoded connection. After solution import, each environment's Connection Reference is updated to point to the appropriate connection.

Environment variables: solution components storing configuration values that differ per environment (API URLs, email addresses, SharePoint site IDs, feature flags).

WITHOUT Connection References/Env Variables (broken ALM):
→ Flow hardcodes connection to dev SharePoint site
→ Import to prod → flow still points to dev site
→ Must manually edit flow in each environment
→ Error-prone, time-consuming, not repeatable

WITH Connection References/Env Variables (proper ALM):
→ Flow references "SharePoint Connection Reference"
→ Import to prod → update Connection Reference to prod SharePoint
→ Set env variable: SiteURL = "https://prod.sharepoint.com/sites/app"
→ No flow edits needed — fully automated, environment-agnostic deployment

Critical: Not using Connection References and Environment Variables is the #1 cause of broken Power Platform deployments. Every solution component with environment-specific configuration MUST use these.


6. Monitoring, Auditing & Compliance

What analytics are available natively in Power Platform Admin Center?

Report Metrics
Power Apps analytics Active users, app launches, sessions, errors, service performance
Power Automate analytics Flow runs, success/failure rates, run durations, top flows
Copilot Studio analytics Sessions, resolution rates, escalation rates per copilot
Capacity Dataverse storage per environment, tenant-wide consumption
Connector usage Which connectors are used, by which environments, top users

For advanced analytics beyond the built-in reports, use the COE Starter Kit Power BI dashboard which aggregates inventory, usage, risk, and compliance data across the entire tenant.


How do you audit Power Platform activity for compliance?

Microsoft Purview (formerly Compliance Center):

  • Power Platform operations are logged in the Microsoft 365 Unified Audit Log
  • Auditable events: app creation/deletion, flow creation/deletion, environment creation, DLP policy changes, permission changes
  • Accessible via Microsoft Purview compliance portal → Audit → Search

Power Platform Admin Center:

  • Environment-level activity logs
  • Admin activity logs (who changed DLP policies, who created environments)

Dataverse auditing:

  • Record-level create/update/delete/access tracking
  • Field-level change history
  • Accessible via audit history on records or Dataverse Web API

What is the Power Platform for Admins connector and how is it used for governance automation?

The Power Platform for Admins connector provides actions for programmatic management of Power Platform resources — used extensively in COE Starter Kit flows.

Key actions:
Get Environments                → list all environments
Create Environment              → provision new environments
Delete Environment              → remove environments
Get Apps as Admin               → inventory all canvas apps
Get Flows as Admin              → inventory all flows
Get Connectors as Admin         → inventory all connectors
Suspend Flow as Admin           → disable a flow remotely
Get DLP Policies                → list all DLP policies

Common governance automation patterns:
→ Daily inventory sync (COE core)
→ Auto-suspend flows violating new DLP policy
→ Send weekly digest to makers of their resource usage
→ Auto-delete trial environments after 30 days
→ Alert when storage exceeds 80% of capacity
→ Notify when a new environment is created in the tenant

7. Scenario-Based Questions

Scenario: A new CISO wants to ensure no corporate data leaks to consumer services via Power Automate. What do you implement?

  1. Tenant-wide baseline DLP policy: move all Microsoft business connectors (SharePoint, Dataverse, Teams, Outlook, SQL Server, Azure services) to Business group. Move all consumer connectors (Gmail, Twitter, Facebook, personal OneDrive, Dropbox) to Non-business. Block high-risk connectors.
  2. Production environment DLP policy: additional restriction — only Microsoft-approved connectors in Business group. Block all non-Microsoft connectors.
  3. HTTP connector endpoint filtering: restrict HTTP action to only approved internal API endpoints.
  4. Connector action controls: on sensitive connectors like Dataverse, allow Read operations but restrict bulk Delete or Export actions.
  5. COE inventory monitoring: daily review of new connectors used across the tenant. Alert when a blocked connector is attempted.
  6. DLP violation reports: weekly report from COE to CISO showing suspended flows, violation trends, and remediation status.

Warning: Exclude the COE environment from the tenant DLP policy — it needs broader connector access for admin flows. Use an explicit exclusion list.


Scenario: Your organisation has 800 environments after 3 years of ungoverned Power Platform usage. How do you clean this up?

  1. Deploy COE Starter Kit — get full inventory of all 800 environments: owners, last activity, resource counts, storage consumption
  2. Categorise: Active (resources used < 90 days), Stale (90–180 days inactive), Abandoned (180+ days inactive)
  3. Owner notification campaign: automated email — "Your environment will be deleted in 30 days unless you confirm it is still needed"
  4. Stale environment archival: export all resources to solutions, back up Dataverse data, delete the environment
  5. Abandoned environment deletion: no owner response + no active resources → delete after backup
  6. Restrict new creation: simultaneously change tenant setting to admin-only environment creation — prevent new sprawl while cleaning up old
  7. Implement request process: going forward, all new environments require formal request with business justification approved by COE team

Tip: Tackle in waves — start with clearly abandoned environments (no activity, no resources), then stale. Never delete without backup and owner notification.


Scenario: A business-critical flow in production is owned by an employee who has left. How do you handle it?

Immediate remediation:

  1. Use Power Platform Admin role to reassign flow ownership to a service account via Admin Center → Flows → Edit owner
  2. Check the flow's connections — departed user's personal connections will be broken. Replace with service account connections or shared Connection References.
  3. Check if the flow uses the departed user's credentials (SharePoint, Outlook) — replace with service account or delegated credentials
  4. Test the flow end-to-end after ownership and connection transfer

Preventive measures:

  1. COE governance policy: all production flows must be owned by a service account or team, not an individual
  2. COE compliance process: quarterly audit of flow ownership — flag production flows owned by individuals
  3. IT offboarding checklist: include a Power Platform asset transfer step — reassign all flows/apps before account deletion

Critical: Flow connections tied to personal user accounts are the #1 cause of production outages when staff leave. This is a governance failure that COE ownership policies prevent.


Scenario: How do you set up a complete Dev → Test → Production ALM pipeline for a Power Apps solution?

Setup:

  1. Three environments: Dev (sandbox), Test (sandbox), Production. Separate DLP policies per environment.
  2. Dedicated service principal for pipeline authentication (not a personal admin account).
  3. Unmanaged solution in Dev. All components — app, flows, tables, connection references, environment variables.
  4. Azure DevOps repo with branch strategy: feature branches → main → release branches.

CI pipeline (on commit to feature branch):

  • Export unmanaged solution from Dev environment
  • Unpack to source files → commit to repo
  • Run Solution Checker → fail build on Critical issues

CD pipeline — to Test (on merge to main):

  • Pack as managed solution
  • Import to Test environment
  • Set Connection References and Environment Variables for Test
  • Run automated smoke test flow
  • Notify QA team for UAT

CD pipeline — to Production (manual trigger + approval gate):

  • Same managed solution promoted from Test
  • Required approval from business owner in Azure DevOps
  • Import to Production
  • Set Production Connection References and Environment Variables
  • Post-deployment Teams notification to stakeholders

Scenario: How do you handle a maker who has built a production app in the Default environment using premium connectors?

Problem: Maker built a business-critical app in the Default environment using Dataverse — violating governance policy. Moving it is risky.

  1. Assess impact: use COE inventory to identify all users of the app, data connections, and dependencies
  2. Create a proper production environment: provision a new Production environment with correct DLP policy and security configuration
  3. Export the app as a solution: package the app, its flows, Dataverse tables, and connection references into a managed solution
  4. Migrate Dataverse data: use Data Export Service or Power Automate to copy data from Default to the new production environment's Dataverse
  5. Redirect users: update the app URL, update any embedded links, communicate the change to users with a transition period
  6. Delete or archive original: quarantine the Default environment copy first, verify all users are on the new app, then delete
  7. Update governance policy: document this scenario in the maker guidelines — new apps requiring Dataverse must request a proper environment from the start

8. Cheat Sheet — Quick Reference

DLP Policy Configuration Reference

Connector groups:
Business     → approved for corporate data
Non-business → consumer/personal services
Blocked      → cannot be used at all

Policy scope:
Tenant-wide  → applies to ALL environments (with optional exclusions)
Environment  → applies to specific environments only
Precedence   → most restrictive policy wins

Key connectors to classify:
Business:    SharePoint, Dataverse, Teams, Outlook, OneDrive for Business,
             SQL Server, Azure Blob, Azure Service Bus, Azure Key Vault,
             Power BI, Dynamics 365, Microsoft Forms
Non-business: Gmail, Twitter/X, Facebook, Dropbox, personal OneDrive,
              Google Sheets, Slack (personal), Trello
Blocked:     HTTP (without endpoint filtering), custom connectors
             to unapproved endpoints

DLP violation outcome:
→ Existing flows: suspended immediately
→ New flows: cannot be saved/published
→ Owner notified by email with policy name and violation details

Environment Strategy Reference

Environment types and purposes:
Default      → personal M365 productivity. Strict DLP. Remove Maker role.
Developer    → individual maker sandbox. Free with Developer Plan.
Sandbox      → team dev/test. Reset-able. Not for production data.
Production   → live business data. Full backup. Strict access control.
COE/Admin    → governance tools only. Permissive DLP for admin connectors.

Creation governance:
→ Restrict to admins only (tenant setting)
→ Request process: maker submits → manager approves → admin provisions
→ Mandatory fields: business owner, purpose, expected lifetime, data classification

Lifecycle:
90 days inactive   → automated owner notification
120 days inactive  → quarantine (access suspended)
150 days inactive  → deletion (with backup)
Trial environments → auto-expire after 30 days

ALM Pipeline Checklist

Solution setup:
☐ Custom publisher prefix (not 'new_')
☐ All components in solution
☐ Connection References for all connectors
☐ Environment Variables for all env-specific config
☐ No hardcoded connection strings or URLs

CI pipeline:
☐ Export solution from Dev
☐ Unpack to source files
☐ Commit unpacked files to Git repo
☐ Run Solution Checker
☐ Fail on Critical issues

CD to Test:
☐ Pack managed solution
☐ Import to Test environment
☐ Configure Connection References
☐ Set Environment Variable values
☐ Run smoke test / automated tests
☐ Notify QA for UAT

CD to Production:
☐ Manual approval gate (business owner sign-off)
☐ Import same managed solution from Test
☐ Configure Production Connection References
☐ Set Production Environment Variable values
☐ Post-deployment notification
☐ Monitor for errors in first 24 hours

COE Starter Kit — Key Components

Core module:
→ Daily inventory sync (flows, apps, envs, makers, connectors)
→ Admin Power BI dashboard
→ Environment overview report

Governance module:
→ Compliance flow (maker emails, grace periods, quarantine, delete)
→ Environment request and approval process
→ Developer environment provisioning automation
→ Orphaned resource identification and cleanup
→ DLP violation reporting

Nurture module:
→ Maker onboarding welcome email automation
→ App gallery / showcase
→ Training resource hub
→ Maker community management
→ Hackathon toolkit

Admin connector actions used:
Get/List Environments, Apps, Flows, Connectors, Makers
Suspend Flow as Admin
Delete Environment
Add/Remove Environment Maker role

Governance Maturity Model

Level 1 — Reactive (no governance):
→ Default environment used for everything
→ No DLP policies
→ No inventory visibility
→ Issues discovered only when things break

Level 2 — Basic (foundational):
→ Tenant-wide DLP policy applied
→ Environment creation restricted to admins
→ COE Starter Kit deployed (inventory only)
→ Maker onboarding process defined

Level 3 — Managed (operational):
→ Environment strategy implemented (dev/test/prod)
→ DLP policies per environment type
→ COE governance module running (compliance emails)
→ ALM pipeline for key solutions
→ Regular environment audits

Level 4 — Optimised (mature):
→ Full COE with champions network
→ Automated environment lifecycle management
→ Solution Checker in CI/CD pipelines
→ Connection References + Env Variables everywhere
→ Monthly governance reviews with metrics
→ Risk scoring and remediation tracking
→ Fusion team model (makers + pro devs + IT)

Top 10 Tips

  1. DLP = connector grouping, not content scanning — DLP in Power Platform prevents connectors from being combined, not scanning message content like M365 DLP. Clarify this distinction confidently.
  2. Tenant policy wins over environment policy — environment policies can only be more restrictive, never more permissive. The tenant baseline always applies.
  3. Restrict environment creation first — this is the single most impactful governance action. Mention it in every environment sprawl question.
  4. Default environment is highest risk — every licensed user is a maker there by default. Always recommend removing the Maker role and applying strict DLP.
  5. Connection References are mandatory for ALM — hardcoded connections are the #1 cause of broken deployments. Mentioning this signals real deployment experience.
  6. COE Starter Kit is the governance answer at scale — know its three modules (Core, Governance, Nurture) and what each does. It is the expected answer to "how do you govern Power Platform."
  7. Service accounts for production flows/apps — never personal user accounts.
  8. Solution Checker in CI pipelines — quality gate that catches technical debt before production. A maturity signal in any ALM discussion.
  9. DLP violations suspend existing flows — a new policy change immediately impacts running flows. Always audit before applying a new policy.
  10. Governance vs adoption balance — Governance that blocks legitimate use is a failure just as much as no governance at all. The goal is "managed self-service."


Featured Post

Connect to SharePoint Online with Connect-PnPOnline

Goal: Starting from a fresh Windows PC, finish by running one PowerShell command that reads the title of a SharePoint site . Who it's f...

Popular posts