Architecture guidance Β· sourced from Microsoft Learn

You have a Copilot Studio agent.
You want a web app.

The instinct is to rebuild it in Microsoft Foundry. Usually that's the wrong call β€” and it costs you the low-code authoring surface, the built-in Microsoft 365 channels, and the Power Platform governance you already have.

The recommendation

Keep the agent in Copilot Studio. Put your own front end on it using the Microsoft 365 Agents SDK.

Rebuild in Foundry only when you can name the specific capability Copilot Studio cannot deliver β€” and even then, prefer adding a Foundry agent as a connected agent over a full migration.

01

What are you actually trying to solve?

Most "migrate to Foundry" requests are really one of seven underlying asks. Four of them don't require migration at all. Find your row before scoping the work.

What they sayWhat they meanMigrate?
"Make it a web app"Branded chat UI on our domainNo
"We need our own UI"Control look, layout, placementNo
"Put it on our SharePoint sites"Wide internal rolloutNo
"We need to own chat history"Compliance, analytics, exportNo
"Copilot Studio can't do X"Custom orchestration or toolingMaybe
"We need model Y / cheaper tokens"Model choice + cost controlLikely
"Our AI team works in Python"Code-first agent engineeringYes

Three integration paths

Option A

Web Chat embed code

Copy an <iframe> into your page. Zero code.

  • Anonymous only β€” the embed code disappears the moment you enable authentication
  • No control over UI
  • Fine for a public FAQ bot
Option B Β· Recommended

Copilot Studio Client

Microsoft 365 Agents SDK + MSAL + Web Chat. Your UI, your auth.

  • Entra user sign-in supported
  • No shared secret in the browser
  • Drops into botframework-webchat
Option C Β· Fallback

Direct Line API

The lowest-level transport. You build more of it yourself.

  • Needed for service principal (app-only) tokens β€” "S2S is not currently supported for Copilot Studio"
  • Also the fallback for GitHub Copilot harness agents
  • Requires a server-side token minting service
Gate 1 Β· Authentication

Option A's embed code is only visible when the agent's security setting is No authentication. Turn on Authenticate with Microsoft or Authenticate manually and it vanishes from the Copilot Studio UI β€” you must use B or C.

Gate 2 Β· Harness

The Copilot Studio Client library supports standard harness agents only: "Currently, you can only use the Copilot Studio client library with Copilot Studio agents created by using the standard harness. Agents using the GitHub Copilot harness aren't yet officially supported."

Check your agent's harness before planning this integration. On the GitHub Copilot harness, Option B isn't available today.

Sources: Integrate with web or native apps Β· Integrate with Copilot Studio

When Foundry genuinely is the answer

SignalWhy Copilot Studio hits a ceiling
Specific model, or model flexibilityFoundry lets you pick from the catalog and swap models without changing agent code
Custom orchestration / multi-agentHosted agents run your Agent Framework, LangGraph, or OpenAI Agents SDK code as a container
Per-token cost control at volumeCopilot Studio bills on credits/message packs; Foundry bills per-call inference
Hard network isolationPrivate networking, BYO VNet, VM-isolated sandboxes per session
Own conversation state in your datastoreBring-your-own Azure Cosmos DB
Code-first CI/CDDefine agents via SDK/REST in a pipeline with version control
The option most people miss

Use both. Keep Copilot Studio as the conversational front door β€” it owns channels, topics, escalation, and governance β€” and delegate only the hard reasoning to a Foundry agent via connected agents. Migration risk stays contained to one capability instead of the whole agent.

Know which docs you're reading

Microsoft restructured this product. Tutorials that tell you to create a thread and then a run are describing Foundry Agent Service (classic) β€” deprecated, with a retirement date of 31 March 2027.

CurrentClassic
Docs path/azure/foundry/agents//azure/foundry-classic/agents/
Agent modelPrompt agents, Hosted agents, Responses APIAssistants-style agents
Conversation stateconversations; BYO Cosmos DBthreads / runs / messages
StatusCurrentRetires 2027-03-31
Also note

The Bot Framework SDK is archived β€” support tickets are no longer serviced as of 31 December 2025. The official successor is the Microsoft 365 Agents SDK. If a blog post tells you to use Bot Framework, it's out of date.

Read the full decision framework β†’

02

How the agent is integrated into a frontend

