Modern SharePoint pages are often the “front door” for users who need quick answers, guided navigation, and structured actions (create/update list items, open documents, etc.). Microsoft Copilot Studio lets you build an agent that can be published to the web and to SharePoint—then embedded into your portal experience.
Embedding a Microsoft Copilot Studio Agent in SharePoint as a Web Part (SPFx)
Modern SharePoint pages are often the “front door” for users who need quick answers, guided navigation, and structured actions (create/update list items, open documents, etc.). Microsoft Copilot Studio lets you build an agent that can be published to the web and to SharePoint—then embedded into your portal experience.
This article covers two proven patterns:
- Pattern A (Fast): Embed the agent using the Copilot Studio Web channel iframe inside an SPFx web part.
- Pattern B (Enterprise): Build a custom chat UI in SPFx using Bot Framework Web Chat with Direct Line tokens generated server-side (no secrets in the browser).
It also mentions a third option: publishing directly via the Copilot Studio SharePoint channel, which may eliminate the need for SPFx in some scenarios.
1) Pattern A — Embed the agent via iframe (fastest path)
When to choose this
Use iframe embedding when:
- you want a working experience quickly,
- UI customization is not critical,
- your organization is okay with the agent being hosted via the Copilot Studio embed surface.
Microsoft documents that you can publish your agent to your own website using an iframe code snippet, and that the demo website is for testing—not production. (Microsoft Learn)
Step-by-step
- In Copilot Studio, publish your agent and open Channels → Web.
- Copy the iframe embed snippet. (Microsoft Learn)
- Extract the
src="..."URL. - Create an SPFx web part that renders an
<iframe>with that URL.
SPFx code (React) — iframe web part
CopilotEmbed.tsx
import * as React from "react";export interface ICopilotEmbedProps { iframeSrc: string; heightPx: number;}export const CopilotEmbed: React.FC<ICopilotEmbedProps> = ({ iframeSrc, heightPx }) => { if (!iframeSrc) { return ( <div style={{ padding: 12 }}> <strong>Copilot agent not configured.</strong> <div>Paste the Copilot Web embed URL (iframe src) in the web part properties.</div> </div> ); } return ( <div style={{ width: "100%" }}> <iframe title="Copilot Studio Agent" src={iframeSrc} style={{ width: "100%", height: `${heightPx}px`, border: "0", borderRadius: "8px", }} allow="clipboard-read; clipboard-write" /> </div> );};
CopilotEmbedWebPart.ts
import * as React from "react";import * as ReactDom from "react-dom";import { Version } from "@microsoft/sp-core-library";import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";import { IPropertyPaneConfiguration, PropertyPaneTextField, PropertyPaneSlider } from "@microsoft/sp-property-pane";import { CopilotEmbed } from "./components/CopilotEmbed";export interface ICopilotEmbedWebPartProps { iframeSrc: string; heightPx: number;}export default class CopilotEmbedWebPart extends BaseClientSideWebPart<ICopilotEmbedWebPartProps> { public render(): void { const element = React.createElement(CopilotEmbed, { iframeSrc: this.properties.iframeSrc, heightPx: this.properties.heightPx ?? 700, }); ReactDom.render(element, this.domElement); } protected onDispose(): void { ReactDom.unmountComponentAtNode(this.domElement); } protected get dataVersion(): Version { return Version.parse("1.0"); } protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { return { pages: [ { header: { description: "Copilot Studio Agent Embed" }, groups: [ { groupName: "Settings", groupFields: [ PropertyPaneTextField("iframeSrc", { label: "Copilot Web embed URL (iframe src)", multiline: true, }), PropertyPaneSlider("heightPx", { label: "Height (px)", min: 400, max: 1200, value: this.properties.heightPx ?? 700, }), ], }, ], }, ], }; }}
Important note: authentication affects whether embed code is visible
Microsoft notes that the iframe embed code visibility depends on the agent authentication option (for example, “No authentication” vs “Authenticate with Microsoft/manually”). (Microsoft Learn)
2) Pattern B — Custom SPFx chat UI with Direct Line tokens (recommended for enterprise portals)
Why this pattern exists
Copilot Studio uses Bot Framework Direct Line to connect a web page/app to the agent. (Microsoft Learn)
The problem is security: you must not expose secrets in browser-delivered code.
Microsoft explicitly warns not to expose the Direct Line secret in code that runs in the browser and states that acquiring a token using the secret in service code is the safest approach. (Microsoft Learn)
So the enterprise-grade pattern is:
SPFx (browser) → calls → Azure Function (server) → calls → Direct Line token endpoint → returns token → browser uses token for chat
What Direct Line expects
Microsoft’s Direct Line docs describe authenticating using either a secret or a token, passed via Authorization: Bearer .... (Microsoft Learn)
They also document generating a token using:
POST https://directline.botframework.com/v3/directline/tokens/generate with Authorization: Bearer SECRET. (Microsoft Learn)
Step-by-step
- In Copilot Studio, open Settings → Security → Web channel security and retrieve your secret (server-side only). (Microsoft Learn)
- Deploy an Azure Function that calls Direct Line
/tokens/generateusing the secret stored as an environment variable. (Microsoft Learn) - In SPFx, render a custom chat component using BotFramework Web Chat, fetch the token from your Function, and connect.
2A) Azure Function (C#) — generate Direct Line token (server-side)
using System.Net;using System.Net.Http.Headers;using System.Text;using System.Text.Json;using Microsoft.Azure.Functions.Worker;using Microsoft.Azure.Functions.Worker.Http;public class DirectLineTokenFunction{ [Function("directline-token")] public async Task<HttpResponseData> Run( [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req) { // Store secret in Function App Settings: // DIRECT_LINE_SECRET = "your-secret" var secret = Environment.GetEnvironmentVariable("DIRECT_LINE_SECRET"); if (string.IsNullOrWhiteSpace(secret)) { var bad = req.CreateResponse(HttpStatusCode.InternalServerError); await bad.WriteStringAsync("Missing DIRECT_LINE_SECRET."); return bad; } using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", secret); var resp = await http.PostAsync( "https://directline.botframework.com/v3/directline/tokens/generate", new StringContent("{}", Encoding.UTF8, "application/json") ); if (!resp.IsSuccessStatusCode) { var fail = req.CreateResponse(HttpStatusCode.BadGateway); await fail.WriteStringAsync("Failed to generate Direct Line token."); return fail; } var json = await resp.Content.ReadAsStringAsync(); var ok = req.CreateResponse(HttpStatusCode.OK); ok.Headers.Add("Content-Type", "application/json"); await ok.WriteStringAsync(json); return ok; }}
This aligns exactly with Microsoft’s Direct Line authentication/token generation documentation. (Microsoft Learn)
Function App Settings
DIRECT_LINE_SECRET= Copilot Studio secret (never commit to Git)- Optional: protect the endpoint with AAD or network restrictions (recommended in enterprise)
2B) SPFx custom Web Part using BotFramework WebChat
Install:
npm i botframework-webchat
CopilotWebChat.tsx
import * as React from "react";import ReactWebChat from "botframework-webchat";export interface ICopilotWebChatProps { tokenEndpoint: string; // Azure Function URL heightPx: number;}export const CopilotWebChat: React.FC<ICopilotWebChatProps> = ({ tokenEndpoint, heightPx }) => { const [directLine, setDirectLine] = React.useState<any>(null); const [error, setError] = React.useState<string>(""); React.useEffect(() => { let alive = true; (async () => { try { if (!tokenEndpoint) { setError("Token endpoint not configured."); return; } const res = await fetch(tokenEndpoint); if (!res.ok) throw new Error(`Token endpoint failed: HTTP ${res.status}`); const data = await res.json(); // expects { token: "..." } if (!data?.token) throw new Error("Token response missing 'token'."); // WebChat factory is typically available on window.WebChat in many builds const dl = (window as any).WebChat.createDirectLine({ token: data.token }); if (alive) setDirectLine(dl); } catch (e: any) { if (alive) setError(e?.message ?? "Failed to initialize chat."); } })(); return () => { alive = false; }; }, [tokenEndpoint]); if (error) { return ( <div style={{ padding: 12 }}> <strong>Copilot chat unavailable</strong> <div>{error}</div> </div> ); } if (!directLine) return <div style={{ padding: 12 }}>Loading chat…</div>; return ( <div style={{ width: "100%", height: `${heightPx}px` }}> <ReactWebChat directLine={directLine} /> </div> );};
Why tokens, not secrets? Because Copilot Studio’s security guidance warns against putting secrets in browser code and recommends token acquisition via service code. (Microsoft Learn)
3) Alternative — Publish the agent directly to SharePoint (no SPFx)
If your goal is simply: “Make the agent available inside SharePoint,” Copilot Studio supports publishing to the SharePoint channel (deploying the agent to a SharePoint site). (Microsoft Learn)
This can be the best option when:
- you don’t need a custom chat container,
- you want minimal maintenance,
- you want the most native deployment approach.
4) Troubleshooting (the stuff that breaks most deployments)
Problem: “Iframe shows blank or embed code isn’t visible”
- If your agent is configured to Authenticate with Microsoft or Authenticate manually, the embed code may not show in the same way as “No authentication.” (Microsoft Learn)
- Your tenant might have embed restrictions, or your page policy might block certain frame sources.
Problem: “Direct Line token generation fails”
- Verify you’re calling the correct endpoint:
POST https://directline.botframework.com/v3/directline/tokens/generate(Microsoft Learn) - Confirm the secret is valid and not rotated (Copilot Studio allows regenerating secrets). (Microsoft Learn)
Problem: “We put the library path in instructions but agent can’t read PDFs”
Instructions are not a data connection. Retrieval depends on knowledge sources and indexing. The embedding method does not change that.
5) Decision matrix: which pattern should you pick?
| Requirement | Best fit |
|---|---|
| “I want this working today” | Pattern A (iframe embed) (Microsoft Learn) |
| “I want enterprise security and a portal-grade UI” | Pattern B (token + WebChat + backend) (Microsoft Learn) |
| “I want this inside SharePoint with minimal custom work” | SharePoint channel (Microsoft Learn) |
Final summary tables
Implementation steps (high level)
| Step | Pattern A: iframe | Pattern B: Direct Line token |
|---|---|---|
| Publish agent | Web channel, copy iframe snippet (Microsoft Learn) | Web channel security enabled (Microsoft Learn) |
| SharePoint UI | SPFx renders <iframe> | SPFx renders WebChat |
| Backend needed | No | Yes (Azure Function) |
| Security posture | Depends on configuration | Stronger (no secret in browser) (Microsoft Learn) |
Technical references
| Topic | Microsoft documentation |
|---|---|
| Publish to web via iframe | (Microsoft Learn) |
| Direct Line channel security | (Microsoft Learn) |
| Don’t expose secret in browser | (Microsoft Learn) |
| Direct Line token generation endpoint | (Microsoft Learn) |
| Publish agent to SharePoint channel | (Microsoft Learn) |