The Copilot Studio Client hands you an object that drops straight into the standard botframework-webchat React component. You get a production chat UI without building one, but you own the page, the styling, and the auth.

 Browser (your React app)
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   1. loginPopup / silent          β”‚
 β”‚  β”‚ MSAL Browser  β”‚ ─────────────────────────────┐    β”‚
 β”‚  β”‚ public client β”‚ ◀──── access token ───────────    β”‚
 β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                              β”‚    β”‚
 β”‚          β”‚ 2. token                             β”‚    β”‚
 β”‚          β–Ό                                      β”‚    β”‚
 β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚    β”‚
 β”‚  β”‚ CopilotStudioClient  β”‚                       β”‚    β”‚
 β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚    β”‚
 β”‚          β”‚ 3. createConnection()                β”‚    β”‚
 β”‚          β–Ό                                      β”‚    β”‚
 β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚    β”‚
 β”‚  β”‚ botframework-webchat β”‚                       β”‚    β”‚
 β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚    β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”˜
                                   β–Ό
              Microsoft Entra ID ──▢ Power Platform API
        scope: CopilotStudio.Copilots.Invoke   (agent runtime)
    

Note what's not there: no Direct Line secret, and no custom token-minting backend. MSAL runs as a public client and the token is a delegated user token, so the browser never holds a long-lived shared secret.

Acquire the token

import { PublicClientApplication, InteractionRequiredAuthError } from '@azure/msal-browser'
import { CopilotStudioClient } from '@microsoft/agents-copilotstudio-client'

export async function acquireToken (settings) {
  const msalInstance = new PublicClientApplication({
    auth: {
      clientId: settings.appClientId,
      authority: `${settings.authority}/${settings.tenantId}`,
    },
  })
  await msalInstance.initialize()

  const loginRequest = {
    // the SDK resolves the correct Power Platform scope for your cloud
    scopes: [CopilotStudioClient.scopeFromSettings(settings)],
    redirectUri: window.location.origin,
  }

  try {
    const accounts = await msalInstance.getAllAccounts()
    if (accounts.length > 0) {
      const r = await msalInstance.acquireTokenSilent({ ...loginRequest, account: accounts[0] })
      return r.accessToken          // silent path β€” no prompt
    }
  } catch (e) {
    if (!(e instanceof InteractionRequiredAuthError)) throw e
  }

  const r = await msalInstance.loginPopup(loginRequest)
  return r.accessToken
}

Wire it into Web Chat

const token = await acquireToken(settings)
const client = new CopilotStudioClient(settings, token)
const connection = CopilotStudioWebChat.createConnection(client, { showTyping: true })

// `directLine` is just the prop name β€” this is NOT the Direct Line channel
<FluentThemeProvider>
  <Composer directLine={connection}>
    <BasicWebChat />
  </Composer>
</FluentThemeProvider>

The Entra app registration step people get wrong

  1. API permissions β†’ Add a permission β†’ APIs my organization uses

    Search for Power Platform API.

  2. Delegated permissions β†’ Copilot Studio β†’ CopilotStudio.Copilots.Invoke

    Add it, then grant admin consent if your tenant requires it.

  3. Authentication β†’ add a Single-page application redirect URI

    Must match window.location.origin for every environment.

An AADSTS65001 error or an empty token is almost always step 1 or 2.

Verified packages

PlatformPackageVersion
JavaScript@microsoft/agents-copilotstudio-client1.7.2 GA
JavaScript@azure/msal-browser4.13.1
JavaScriptbotframework-webchat4.19.1
.NETMicrosoft.Agents.CopilotStudio.Client1.8.77 stable Β· 1.9.40 beta
Pythonmicrosoft-agents-copilotstudio-client1.5.0
Version trap

A stable .NET release exists (1.8.77) alongside a -beta prerelease channel. Search NuGet with prereleases enabled and you'll only see 1.9.40-beta β€” make sure you're pinning the stable line unless you specifically need prerelease features.

Start with Microsoft's copilotstudio-webchat-react sample β€” validate your config with the console sample first, then bring up the UI.

Read the full frontend guide β†’

03

How user sessions are managed

There are three independent "sessions" in play, and they expire on different clocks. Most production bugs here come from assuming they're the same thing.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. BROWSER SESSION          owned by: your app                 β”‚
β”‚    MSAL cache, your app cookie                                 β”‚
β”‚    Ends: tab close, cache clear, explicit sign-out             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 2. IDENTITY SESSION         owned by: Microsoft Entra ID       β”‚
β”‚    Access token for the Power Platform / Direct Line scope     β”‚
β”‚    Ends: token expiry β€” renew silently via MSAL                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 3. CONVERSATION SESSION     owned by: Copilot Studio           β”‚
β”‚    conversationId + agent-side variable state                  β”‚
β”‚    Ends: ~30 min inactivity, or you start a new one            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    

A signed-in user with a valid token can still be talking to a brand-new conversation with none of their earlier context. If you want continuity, persist and reuse the conversationId yourself.

Pick your authentication mode first

OptionVariables your topics getControl who chats?Embed code?
No authenticationβ€”No β€” "anyone with the link"Yes
Authenticate with MicrosoftUser.ID, User.DisplayNameYes, via sharingNo
Authenticate manually+ User.AccessToken, User.IsLoggedInYes (Entra ID)No

The decisive question: does the agent need to call a downstream API as the user? If yes, only Authenticate manually gives you User.AccessToken.

Operational traps

Auth changes only take effect after you publish. Switching manual β†’ Microsoft turns existing User.AccessToken references into Unknown variables and breaks those topics at runtime. And agents using Entra ID auth can't be embedded in Power BI reports via iframe visuals.

Source: Configure user authentication

If you use Direct Line: never ship the secret

Microsoft's words

"Don't expose the secret in any code that runs in the browser, either hard-coded or transferred through a network call." Exchange it for a short-lived token server-side.

PathEndpointexpires_in
Copilot Studio token endpointGET <BOT TOKEN ENDPOINT>3600 s
Direct Line secret exchangePOST /v3/directline/tokens/generate1800 s

Refresh proactively at ~75% of expires_in. You can refresh an unlimited number of times β€” but you cannot refresh an expired token. Let it lapse and the user's conversation dies.

Two-hour propagation

Toggling Require secured access takes up to two hours to propagate. You don't need to publish the agent β€” but you do need to wait. Plan the change window.

SSO needs two app registrations

Don't reuse one

Microsoft: "For security reasons, don't reuse the same app registration for both your agent and your custom website."

An authentication app (exposes the custom scope, authorises the canvas app) and a canvas app (SPA platform, redirect URI, access + ID tokens). Confirm accessTokenAcceptedVersion is 2. The canvas intercepts the sign-in prompt, requests an on-behalf-of token, and the agent exchanges it to populate User.AccessToken.

ChannelSSOChannelSSO
Custom websiteYesDemo websiteNo
SharePointYesMobile appNo
Microsoft TeamsYes*Azure Bot Service channelsNo
Omnichannel (live chat)YesPower Apps / Power PagesNo

* Teams requires extra config β€” skipping it makes users always fail authentication. Source: Configure SSO

Guest users

"Guest users can't access AI-generated answers from SharePoint and Graph Connector data sources in SSO-enabled apps." Test this explicitly if external collaborators are in scope β€” it fails quietly.

Read the full sessions guide β†’

04

How chat history is stored and maintained

Copilot Studio stores transcripts in two places, with two different retentions β€” and there is one exclusion that invalidates most compliance reporting built on them.

Dataverse ConversationTranscript

30 days by default, via a recurring bulk-delete job.

  • Queryable and exportable β€” this is your data
  • Retention is extendable

Copilot Studio internal storage

28 days, then auto-deleted.

  • Not queryable
  • Exists "to support proper agent functionality"
  • Extending Dataverse retention does not extend this
🚨 Critical for SharePoint-grounded agents

Microsoft: "Agent responses that use SharePoint as a knowledge source aren't included in conversation transcripts… conversation transcripts include the question and the content of the source documents used to generate the response. However, the answer isn't included, and is marked as REDACTED."

For a typical intranet agent, Dataverse gives you the question and the sources β€” but not what the agent actually told the user. Any QA, compliance, or hallucination-review process built on Dataverse transcripts will not work.

Other things that will bite you

Source: Conversation transcript controls

Audit vs DSPM β€” the distinction that matters

Purview

"The audit logs in the Audit solution don't include the full text or transcript of the interactions between a user and the agent, only the transcript thread ID. The Data Security Posture Management (DSPM) for AI solution attempts to retrieve the chat text related to the event."

If you need discoverable chat text, Audit alone is not enough. To suppress retention entirely, use a Purview Data Lifecycle Management policy with 0-day retention for Copilot Experiences.

Own your own history

Given the redaction behaviour, the 28/30-day windows, and the 30-minute write lag, most enterprise web apps should mirror the transcript into their own store.

   Browser (Web Chat)
        β”‚  activities in / out
        β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  Your BFF / API       │───────▢│  Azure Cosmos DB         β”‚
  β”‚  β€’ mints tokens       β”‚        β”‚  β€’ partition: userId     β”‚
  β”‚  β€’ mirrors activities β”‚        β”‚  β€’ doc: conversationId   β”‚
  β”‚  β€’ enforces retention β”‚        β”‚  β€’ items: activities[]   β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β–Ό
     Copilot Studio agent
    

Mirror server-side, not in the browser β€” a client-side mirror can be tampered with and is lost on tab close. Microsoft's baseline chat architecture names three triggers for self-managing history: zero-data-retention obligations, custom context-window control, and multiple UX channels with independent session semantics.

Two working references with real server-side history you can port: sample-app-aoai-chatGPT and azure-search-openai-demo. Both are OpenAI-centric, but the history layer is agent-agnostic.

Read the full chat history guide β†’

05

Embedding across multiple SharePoint sites

Four options. Microsoft's own sample repo tells you to prefer the built-in channel over custom code β€” and tenant-wide SPFx deployment is a blunt instrument with a 20-minute fuse.

ApproachAuthMulti-site storyVerdict
Built-in SharePoint channelFull, incl. SSODeploy per siteRecommended
iframe / Embed web partNo auth onlyManual, per pageUsually disqualified
SPFx web partCustomPer-site app installViable
SPFx Application CustomizerCustom + SSOTenant-wideFor "every page"
Microsoft's own recommendation

From the README of their official SPFx sample: "This is an open-source reference sample only. For production deployments, we strongly recommend using the built-in SharePoint channel in Copilot Studio."

The built-in channel

  1. Publish the agent

    Nothing works until you do.

  2. Channels β†’ SharePoint tile

    Select the site, or type its name.

  3. Deploy β†’ Confirm

    You need WRITE access to the target site.

  4. Mark the agent Approved

    It then appears in the dedicated Approved section of the agent picker, visible to all users of that site.

The multi-site catch

The documented flow deploys one site at a time. We found no documented bulk or tenant-wide deployment API for the SharePoint channel β€” for N sites you repeat the step N times. Fine for a handful; it's the reason people move to an Application Customizer for hundreds.

Also: the agent "consumes Copilot Studio capacity, even when used in SharePoint." SharePoint is not a free surface.

Source: Publish an agent to SharePoint

Tenant-wide SPFx deployment

Set "skipFeatureDeployment": true, upload the .sppkg to the tenant app catalog, and check "Enable this app and add it to all sites". That writes a row to the Tenant Wide Extensions list. Scope with WebTemplateId:

<ClientSideComponentInstance
    Title="AgentLauncher"
    Location="ClientSideExtension.ApplicationCustomizer"
    ComponentId="917a86f2-15c1-403e-bbe1-59c6bd50d1e1"
    WebTemplateId="SITEPAGEPUBLISHING#0"   <!-- communication sites only -->
    Sequence="10"
    HostProperties="{&quot;preAllocatedApplicationCustomizerBottomHeight&quot;:&quot;50&quot;}">
</ClientSideComponentInstance>
Site typeWebTemplateId
Group-associated team siteGROUP#0
Communication siteSITEPAGEPUBLISHING#0
Modern team site without a groupSTS#3
Classic team siteSTS#0

Gotchas, all directly sourced

Source: Tenant-wide deployment of SPFx extensions

Misleading prompt in site collection app catalogs

"…you will be prompted if you want to make this solution available to all sites in the organization. Despite the wording, if you check this box, the solution will be available immediately only in the same site collection as where the app catalog is."

Performance β€” the part that gets teams in trouble

Page Diagnostics resultThreshold
πŸ”΄ Attention requiredAny extension > 1 second, or 7+ extensions on a page
🟑 Improvement opportunity5 or more extensions on the page
🟒 No action requiredNo extension over 1 second
The trap

"Application customizers may be executed in the early stage during the lifecycle of a page and it may influence the performance of other extensions on the page."

Web Chat + Agents SDK + MSAL is a heavy bundle. Loading it in onInit() blows the 1-second budget and degrades every other extension on the page.

Source: Modern custom extensions performance

Rollout best practices

  1. Pilot with the built-in channel on one site

    Prove value before investing in custom SPFx code.

  2. Decide contextual vs ambient

    A few key sites β†’ built-in channel. Help on every page β†’ Application Customizer. Don't deploy both to the same site or users see two entry points.

  3. Scope by WebTemplateId from day one

    Going tenant-wide then narrowing means an awkward removal across sites.

  4. Use a site collection app catalog to pilot

    It's your blast-radius control β€” tenant-wide has a 20-minute delay in both directions.

  5. Check the Tenant Wide Extensions list after every update

    To catch duplicate rows before users do.

  6. Test as a guest user

    Guests can't get AI-generated answers from SharePoint or Graph Connector sources in SSO-enabled apps.

  7. Plan transcript capture before rollout

    SharePoint-grounded answers are REDACTED in Dataverse. Solve it before an audit asks.

  8. Model the capacity cost

    Wide deployment multiplies message volume, and SharePoint usage consumes Copilot Studio capacity.

Read the full SharePoint guide β†’