<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Der Sascha</title>
    <description>The latest articles on DEV Community by Der Sascha (@saschadev).</description>
    <link>https://dev.to/saschadev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F17720%2Fc9f0d090-121a-4a20-b572-98842a2e8de7.jpg</url>
      <title>DEV Community: Der Sascha</title>
      <link>https://dev.to/saschadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saschadev"/>
    <language>en</language>
    <item>
      <title>If You Still Print and Scan Contracts in 2026, That’s a Security Bug</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Mon, 16 Mar 2026 11:16:23 +0000</pubDate>
      <link>https://dev.to/saschadev/if-you-still-print-and-scan-contracts-in-2026-thats-a-security-bug-2p58</link>
      <guid>https://dev.to/saschadev/if-you-still-print-and-scan-contracts-in-2026-thats-a-security-bug-2p58</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5ka4nh9ymd52ii29kgw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5ka4nh9ymd52ii29kgw.png" alt="If You Still Print and Scan Contracts in 2026, That’s a Security Bug" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In almost every company I visit, I still see the same scene: someone prints a contract, signs it with a pen, walks it over to a manager for a second signature, then scans it back in and sends a PDF around by email. Nobody really questions it, because “we’ve always done it that way”.&lt;/p&gt;

&lt;p&gt;In 2012, that was normal. In 2026, I’d call it a &lt;strong&gt;security bug&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not because paper is evil, but because the whole print–sign–scan workflow lives &lt;em&gt;outside&lt;/em&gt; the security and compliance world you’ve already invested in. It’s hard to reconstruct who saw what, when. Copies land in places nobody tracks. And while you spend a lot of time and money hardening Microsoft 365 – Entra ID, Conditional Access, DLP, retention – one of your most important processes, signing agreements, is often running as a side quest next to it.&lt;/p&gt;

&lt;p&gt;In this post I want to explain why I see it that way and why Microsoft 365 eSignature is not just a “nice convenience feature”, but a real security and compliance upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens when you print and scan
&lt;/h2&gt;

&lt;p&gt;Let’s be honest about what your paper signing flow really looks like.&lt;/p&gt;

&lt;p&gt;It usually starts in some system – ERP, CRM, DMS, whatever. Someone exports a contract as a PDF and saves it somewhere: on the desktop, in a random file share, in a personal OneDrive folder, wherever they have muscle memory.&lt;/p&gt;

&lt;p&gt;They print it. From that moment on, you have a physical copy lying around: on the printer, on a desk, in a stack of “I’ll deal with this later”. Nobody knows exactly how many people walk past it or glance at it.&lt;/p&gt;

&lt;p&gt;Then come signatures. The first person signs, the second is hunted down in the hallway, maybe there’s a last-minute change, so the whole thing is printed again, re-signed, re-scanned. At the end, the contract hits a scanner – often a shared multi‑function device that “belongs” to no one. The device drops a PDF into some generic scan folder or sends it via email from a technical SMTP account.&lt;/p&gt;

&lt;p&gt;From there, the file goes on tour: legal, finance, the customer, internal stakeholders. Everyone saves their own copy. Some people forward it, some print it again. Six months later, when you try to reconstruct who saw which version and who signed what, you end up doing email archaeology and digging through files named &lt;code&gt;final_contract_v7_signed_scan.pdf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Formally, the process might still be “okay enough” to get by. But from a security and compliance perspective it’s a mess: uncontrolled digital copies, at least one physical copy, and at best a fuzzy audit trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes with Microsoft 365 eSignature
&lt;/h2&gt;

&lt;p&gt;With Microsoft 365 eSignature, you move that signing flow into an environment that already knows a lot about your users and your documents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who the user is (Entra ID identity),&lt;/li&gt;
&lt;li&gt;which tenant they belong to,&lt;/li&gt;
&lt;li&gt;which policies apply (MFA, Conditional Access, DLP, retention),&lt;/li&gt;
&lt;li&gt;and where content is supposed to live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of loosely connected PDFs and scans, you get a tracked signing workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a clear request that is initiated from within Microsoft 365,&lt;/li&gt;
&lt;li&gt;recipients that are real identities (internal Entra users or properly onboarded guests),&lt;/li&gt;
&lt;li&gt;and an audit trail that sits inside your existing compliance boundaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the practical side, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;far fewer “Scan_0001.pdf” zombies in random mailboxes,&lt;/li&gt;
&lt;li&gt;no guessing which PDF is actually the final one,&lt;/li&gt;
&lt;li&gt;a much cleaner story for your DPO and your auditors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Screenshot 1: Sending a request inside Microsoft 365
&lt;/h3&gt;

&lt;p&gt;This is where a visual helps. If you open the Microsoft adoption page for eSignature, you can see how the “send for signature” experience is meant to look directly inside Microsoft 365.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In your blog, this is a good place to show a screenshot like:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a real eSignature send dialog in Word, Outlook or SharePoint,&lt;/li&gt;
&lt;li&gt;with a document already selected, recipients added and fields configured.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point of that image is simple: signing doesn’t start on a printer anymore – it starts where the document already lives, inside Microsoft 365.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy8u38q00vn8svqybkv1l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy8u38q00vn8svqybkv1l.png" alt="If You Still Print and Scan Contracts in 2026, That’s a Security Bug" width="151" height="104"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I call print–sign–scan a security bug
&lt;/h2&gt;

&lt;p&gt;Security is not just about crypto and fancy products. It’s also about how easy it is for normal people to accidentally do the wrong thing.&lt;/p&gt;

&lt;p&gt;The classic print–sign–scan flow makes it trivial to do things you don’t actually want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;leave sensitive contracts on shared printers,&lt;/li&gt;
&lt;li&gt;store “final” versions in personal file locations,&lt;/li&gt;
&lt;li&gt;forward PDFs from unmanaged devices or even private email accounts,&lt;/li&gt;
&lt;li&gt;lose track of which copy is the one that matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don’t need a malicious insider for this to be a problem. Normal, well‑meaning people create unnecessary attack surface simply because the process is messy and unstructured.&lt;/p&gt;

&lt;p&gt;When you move the same use case into Microsoft 365 eSignature, you don’t instantly become perfectly compliant. But you do something important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you anchor signing in your identity system (Entra ID),&lt;/li&gt;
&lt;li&gt;you reuse policies you already have (MFA, Conditional Access, DLP),&lt;/li&gt;
&lt;li&gt;and you cut down the number of uncontrolled copies by design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For me, that’s the difference between “we sort of manage” and “we are actively reducing attack surface”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Microsoft 365 eSignature fits in the real world
&lt;/h2&gt;

&lt;p&gt;I’m not saying Microsoft 365 eSignature replaces every signing solution on the planet. There are absolutely scenarios where a specialised provider like Adobe Acrobat Sign or DocuSign still makes more sense – for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;very complex external signing chains,&lt;/li&gt;
&lt;li&gt;highly regulated cross‑border scenarios,&lt;/li&gt;
&lt;li&gt;or very specific legal requirements in certain industries or jurisdictions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there is a &lt;em&gt;huge&lt;/em&gt; middle ground that looks the same in many organisations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;internal HR flows (contracts, policy acknowledgements, consent forms),&lt;/li&gt;
&lt;li&gt;standard customer contracts and renewals,&lt;/li&gt;
&lt;li&gt;smaller vendor agreements,&lt;/li&gt;
&lt;li&gt;one‑off approvals and sign‑offs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For that space, the combination of Microsoft 365 + eSignature is almost too obvious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you already create and store documents there,&lt;/li&gt;
&lt;li&gt;your employees already authenticate with Entra ID,&lt;/li&gt;
&lt;li&gt;you already pay for the platform and its security features.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enabling eSignature doesn’t mean introducing yet another tool with its own identity silo. It means attaching a structured signing experience to something you’ve already secured.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0vye7mlfr59nz8gl12q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0vye7mlfr59nz8gl12q.png" alt="If You Still Print and Scan Contracts in 2026, That’s a Security Bug" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This makes it obvious that signers are not dealing with some random scan attachment, but a structured flow with clear steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I’d approach this as a security or IT owner
&lt;/h2&gt;

&lt;p&gt;If I were responsible for security or IT in a mid‑sized company, I wouldn’t treat signing as a background chore anymore. I’d do a one‑time mapping of the main signing flows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which flows are internal (HR, internal approvals)?&lt;/li&gt;
&lt;li&gt;Which flows hit customers or partners?&lt;/li&gt;
&lt;li&gt;Which flows are legally critical vs. “nice to have on record”?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I’d split them into two buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;high complexity / high risk&lt;/strong&gt; – this is where a specialised provider might remain the best option,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;high volume / moderate complexity&lt;/strong&gt; – this is where Microsoft 365 eSignature is a very strong default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For that second bucket, I’d explicitly flip the default:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“If this scenario can run through Microsoft 365 eSignature, that’s what we do. Printing and scanning is the exception, not the standard.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That change alone doesn’t transform your entire security posture overnight, but it nudges a huge amount of day‑to‑day work into a safer, more traceable pattern. And it gives you a far cleaner story when someone asks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who approved this?&lt;/li&gt;
&lt;li&gt;When exactly did they sign?&lt;/li&gt;
&lt;li&gt;How long do we keep the signed version?&lt;/li&gt;
&lt;li&gt;What happens if we need to prove this in front of an auditor or a regulator?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuenhdy04986se6enw0rv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuenhdy04986se6enw0rv.png" alt="If You Still Print and Scan Contracts in 2026, That’s a Security Bug" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the picture you want in people’s heads when they think about “how signing works here” – not a pile of PDFs in someone’s mailbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: stop treating signatures as a side quest
&lt;/h2&gt;

&lt;p&gt;Print–sign–scan survived for a long time because it “kind of worked” and everyone knew how to use a printer. But in 2026, when you’re already investing heavily into identity, cloud security and compliance, it doesn’t make sense to run a core business process on the side, disconnected from all of that.&lt;/p&gt;

&lt;p&gt;Taking Microsoft 365 eSignature seriously means treating signing like any other critical process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bound to real identities,&lt;/li&gt;
&lt;li&gt;protected by the policies you already have,&lt;/li&gt;
&lt;li&gt;and visible in an audit trail you can actually explain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s why I’m comfortable calling the old print–sign–scan approach a security bug in 2026. Not because it never worked, but because there’s now a much better default available – and choosing not to use it is, in many environments, a conscious decision to accept more risk than you need to.&lt;/p&gt;

</description>
      <category>m365</category>
      <category>esignature</category>
      <category>security</category>
      <category>compliance</category>
    </item>
    <item>
      <title>MCP Is Not Magic – It’s Just a Cleaner Way to Admit You Need an Orchestrator</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Sun, 15 Mar 2026 11:15:59 +0000</pubDate>
      <link>https://dev.to/saschadev/mcp-is-not-magic-its-just-a-cleaner-way-to-admit-you-need-an-orchestrator-am9</link>
      <guid>https://dev.to/saschadev/mcp-is-not-magic-its-just-a-cleaner-way-to-admit-you-need-an-orchestrator-am9</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4kjgrqbq69kmw6fydw8y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4kjgrqbq69kmw6fydw8y.png" alt="MCP Is Not Magic – It’s Just a Cleaner Way to Admit You Need an Orchestrator" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every few months there is a new framework that supposedly “changes everything” about how we build AI systems.&lt;/p&gt;

&lt;p&gt;Right now, MCP and Foundry are in that spotlight.&lt;/p&gt;

&lt;p&gt;If you read some of the marketing posts, you get the impression that MCP is a kind of magic dust you sprinkle on your agents and suddenly everything is more powerful, safer and easier to manage.&lt;/p&gt;

&lt;p&gt;I don’t buy that.&lt;/p&gt;

&lt;p&gt;I like MCP. I like Foundry. But not because they are magic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;MCP is essentially a cleaner way to admit that you need an orchestrator.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this post I want to unpack what that means, using a very concrete example: an agent that understands the gap between SAP / SuccessFactors and Entra ID, and reports identity drift back to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The reality before MCP: ad-hoc glue everywhere
&lt;/h2&gt;

&lt;p&gt;Before MCP/Foundry, most “agents” in companies looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a chat UI somewhere (Teams bot, web frontend, Slack app),&lt;/li&gt;
&lt;li&gt;a backend service written in whoever’s favourite language,&lt;/li&gt;
&lt;li&gt;a bunch of custom HTTP endpoints or SDK calls to systems like SAP, Entra, Jira, Confluence, ServiceNow, …&lt;/li&gt;
&lt;li&gt;some prompt engineering and an LLM call glued on top.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you were disciplined, you at least hid those systems behind a minimal set of APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getUserProfile(email)&lt;/code&gt; instead of “call SAP, then Graph, then some random DB”,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;createTicket(summary, details)&lt;/code&gt; instead of “POST to Jira with a half-baked payload”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you weren’t, your agent prompt probably contained a free-form explanation like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“When you need SAP data, call this URL with this payload. When you need Entra data, call this other URL…”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It worked, but it was fragile and hard to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What MCP actually gives you
&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol) formalises something many of us were already doing intuitively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you describe tools in a machine-readable way,&lt;/li&gt;
&lt;li&gt;you keep business logic and credentials on your side,&lt;/li&gt;
&lt;li&gt;the agent gets a clean menu of what it can do and how to call it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of informal “when you need SAP…” paragraphs, you get structured capabilities like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sap_list_users(limit)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;entra_list_users(limit)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;report_mismatches(filter)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Foundry builds on top of this by giving you a consistent runtime, hosting and lifecycle around those tools.&lt;/p&gt;

&lt;p&gt;None of that is magic. It’s just good software engineering discipline encoded into a protocol and a platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Example: A Sync Insights agent on MCP/Foundry
&lt;/h2&gt;

&lt;p&gt;Let’s make this less abstract.&lt;/p&gt;

&lt;p&gt;Imagine you want an agent that can answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Show me active SAP employees who don’t have an Entra account.”&lt;/li&gt;
&lt;li&gt;“Where do department attributes differ between SAP and Entra?”&lt;/li&gt;
&lt;li&gt;“Which Entra accounts look like they should have been deprovisioned already?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood you need three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;an SAP/SuccessFactors API client,&lt;/li&gt;
&lt;li&gt;an Entra ID client (Microsoft Graph),&lt;/li&gt;
&lt;li&gt;a bit of logic that compares both and reports mismatches.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With MCP/Foundry, you expose this as a small set of tools instead of one giant do-everything endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1. Tool-level view
&lt;/h3&gt;

&lt;p&gt;The tools might look like this in conceptual terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sap_list_users(limit, departmentFilter)&lt;/code&gt; – returns a normalised list of SAP users.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entra_list_users(limit, departmentFilter)&lt;/code&gt; – returns a normalised list of Entra users.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;report_mismatches(scope)&lt;/code&gt; – returns a summary of differences between both sides.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Sync Insights agent doesn’t need to know how SAP authentication works or which Entra Graph scopes you requested. It only “sees” the tools.&lt;/p&gt;

&lt;p&gt;Behind those tools lives your service code – written in Node, .NET, whatever you like – that you can test like any other backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2. Why this is better than pure prompt glue
&lt;/h3&gt;

&lt;p&gt;The benefits are subtle but important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discoverability&lt;/strong&gt; : tools are explicit. You can introspect them, generate docs, reason about what the agent can and cannot do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt; : credentials stay in your server. The agent never sees SAP passwords or client secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusability&lt;/strong&gt; : the same toolset can be used by multiple agents, CLI tools, scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testability&lt;/strong&gt; : you can unit test &lt;code&gt;report_mismatches()&lt;/code&gt; without an LLM in the loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You could have built all of this without MCP, of course. But MCP gives you a shared language and wiring for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Where Foundry adds value
&lt;/h2&gt;

&lt;p&gt;Foundry is essentially a home for your MCP tools and agents.&lt;/p&gt;

&lt;p&gt;From my perspective, its value in this story comes from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;standardised hosting&lt;/strong&gt; for your agents and tools (no more “where is that container running again?”),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;configuration and environment separation&lt;/strong&gt; (dev vs. prod, different tenants),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;observability&lt;/strong&gt; : calls, latency, errors per tool,&lt;/li&gt;
&lt;li&gt;and a place to evolve your agents over time without rewriting everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the Sync Insights agent, a Foundry setup might look like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one Foundry project per organisation/tenant,&lt;/li&gt;
&lt;li&gt;a tool bundle exposing SAP and Entra operations,&lt;/li&gt;
&lt;li&gt;an “Insights” agent that knows how to combine them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there, you can connect different frontends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Teams message extension,&lt;/li&gt;
&lt;li&gt;a simple web UI for identity admins,&lt;/li&gt;
&lt;li&gt;scheduled runs that post reports into a channel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. The orchestrator question you can’t dodge
&lt;/h2&gt;

&lt;p&gt;MCP and Foundry don’t change a fundamental truth about non-trivial AI systems:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Somewhere, you need an orchestrator that decides which tools to call, in which order, and with which data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can pretend that this logic “just happens” inside the LLM because you wrote a long prompt. Or you can admit that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;part of that orchestration belongs in the model (planning, natural language understanding),&lt;/li&gt;
&lt;li&gt;and part of it belongs in your code (validation, retries, safety checks, state).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MCP makes that split a bit more honest: tools are first-class citizens. Foundry gives you a place to run them.&lt;/p&gt;

&lt;p&gt;But you still have to design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which tools an agent is allowed to use,&lt;/li&gt;
&lt;li&gt;what a “good plan” looks like for a given workflow,&lt;/li&gt;
&lt;li&gt;and where you want a human in the loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Where I see the sweet spot for MCP + Foundry
&lt;/h2&gt;

&lt;p&gt;In my own projects, the combination of MCP and Foundry shines when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I have to integrate multiple enterprise systems (SAP, Entra, Jira, Confluence, ticketing),&lt;/li&gt;
&lt;li&gt;I want to keep all real credentials and complexity in a backend I own,&lt;/li&gt;
&lt;li&gt;but I want agents to feel like they have a unified “brain” for these systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Sync Insights agent is a great fit for this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it’s cross-system by nature (HR vs. Identity),&lt;/li&gt;
&lt;li&gt;it’s read-heavy and insight-focused (perfect for AI summarisation),&lt;/li&gt;
&lt;li&gt;it benefits massively from being able to call multiple tools in a single conversation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wouldn’t use MCP/Foundry for a one-off “call this single API and return JSON” plugin – that’s overkill. But the moment you’re juggling several systems and workflows, you need an orchestrator anyway. MCP just gives that orchestrator a standard shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. My take
&lt;/h2&gt;

&lt;p&gt;I don’t see MCP or Foundry as magic. I see them as an honest acknowledgement that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tool calls matter,&lt;/li&gt;
&lt;li&gt;backends matter,&lt;/li&gt;
&lt;li&gt;and orchestration is too important to hide in a prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For scenarios like SAP ↔ Entra Sync Insights, that’s exactly what I want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a clean set of tools that express what my backend can do,&lt;/li&gt;
&lt;li&gt;a platform that runs them with proper observability,&lt;/li&gt;
&lt;li&gt;and agents that are powerful because their world is well-structured – not because we believed in magic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you go into MCP/Foundry with that mindset, you’re less likely to be disappointed – and much more likely to build something that survives the next hype cycle.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>foundry</category>
      <category>aiagents</category>
      <category>orchestration</category>
    </item>
    <item>
      <title>SAP vs. Entra ID: Why Your User Sync Should Be a Product, Not a Script</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Sat, 14 Mar 2026 12:02:47 +0000</pubDate>
      <link>https://dev.to/saschadev/sap-vs-entra-id-why-your-user-sync-should-be-a-product-not-a-script-1227</link>
      <guid>https://dev.to/saschadev/sap-vs-entra-id-why-your-user-sync-should-be-a-product-not-a-script-1227</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxti8jk8a828jr92b94b4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxti8jk8a828jr92b94b4.png" alt="SAP vs. Entra ID: Why Your User Sync Should Be a Product, Not a Script" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a lot of organisations I talk to, the story sounds like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SAP (or SuccessFactors) is the source of truth for people and org structure.&lt;/li&gt;
&lt;li&gt;Entra ID (formerly Azure AD) is the front door for apps and services.&lt;/li&gt;
&lt;li&gt;Somewhere between them lives a “sync”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you ask what that sync is, you usually get one of these answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“We have a script for that.”&lt;/li&gt;
&lt;li&gt;“That’s handled by a black-box connector, don’t touch it.”&lt;/li&gt;
&lt;li&gt;“IT and HR sort it out manually when things break.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That might have been acceptable in 2012. In 2026, with hundreds of SaaS apps behind Entra, compliance requirements and AI agents that can act on top of your identity graph, I think that mindset is dangerous.&lt;/p&gt;

&lt;p&gt;My argument in this post is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your SAP ↔ Entra ID user sync is not a script. It’s a &lt;strong&gt;product&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And you should treat it like one.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What goes wrong when sync is “just a script”
&lt;/h2&gt;

&lt;p&gt;When user sync is an invisible background job, a couple of things tend to happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Responsibilities are fuzzy (“Is this HR’s fault or IT’s fault?”).&lt;/li&gt;
&lt;li&gt;There is no clear SLO (“How long until changes in SAP show up in Entra?”).&lt;/li&gt;
&lt;li&gt;Drift accumulates silently:&lt;/li&gt;
&lt;li&gt;departments don’t match,&lt;/li&gt;
&lt;li&gt;people who left still have accounts,&lt;/li&gt;
&lt;li&gt;service accounts live forever,&lt;/li&gt;
&lt;li&gt;shadow roles are assigned and never cleaned up.&lt;/li&gt;
&lt;li&gt;Nobody has a simple answer to “How many users are in SAP but not in Entra (and vice versa)?”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you then start layering things like Conditional Access, Just-in-Time access or Copilot on top of Entra, this drift turns from nuisance into real risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Thinking of sync as a product
&lt;/h2&gt;

&lt;p&gt;If you treat the SAP ↔ Entra sync as a product, the conversation changes.&lt;/p&gt;

&lt;p&gt;A product has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a clear &lt;strong&gt;scope&lt;/strong&gt; and responsible owner,&lt;/li&gt;
&lt;li&gt;defined &lt;strong&gt;inputs&lt;/strong&gt; and &lt;strong&gt;outputs&lt;/strong&gt; ,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;health metrics&lt;/strong&gt; and SLOs,&lt;/li&gt;
&lt;li&gt;and a roadmap for improvement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this framing, the sync product might be responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keeping core identity attributes aligned (name, department, manager, employment status),&lt;/li&gt;
&lt;li&gt;ensuring there are no “orphaned” accounts (SAP-only or Entra-only, within agreed rules),&lt;/li&gt;
&lt;li&gt;providing reliable data for downstream systems (RBAC, licences, security policies),&lt;/li&gt;
&lt;li&gt;and giving HR / IT &lt;strong&gt;insights&lt;/strong&gt; about drift, not just blind automation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where I like the idea of an AI-assisted &lt;strong&gt;Sync Insights agent&lt;/strong&gt; on top of the raw mechanics.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A Sync Insights agent as a first-class tool
&lt;/h2&gt;

&lt;p&gt;In a previous post, I sketched an MCP-based agent that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;talks to SAP/SuccessFactors via API,&lt;/li&gt;
&lt;li&gt;talks to Entra ID via Microsoft Graph,&lt;/li&gt;
&lt;li&gt;compares both sides,&lt;/li&gt;
&lt;li&gt;and returns a structured report of mismatches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key twist is: it doesn’t auto-fix anything. It gives you visibility.&lt;/p&gt;

&lt;p&gt;Conceptually the flow looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mermaid.live/edit?ref=blog.bajonczak.com#pako:eNplkF9rwjAUxb9KuM_V1dpomodBmU6FbYy6p7U-hPbaFmwi-cPmxO--pMJe9hC4nJz87sm5Qq0aBA7Hk_qqO6EteSkqScg-fy_9IQ9k7-oajXkWtVXaHMhkQpxBbfzwSPJNub_Imuyk6dvOGpK3KO0hENZvH0VerqXVguxW_94FS74Jqsaz8nuDvC3KbeF37hpP6e2FfKAYRhohEMGAehB94-Neg1aB7XDACrgfGzwKd7IVVPLmrcJZFZIBP4qTwQjcuREWV71otRiAW-28eBYS-BW-gc-W6TTJaDpjLFnGcbxYRnDxMp16KZsvUsYWSZrQ9BbBj1KeMJvSOYszmswpS1LK6Ij7HO_udGx6X9nrveCx5wi0cm33F6rV4Td3t0bZoH5STlrg2e0Xy9F9vA" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8bhy0747yvr3a4ivg1c.png" alt="SAP vs. Entra ID: Why Your User Sync Should Be a Product, Not a Script" width="800" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The agent can answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Show me all active employees in SAP who have no Entra account.”&lt;/li&gt;
&lt;li&gt;“List Entra accounts not present in SAP (excluding technical accounts).”&lt;/li&gt;
&lt;li&gt;“Where do department attributes differ between SAP and Entra?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood, you can structure it as a small Node/TypeScript service (or a Foundry agent) with tools like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sap_list_users(limit)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;entra_list_users(limit)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;report_mismatches(filters)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From a Foundry/MCP perspective, the implementation details are less important than the principle: you expose &lt;strong&gt;high-level capabilities&lt;/strong&gt; , not raw SQL or shell.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1. Example: computing mismatches
&lt;/h3&gt;

&lt;p&gt;This is roughly what the core comparison might look like (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SapUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;department&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EntraUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;department&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SyncMismatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MissingInEntra&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MissingInSap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AttributeMismatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sapUser&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;SapUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;entraUser&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;EntraUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;computeMismatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;sapUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SapUser&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;entraUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntraUser&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;SyncMismatch&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SyncMismatch&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entraByEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entraByUpn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entraUsers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailNorm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailNorm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;entraByEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailNorm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;entraByUpn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// SAP -&amp;gt; Entra&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;sapUsers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailNorm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntraUser&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailNorm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entraByEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailNorm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entraByUpn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MissingInEntra&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sapUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`No Entra user for SAP userId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, email=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sapDept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entraDept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sapDept&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;entraDept&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sapDept&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;entraDept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AttributeMismatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sapUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;entraUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Department mismatch: SAP="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sapDept&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" vs ENTRA="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entraDept&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Entra -&amp;gt; SAP&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sapEmails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;sapUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sapUserIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sapUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entraUsers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailNorm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailInSap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;emailNorm&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sapEmails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailNorm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idInSap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sapUserIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;upn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;emailInSap&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;idInSap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MissingInSap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;entraUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`No SAP user for Entra UPN=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, mail=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then have the agent format this into a CSV, a table in Teams, or a ticket summary – whatever your identity team prefers.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Where Foundry fits into this picture
&lt;/h2&gt;

&lt;p&gt;Foundry (and MCP in general) gives you a more structured way to expose these tools to agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you describe tools like &lt;code&gt;sap_list_users&lt;/code&gt;, &lt;code&gt;entra_list_users&lt;/code&gt;, &lt;code&gt;report_mismatches&lt;/code&gt; in a machine-readable schema,&lt;/li&gt;
&lt;li&gt;you keep all credentials and real logic in your backend,&lt;/li&gt;
&lt;li&gt;the agent orchestrates calls and presents results, but doesn’t hold secrets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, I’d see a stack like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SAP API client + Entra Graph client (Node/TS, .NET, whatever you like),&lt;/li&gt;
&lt;li&gt;a thin Foundry/MCP server that exposes “Sync Insights” tools,&lt;/li&gt;
&lt;li&gt;a Foundry agent (or Copilot Studio scenario) that uses those tools to answer identity questions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key point is: Foundry is not the sync engine. It’s the orchestration/interaction layer on top of the engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Security trimming and guardrails
&lt;/h2&gt;

&lt;p&gt;Identity data is sensitive. A Sync Insights agent should be held to the same standards as any identity admin tool.&lt;/p&gt;

&lt;p&gt;A few guardrails I’d put in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run the backend under a dedicated identity with scoped permissions:&lt;/li&gt;
&lt;li&gt;for SAP: read-only on the relevant endpoints,&lt;/li&gt;
&lt;li&gt;for Entra: limited Graph permissions (no Directory.AccessAsUser.All in production).&lt;/li&gt;
&lt;li&gt;Record who asked which question and when, for auditing.&lt;/li&gt;
&lt;li&gt;Make clear that the agent does &lt;strong&gt;not&lt;/strong&gt; auto-fix anything; it only reports.&lt;/li&gt;
&lt;li&gt;Expose “dangerous” actions (e.g. disable accounts, modify groups) via separate, more tightly controlled tools – or not at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to have an agent that helps you see problems early, not an unsupervised system that changes identities on the fly because a prompt asked nicely.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Treating sync as a product: what changes in practice?
&lt;/h2&gt;

&lt;p&gt;If you accept the idea that SAP ↔ Entra sync is a product, a few concrete changes tend to follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You assign an owner (or small team) responsible for identity integrity between the two systems.&lt;/li&gt;
&lt;li&gt;You define what “healthy” looks like (drift thresholds, sync latency, allowed exceptions).&lt;/li&gt;
&lt;li&gt;You invest in &lt;strong&gt;observability&lt;/strong&gt; (reports, dashboards, agents) instead of just “it runs at 3am”.&lt;/li&gt;
&lt;li&gt;You give HR and IT a shared view of where reality and systems disagree.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you have that foundation, layering Foundry/MCP-style agents on top becomes a force multiplier instead of a risky experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. My take
&lt;/h2&gt;

&lt;p&gt;In 2026, SAP and Entra are not going away. If anything, they’re getting tighter and more central as identity sources.&lt;/p&gt;

&lt;p&gt;You can treat the sync between them as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“that one script we inherited from someone who left”,&lt;/li&gt;
&lt;li&gt;or as a small but critical &lt;strong&gt;product&lt;/strong&gt; with proper attention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you pick the second option, things like Sync Insights agents, Foundry tools and AI-based reporting suddenly make a lot more sense – because sie sitzen auf einem Fundament, das jemand bewusst gebaut und verantwortet.&lt;/p&gt;

&lt;p&gt;And that, in my view, is the only way to bring AI into identity management without waking up one day and realising nobody really knows who is supposed to have access to what.&lt;/p&gt;

</description>
      <category>sap</category>
      <category>entraid</category>
      <category>identity</category>
      <category>foundry</category>
    </item>
    <item>
      <title>Stop Feeding Copilot Everything: Where ‘Bring Your Own Data’ Should Have Hard Limits</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Wed, 11 Mar 2026 17:35:21 +0000</pubDate>
      <link>https://dev.to/saschadev/stop-feeding-copilot-everything-where-bring-your-own-data-should-have-hard-limits-53e</link>
      <guid>https://dev.to/saschadev/stop-feeding-copilot-everything-where-bring-your-own-data-should-have-hard-limits-53e</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6e0jbv03m5q7pzpyxsth.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6e0jbv03m5q7pzpyxsth.png" alt="Stop Feeding Copilot Everything: Where ‘Bring Your Own Data’ Should Have Hard Limits" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;“Bring your own data” is the new magic phrase in the Copilot world. Vendors demo it as if you can just flip a switch and suddenly your AI assistant knows everything your company knows.&lt;/p&gt;

&lt;p&gt;Technically, you &lt;em&gt;can&lt;/em&gt; plug a lot of sources into Microsoft 365 Copilot: SharePoint, file shares, Confluence, Jira, databases, custom APIs…&lt;/p&gt;

&lt;p&gt;The more interesting question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which data should you &lt;strong&gt;never&lt;/strong&gt; feed into a generic Copilot in the first place?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this post I’ll argue for some hard limits on “bring your own data”, based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where Security Trimming actually works out of the box,&lt;/li&gt;
&lt;li&gt;where you need your own guardrails,&lt;/li&gt;
&lt;li&gt;and where the right answer is simply “No, this doesn’t go into Copilot at all.”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Where Copilot is naturally strong: M365 content with sane permissions
&lt;/h2&gt;

&lt;p&gt;Let’s start with the part that works best:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;documents in SharePoint and OneDrive,&lt;/li&gt;
&lt;li&gt;conversations in Teams,&lt;/li&gt;
&lt;li&gt;emails in Exchange,&lt;/li&gt;
&lt;li&gt;tasks/plans in Planner, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this flows into the Microsoft Graph index and Microsoft Search. When Copilot asks Graph for context, &lt;strong&gt;Graph enforces ACLs&lt;/strong&gt; for the current user. If Alice doesn’t have access to a file, it doesn’t show up in her Copilot answers.&lt;/p&gt;

&lt;p&gt;That doesn’t mean you’re done – you still need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stop dumping “secret CEO docs” into broad collaboration sites,&lt;/li&gt;
&lt;li&gt;avoid overusing “Everyone except external users” on sensitive libraries,&lt;/li&gt;
&lt;li&gt;and generally treat SharePoint as a real DMS, not a file dump.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But at least the security model is clear and enforced at the platform level.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Where BYO data starts to hurt: external sources without proper trimming
&lt;/h2&gt;

&lt;p&gt;The story changes once you plug in external systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confluence wikis,&lt;/li&gt;
&lt;li&gt;file shares,&lt;/li&gt;
&lt;li&gt;line-of-business apps,&lt;/li&gt;
&lt;li&gt;databases, custom APIs…&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are roughly two ways people do this today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Microsoft Graph connectors that index external content as &lt;code&gt;externalItem&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Custom agents/plugins that call your APIs at runtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both are valid. Both can be safe. Both can be incredibly dangerous if you ignore Security Trimming.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1. The “god mode service account” trap
&lt;/h3&gt;

&lt;p&gt;A common anti-pattern looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copilot calls a custom connector / agent.&lt;/li&gt;
&lt;li&gt;The agent uses a technical account to query Confluence, Jira, SQL, etc.&lt;/li&gt;
&lt;li&gt;That account can see “everything”.&lt;/li&gt;
&lt;li&gt;The agent returns whatever it finds to Copilot, regardless of who is asking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’ve effectively built a very polite internal data exfiltration tool.&lt;/p&gt;

&lt;p&gt;Queries like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“List upcoming restructurings.”&lt;/li&gt;
&lt;li&gt;“What are the salaries of senior engineers in Germany?”&lt;/li&gt;
&lt;li&gt;“Show me current investigation reports.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;might not return anything in normal M365 search, but suddenly work great via Copilot – because you gave one backend user access to all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Categories of data that should trigger a hard “why?”
&lt;/h2&gt;

&lt;p&gt;Before talking about mechanics, let’s be very clear on &lt;strong&gt;what&lt;/strong&gt; we’re talking about.&lt;/p&gt;

&lt;p&gt;Whenever someone suggests “let’s bring system X into Copilot”, I’d ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this system contain any of the following?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HR &amp;amp; compensation data&lt;/strong&gt;
Salaries, bonuses, performance reviews, promotion decisions, layoff lists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal &amp;amp; compliance data&lt;/strong&gt;
Investigations, incident reports, privileged legal advice, whistleblower cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security-sensitive logs &amp;amp; configs&lt;/strong&gt;
Firewall rules, security incident logs, vulnerability reports, secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Highly regulated customer data&lt;/strong&gt;
Health records, financial transaction details, anything under strict regulation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is “yes”, I’d default to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;no generic Copilot access&lt;/strong&gt; to that system,&lt;/li&gt;
&lt;li&gt;or only carefully scoped, role-specific agents with explicit topic guards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not “never”, but “only with a very conscious design”.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Graph connectors: powerful, but ACLs are everything
&lt;/h2&gt;

&lt;p&gt;Graph connectors extend the Microsoft Search/Copilot index to external content sources. They’re great when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;content is mostly read-only,&lt;/li&gt;
&lt;li&gt;you can express permissions as ACLs,&lt;/li&gt;
&lt;li&gt;and you’re okay with near-real-time instead of per-request freshness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The security story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you push items as &lt;code&gt;externalItem&lt;/code&gt; into an &lt;code&gt;externalConnection&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;each item carries an &lt;code&gt;acl&lt;/code&gt; array,&lt;/li&gt;
&lt;li&gt;Graph uses that ACL when answering searches and Copilot requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your ACLs are wrong, your security is wrong.&lt;/p&gt;

&lt;p&gt;Example (TypeScript, simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GRAPH_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://graph.microsoft.com/v1.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CONNECTION_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contosoConfluence&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAppToken&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// client credentials flow for your app registration&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;token&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SourcePage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;allowedUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="c1"&gt;// AAD IDs or UPNs&lt;/span&gt;
  &lt;span class="nl"&gt;forbiddenUsers&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="c1"&gt;// optional explicit deny list&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pushExternalItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SourcePage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAppToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grantAcl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowedUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accessType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;denyAcl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forbiddenUsers&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accessType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;acl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;grantAcl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;denyAcl&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;GRAPH_BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/external/connections/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CONNECTION_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to push externalItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;graph_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you take the time to map your external permissions into these ACLs properly, Graph will do the trimming for you. If you don’t, you’re back to square one.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Custom agents: your backend &lt;em&gt;is&lt;/em&gt; the security model
&lt;/h2&gt;

&lt;p&gt;For live data and actions (“create ticket”, “get current balance”, “trigger deployment”), Graph connectors aren’t enough. You need an agent that calls your APIs.&lt;/p&gt;

&lt;p&gt;The architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  U[User] -- ask --&amp;gt; C[Copilot]
  C -- call tool --&amp;gt; A[Agent]
  A -- HTTP --&amp;gt; B[Your Backend]
  B --&amp;gt; SYS[Systems]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, your backend is the entire security model.&lt;/p&gt;

&lt;p&gt;Minimal pattern in Node/Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// permissions.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUserPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserContext&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;directoryLookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Entra / IAM lookup&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="c1"&gt;// acl.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UserContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./permissions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DocumentAcl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;allowedRoles&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;allowedGroups&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;forbiddenRoles&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ExternalDoc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;acl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DocumentAcl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExternalDoc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;allowedRoles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;allowedGroups&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;forbiddenRoles&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;forbiddenRoles&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;forbiddenRoles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowedRoles&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowedGroups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Default: visible to all employees&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowedRoles&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;allowedRoles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowedGroups&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;allowedGroups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;filterDocsByAcl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExternalDoc&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ExternalDoc&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then your agent endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// agentHandler.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getUserPermissions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./permissions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;filterDocsByAcl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ExternalDoc&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./acl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildAnswerFromDocs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./rag&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AskRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AskResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isForbiddenQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userRoles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sensitivePatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;salary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compensation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bonus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;layoff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;termination list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;performance review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;investigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSensitive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sensitivePatterns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isSensitive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;privilegedRoles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HR_ADMIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LEGAL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;C_LEVEL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isPrivileged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;privilegedRoles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;userRoles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isPrivileged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;copilotAgentHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AskRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userEmail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;question and userEmail are required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUserPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userEmail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown_user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 1) Topic guard: block certain questions for non‑privileged roles&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isForbiddenQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I’m not allowed to answer this type of question for your role.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;AskResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2) Fetch docs from your system&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawDocs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExternalDoc&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rawSearchDocs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;filterDocsByAcl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawDocs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`I couldn't find any documents you are allowed to see that answer "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;".`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;AskResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3) Use an LLM to build the final answer&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;usedDocs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;buildAnswerFromDocs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AskResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usedDocs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two important observations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can say “no” at the question level (topic guard),&lt;/li&gt;
&lt;li&gt;and you can say “no” at the data level (ACL filter).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. A simple decision table for BYO data
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Security trimming&lt;/th&gt;
&lt;th&gt;My default stance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docs already in SharePoint/OneDrive/Teams&lt;/td&gt;
&lt;td&gt;Let Graph handle it&lt;/td&gt;
&lt;td&gt;Graph ACLs based on M365 permissions&lt;/td&gt;
&lt;td&gt;✅ Good starting point, focus on cleaning permissions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External wiki / KB&lt;/td&gt;
&lt;td&gt;Graph connector&lt;/td&gt;
&lt;td&gt;ACLs you attach per item&lt;/td&gt;
&lt;td&gt;✅ Do it, if you can map permissions cleanly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On‑prem file shares&lt;/td&gt;
&lt;td&gt;File share / Azure Files connector&lt;/td&gt;
&lt;td&gt;Connector maps existing ACLs&lt;/td&gt;
&lt;td&gt;✅ Good bridge if moving to M365 is not trivial.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live business data (status, metrics, actions)&lt;/td&gt;
&lt;td&gt;Custom agent + backend API&lt;/td&gt;
&lt;td&gt;Backend enforces roles/groups + topic guards&lt;/td&gt;
&lt;td&gt;✅ With care; design security into the API.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HR / compensation / legal investigations&lt;/td&gt;
&lt;td&gt;Generic Copilot access&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;🚫 Default “no”; only very scoped agents if really needed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  7. Guardrails I’d put in place
&lt;/h2&gt;

&lt;p&gt;Independent of the technical path, I’d hard-code a few rules into any “bring your own data” story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No global service accounts without filters&lt;/strong&gt;
If an agent uses an account that can see everything, you must filter aggressively in your backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging and auditing&lt;/strong&gt;
Log which user asked what, and which systems were queried. You don’t need to store content, but you should know when someone repeatedly pokes around sensitive areas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topic-level deny lists&lt;/strong&gt;
It’s okay to hard-block certain topics in generic agents (“salary”, “layoff list”, “investigation”). For those, build a separate, role-specific agent if needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with low-risk sources&lt;/strong&gt;
Bring in policies, guidelines, runbooks, KB articles first. Leave HR/Legal/Security data for last – or never.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Conclusion: BYO data is not about dumping everything into Copilot
&lt;/h2&gt;

&lt;p&gt;“Bring your own data” for Copilot should not mean “dump every system we have into a single model and hope for the best.”&lt;/p&gt;

&lt;p&gt;Instead, I’d frame it like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use native M365 content where possible and fix your permissions.&lt;/li&gt;
&lt;li&gt;Use Graph connectors for read‑mostly knowledge sources where you can express ACLs cleanly.&lt;/li&gt;
&lt;li&gt;Use custom agents for live systems, but treat your backend as the security boundary and enforce roles, groups and topic guards there.&lt;/li&gt;
&lt;li&gt;Accept that some data is better handled by dedicated, role-specific tools – not a general company-wide Copilot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you get those basics right, “bring your own data” stops being ein Buzzword und wird zu etwas, das deinen Kolleg:innen echte Antworten liefert – ohne, dass sie Dinge sehen, die sie nie hätten sehen sollen.&lt;/p&gt;

</description>
      <category>m365copilot</category>
      <category>security</category>
      <category>graphconnectors</category>
      <category>integration</category>
    </item>
    <item>
      <title>OpenClaw in 2026: Power, Risk, and How to Keep Your Self-Hosted AI Agent in Check</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Tue, 10 Mar 2026 06:03:26 +0000</pubDate>
      <link>https://dev.to/saschadev/openclaw-in-2026-power-risk-and-how-to-keep-your-self-hosted-ai-agent-in-check-8a8</link>
      <guid>https://dev.to/saschadev/openclaw-in-2026-power-risk-and-how-to-keep-your-self-hosted-ai-agent-in-check-8a8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13599ucem8y0lz0pb32k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13599ucem8y0lz0pb32k.png" alt="OpenClaw in 2026: Power, Risk, and How to Keep Your Self-Hosted AI Agent in Check" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OpenClaw is one of those projects that looks harmless in a README and very different once you’ve given it real access to your life.&lt;/p&gt;

&lt;p&gt;From the outside it’s “just” a self-hosted AI agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a control plane on your own machine or VPS,&lt;/li&gt;
&lt;li&gt;a chat interface where you “DM your assistant like a friend”,&lt;/li&gt;
&lt;li&gt;and skills that let it work with your files, calendar, home automation, dev tools, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the inside it’s something else:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You’re effectively giving an AI a remote control for everything you can do on that system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s both the point and the risk.&lt;/p&gt;

&lt;p&gt;In this post I want to walk through how I see OpenClaw in 2026 from a &lt;strong&gt;security&lt;/strong&gt; angle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what it actually is,&lt;/li&gt;
&lt;li&gt;why self-hosted doesn’t automatically mean “safe”,&lt;/li&gt;
&lt;li&gt;a few concrete misconfiguration risks that have already shown up in the wild,&lt;/li&gt;
&lt;li&gt;and some practical hardening advice – plus a couple of use cases where the trade-off is worth it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What OpenClaw actually is (in practice)
&lt;/h2&gt;

&lt;p&gt;If you strip away buzzwords, OpenClaw is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a daemon that runs under your user account (on a server, a Mac Mini, a VM…),&lt;/li&gt;
&lt;li&gt;a toolbox for AI agents: shell access, HTTP, file I/O, messaging, cron, browser automation, …&lt;/li&gt;
&lt;li&gt;a chat/control surface (Telegram, Signal, Discord, web UI) to talk to that daemon,&lt;/li&gt;
&lt;li&gt;and a skills system that wires specific workflows together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The security model is very simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The agent can do anything you can do on that machine, plus whatever external APIs you wire in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s powerful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can have an AI look through log files,&lt;/li&gt;
&lt;li&gt;auto-generate blog drafts,&lt;/li&gt;
&lt;li&gt;monitor services,&lt;/li&gt;
&lt;li&gt;interact with Jira, M365, home automation, dev tools, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it’s also a clear warning sign: if you wouldn’t trust a human with your shell and API keys, you shouldn’t trust an unconstrained agent with them either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosted ≠ automatically safe
&lt;/h2&gt;

&lt;p&gt;A lot of people mentally equate “self-hosted” with “secure”.&lt;/p&gt;

&lt;p&gt;That’s not how it works.&lt;/p&gt;

&lt;p&gt;Self-hosted AI agents like OpenClaw remove one layer of risk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your &lt;strong&gt;control plane&lt;/strong&gt; and context live on a machine you manage.&lt;/li&gt;
&lt;li&gt;Data doesn’t flow through a SaaS provider’s infrastructure (beyond the LLM API you choose).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But they add another layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have to harden the host yourself.&lt;/li&gt;
&lt;li&gt;You are responsible for network exposure, API keys, file permissions, cron jobs, etc.&lt;/li&gt;
&lt;li&gt;If you misconfigure it, there is no vendor kill switch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The security posture of OpenClaw is basically:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As secure as the machine you run it on, and as careful as you are with its capabilities.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That can be very good – or very bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common risk patterns we’re already seeing
&lt;/h2&gt;

&lt;p&gt;When you look at write-ups from cloud providers and security firms around OpenClaw, a few themes show up:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Exposed instances on the public internet
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Some people run OpenClaw on a VPS and expose the control port directly to the internet (no firewall, no reverse proxy, default config).&lt;/li&gt;
&lt;li&gt;If there’s any bug or weak auth in the control channel, you’ve essentially opened a remote shell to anyone who finds it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep the control plane behind a VPN / private network,&lt;/li&gt;
&lt;li&gt;or at least protect it with a reverse proxy (nginx, Caddy) + strong auth + IP restrictions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Running as root / with too-broad permissions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Running OpenClaw as root means any agent action is effectively root.&lt;/li&gt;
&lt;li&gt;Even as a normal user, if that user has passwordless sudo or access to sensitive directories, the agent inherits that power.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run OpenClaw under a &lt;strong&gt;dedicated, least-privilege user&lt;/strong&gt; ,&lt;/li&gt;
&lt;li&gt;restrict that user’s access to only what the agent really needs,&lt;/li&gt;
&lt;li&gt;avoid passwordless sudo or broad sudoers rules tied to that account.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Dropping secrets and API keys directly into skills
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Hardcoding API keys in skill scripts or environment without scoping them.&lt;/li&gt;
&lt;li&gt;Giving the agent “all the keys to everything” instead of per-skill/per-service keys with tight scopes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep secrets in a dedicated, minimal &lt;code&gt;.env&lt;/code&gt; or secrets manager,&lt;/li&gt;
&lt;li&gt;use different keys/tokens per service with least privilege (e.g. read-only where possible),&lt;/li&gt;
&lt;li&gt;never give the agent access to banking, HR, or other high-risk systems unless you really know what you’re doing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Over-trusting model behaviour
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Prompting an LLM to “run whatever commands you think are needed” without guardrails.&lt;/li&gt;
&lt;li&gt;Letting it auto-accept or auto-execute actions from external inputs (webhooks, emails, chat, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep a &lt;strong&gt;human in the loop&lt;/strong&gt; for destructive or sensitive actions,&lt;/li&gt;
&lt;li&gt;design skills so that the agent proposes actions, but you confirm,&lt;/li&gt;
&lt;li&gt;log actions and review unusual commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practical hardening tips for an OpenClaw deployment
&lt;/h2&gt;

&lt;p&gt;If I had to summarise a baseline hardening checklist for a serious OpenClaw instance:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Use a dedicated user and machine
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Run OpenClaw under a user (&lt;code&gt;openclaw&lt;/code&gt;, &lt;code&gt;ai-agent&lt;/code&gt;, …) that:&lt;/li&gt;
&lt;li&gt;has no sudo rights,&lt;/li&gt;
&lt;li&gt;only has access to directories you consciously allow (workspace, logs, some project folders).&lt;/li&gt;
&lt;li&gt;Prefer a dedicated VPS or small server over your daily-use laptop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Keep the control port private
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bind the control server to &lt;code&gt;localhost&lt;/code&gt; or a private network interface.&lt;/li&gt;
&lt;li&gt;Use a VPN (WireGuard, Tailscale) or SSH tunnelling for remote access.&lt;/li&gt;
&lt;li&gt;If you must expose it via HTTP(S), put it behind a reverse proxy with:&lt;/li&gt;
&lt;li&gt;HTTPS,&lt;/li&gt;
&lt;li&gt;strong auth (OIDC, access tokens, IP allowlist),&lt;/li&gt;
&lt;li&gt;and rate limiting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Scope skills tightly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Instead of a skill that can &lt;code&gt;exec&lt;/code&gt; arbitrary shell commands everywhere, prefer:&lt;/li&gt;
&lt;li&gt;specific scripts for specific tasks,&lt;/li&gt;
&lt;li&gt;limited working directories,&lt;/li&gt;
&lt;li&gt;explicit allowlists of commands.&lt;/li&gt;
&lt;li&gt;For external systems (Jira, M365, SAP, …), use service principals with minimal scopes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Log and monitor
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Log all agent-initiated commands and API calls.&lt;/li&gt;
&lt;li&gt;Set up simple alerts for:&lt;/li&gt;
&lt;li&gt;unusual command patterns,&lt;/li&gt;
&lt;li&gt;high error rates,&lt;/li&gt;
&lt;li&gt;spikes in external API usage.&lt;/li&gt;
&lt;li&gt;Periodically review logs like you would for a CI/CD system.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Separate “toy” and “production”
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Have a sandbox instance where you play with new skills,&lt;/li&gt;
&lt;li&gt;and a more locked-down instance for anything that touches important infrastructure.&lt;/li&gt;
&lt;li&gt;Don’t test experimental agents on the same machine that has access to production secrets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use cases where OpenClaw shines (and the security trade-offs)
&lt;/h2&gt;

&lt;p&gt;Despite the risks, there are use cases where a well-hardened OpenClaw instance is worth it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Personal AI ops assistant
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Monitor services (via cron + curl + logs parsing),&lt;/li&gt;
&lt;li&gt;summarise incidents,&lt;/li&gt;
&lt;li&gt;open/triage tickets,&lt;/li&gt;
&lt;li&gt;generate postmortems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security angle:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat it like a junior SRE with read-only access to prod metrics/logs and limited ticket/alert rights,&lt;/li&gt;
&lt;li&gt;not like a root shell glued to a model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Developer productivity hub
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generate and update documentation from your codebase,&lt;/li&gt;
&lt;li&gt;run safe automated refactors in local clones,&lt;/li&gt;
&lt;li&gt;help you navigate multiple repos and PRs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security angle:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give it access only to the repos where it’s needed,&lt;/li&gt;
&lt;li&gt;keep it away from deployment keys and secrets,&lt;/li&gt;
&lt;li&gt;enforce human review before any change gets merged.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Knowledge and blog assistant
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Draft posts,&lt;/li&gt;
&lt;li&gt;aggregate notes and links,&lt;/li&gt;
&lt;li&gt;keep track of “what did I do on project X last month?”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security angle:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Low risk as long as you don’t feed it sensitive docs,&lt;/li&gt;
&lt;li&gt;perfect playground for new skills.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Home automation / personal life admin
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Interact with Home Assistant, calendars, todo lists, shopping…&lt;/li&gt;
&lt;li&gt;Very comfortable, but also very revealing about your private life.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security angle:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run it on a local machine/Mac Mini behind your home router/VPN,&lt;/li&gt;
&lt;li&gt;think carefully before wiring in anything with cameras, locks or finances.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Concrete security ideas I’d put into config
&lt;/h2&gt;

&lt;p&gt;If I were writing an OpenClaw config/skill set for myself or someone like you, I’d:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;tag skills by risk level&lt;/strong&gt; : &lt;code&gt;safe&lt;/code&gt;, &lt;code&gt;sensitive&lt;/code&gt;, &lt;code&gt;dangerous&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;require:&lt;/li&gt;
&lt;li&gt;no confirmation for &lt;code&gt;safe&lt;/code&gt; (read-only queries, summaries),&lt;/li&gt;
&lt;li&gt;explicit confirmation for &lt;code&gt;sensitive&lt;/code&gt; (writes, API calls),&lt;/li&gt;
&lt;li&gt;two-step confirmation or even a separate instance for &lt;code&gt;dangerous&lt;/code&gt; (anything with infra or finances).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’d also consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a simple &lt;strong&gt;denylist of prompt topics&lt;/strong&gt; for certain skills:&lt;/li&gt;
&lt;li&gt;no HR/compensation data,&lt;/li&gt;
&lt;li&gt;no copying entire password stores,&lt;/li&gt;
&lt;li&gt;no scraping banking emails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don’t have to forbid everything – but a few hard “No, this agent never does that” help avoid accidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  My take: OpenClaw is powerful, but it needs an adult in the room
&lt;/h2&gt;

&lt;p&gt;OpenClaw’s promise is compelling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your own AI agent,&lt;/li&gt;
&lt;li&gt;on your own hardware,&lt;/li&gt;
&lt;li&gt;with deep integrations into the stuff you actually care about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From a security point of view, I see it as a mix of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI/CD system,&lt;/li&gt;
&lt;li&gt;home lab server,&lt;/li&gt;
&lt;li&gt;and a very curious co-worker with shell and API keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you treat it that way – with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clear permissions,&lt;/li&gt;
&lt;li&gt;separate environments,&lt;/li&gt;
&lt;li&gt;logging &amp;amp; monitoring,&lt;/li&gt;
&lt;li&gt;and a conscious scope,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;you can get a lot of value out of OpenClaw without putting your environment at unnecessary risk.&lt;/p&gt;

&lt;p&gt;If you treat it like a toy that “just runs on the side” and can access everything, it’s only a matter of time until you shoot yourself in the foot – with or without a hyperactive agent.&lt;/p&gt;

</description>
      <category>openclaw</category>
      <category>security</category>
      <category>aiagents</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Bringing Your Own Data into Microsoft 365 Copilot (Without Breaking Security)</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Sun, 08 Mar 2026 11:00:58 +0000</pubDate>
      <link>https://dev.to/saschadev/bringing-your-own-data-into-microsoft-365-copilot-without-breaking-security-3ple</link>
      <guid>https://dev.to/saschadev/bringing-your-own-data-into-microsoft-365-copilot-without-breaking-security-3ple</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fay3i8rfq615020thbytp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fay3i8rfq615020thbytp.png" alt="Bringing Your Own Data into Microsoft 365 Copilot (Without Breaking Security)" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When people first see Microsoft 365 Copilot, the first questions are usually fun:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Summarise this document.”&lt;/li&gt;
&lt;li&gt;“Draft a reply to this email.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nice for demos – but not why I’m interested.&lt;/p&gt;

&lt;p&gt;The interesting part starts when you ask Copilot things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“How do we deploy project Phoenix?”&lt;/li&gt;
&lt;li&gt;“What did we decide about feature X last quarter?”&lt;/li&gt;
&lt;li&gt;“Show me our API guidelines.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those answers live in your &lt;em&gt;own&lt;/em&gt; systems: SharePoint, Confluence, Jira, custom apps, file shares. Bringing that data into Copilot is where the value is – and where you can easily break security if you’re not careful.&lt;/p&gt;

&lt;p&gt;In this post I want to give a pragmatic overview of how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bring your own data into Microsoft 365 Copilot,&lt;/li&gt;
&lt;li&gt;understand where security trimming happens,&lt;/li&gt;
&lt;li&gt;and actively prevent certain things from being answered at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code is copy‑paste‑able; adapt it to your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Three main paths for your own data
&lt;/h2&gt;

&lt;p&gt;Simplified, you have three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data already in M365&lt;/strong&gt;
SharePoint, OneDrive, Teams, Exchange – indexed by Microsoft Search/Graph out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External content via Microsoft Graph connectors&lt;/strong&gt;
Confluence, Jira, on‑prem file shares, custom data – indexed as &lt;code&gt;externalItem&lt;/code&gt; objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom agents/plugins that call your APIs at runtime&lt;/strong&gt;
For live data and actions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We’ll go through each with one question in mind: &lt;strong&gt;who decides what the user is allowed to see?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Data already in M365: Graph trims for you
&lt;/h2&gt;

&lt;p&gt;For content living in SharePoint/OneDrive/Teams/Exchange, the flow looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mermaid.live/edit?ref=blog.bajonczak.com#pako:eNo9kMtuwjAQRX_FmnWgmEceXnQDqEIqAjVdIBIWVjJJLCV2NDgtFPHvdRKJ1cz13Dka3wdkJkcQUNTmN6skWfb5lWrG4mMSO4lHo7Rlb-ygcUPqBy9sMnlnu80p2auMzNUUlsUoKauc6YNkW7GdzvF26SHbU7K9Oaou0U2_UTbX134_d2WQ68MxWZtW1ca6PfCgQWqkyt1dj96Xgq2wwRSEa3MsZFfbFFL9dFbZWRPfdQaikPUVPejaXFrcKFmSbEBY6txjKzWIB9xAcO5PZ3y-DH0_nHO-4HMP7iCCYBoso2ixjFYzP-RB9PTgzxgHmE3DYDUQzoMegZgra2g_hjdk6AGZrqxed5TUf2B0E7pMaG06bUFEz39KoXXm" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxvhmt7j8xwz38uf0ed7.png" alt="Bringing Your Own Data into Microsoft 365 Copilot (Without Breaking Security)" width="706" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Security trimming happens in Graph/Search:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Items have ACLs tied to users/groups.&lt;/li&gt;
&lt;li&gt;Graph only returns items the &lt;strong&gt;current user&lt;/strong&gt; can see.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your job here ist „nur“:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docs, die für Copilot relevant sein sollen, wirklich nach M365 ziehen.&lt;/li&gt;
&lt;li&gt;Berechtigungen in SharePoint/Teams sinnvoll pflegen (keine „Everyone“-Rechte auf sensible Sites).&lt;/li&gt;
&lt;li&gt;Nicht am Graph vorbei direkt auf Datenbanken/Storage zugreifen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wenn du nichts anderes tust, ist „erstmal alles in M365 ordentlich ablegen“ der wichtigste Schritt.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. External content: Graph connectors + ACLs
&lt;/h2&gt;

&lt;p&gt;Für Systeme, die nicht in M365 leben können oder sollen (Confluence, on‑prem shares, Legacy‑Apps), sind &lt;strong&gt;Microsoft Graph connectors&lt;/strong&gt; das Mittel der Wahl.&lt;/p&gt;

&lt;p&gt;Ein Connector:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;zieht oder erhält Items aus einem externen System,&lt;/li&gt;
&lt;li&gt;legt sie als &lt;code&gt;externalItem&lt;/code&gt; in einer &lt;code&gt;externalConnection&lt;/code&gt; im Graph ab,&lt;/li&gt;
&lt;li&gt;und versieht sie mit einer &lt;code&gt;acl&lt;/code&gt;, die Security Trimming steuert.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.1. Connection anlegen
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/graphConnection.ts
import fetch from 'node-fetch';

const GRAPH_BASE = 'https://graph.microsoft.com/v1.0';

async function getAppToken(): Promise&amp;lt;string&amp;gt; {
  // Implement client credentials flow for your app registration
  // return access token with ExternalConnection.ReadWrite.All etc.
  return '&amp;lt;token&amp;gt;';
}

export async function createExternalConnection(connectionId: string, name: string, description: string) {
  const token = await getAppToken();

  const res = await fetch(`${GRAPH_BASE}/external/connections`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify({ id: connectionId, name, description })
  });

  if (!res.ok) {
    console.error('Failed to create connection', await res.text());
    throw new Error('graph_error');
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.2. Items mit ACLs pushen
&lt;/h3&gt;

&lt;p&gt;Angenommen, du ziehst Seiten aus Confluence und bekommst:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type SourcePage = {
  id: string;
  title: string;
  url: string;
  body: string;
  allowedUsers: string[]; // AAD IDs oder UPNs
  forbiddenUsers?: string[]; // optional: explizite Ausschlüsse
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Du kannst das in ein &lt;code&gt;externalItem&lt;/code&gt; mit &lt;code&gt;acl&lt;/code&gt; mappen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/graphItems.ts
import fetch from 'node-fetch';

const GRAPH_BASE = 'https://graph.microsoft.com/v1.0';
const CONNECTION_ID = 'contosoConfluence';

async function pushExternalItem(page: SourcePage) {
  const token = await getAppToken();

  const grantAcl = page.allowedUsers.map(userId =&amp;gt; ({
    type: 'user',
    value: userId,
    accessType: 'grant' as const
  }));

  const denyAcl = (page.forbiddenUsers ?? []).map(userId =&amp;gt; ({
    type: 'user',
    value: userId,
    accessType: 'deny' as const
  }));

  const item = {
    id: page.id,
    properties: {
      title: page.title,
      url: page.url
    },
    content: {
      type: 'text',
      value: page.body
    },
    acl: [...grantAcl, ...denyAcl]
  };

  const res = await fetch(
    `${GRAPH_BASE}/external/connections/${CONNECTION_ID}/items/${page.id}`,
    {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      },
      body: JSON.stringify(item)
    }
  );

  if (!res.ok) {
    console.error('Failed to push externalItem', await res.text());
    throw new Error('graph_error');
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wichtig:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security Trimming hängt an dieser ACL&lt;/strong&gt;. Wenn hier Mist steht, sieht Copilot denselben Mist.&lt;/li&gt;
&lt;li&gt;„Forbidden“‑User kannst du mit &lt;code&gt;accessType: 'deny'&lt;/code&gt; explizit ausschließen (je nach Szenario sinnvoll).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ab dann behandelt Microsoft Search/Copilot diese Items wie native Inhalte – natürlich nur, wenn du sie im Copilot‑Scope aktiviert hast.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Custom Agents: Live-Daten mit eigenem Backend
&lt;/h2&gt;

&lt;p&gt;Connectors sind super für „Was wissen wir?“‑Fragen. Für „Was ist gerade der Status?“ oder „Leg ein Ticket an“ brauchst du eine HTTP‑Schnittstelle, an die Copilot Tools/Agents Aufrufe schicken können.&lt;/p&gt;

&lt;p&gt;Architektur:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mermaid.live/edit?ref=blog.bajonczak.com#pako:eNo9kFFrwjAUhf9KuM9Vam1s7cNAO2EPG4ypMNf6ENprLaa5kiZMJ_73pRF8uuGcj3Nvzg0qqhEyOEj6rY5CG_b-VSrGtsW2R71noxET_cmNF5YXOZ1bSWY_APlgVUJKZoikBxZFbntDHVs0qB7UYqDeNptPDyyLHVnNlqI6oao9sPTG6ntTrC4GtRKSra-9wc65EECHuhNt7Q68DXQJ5ogdlpC5Z40HYaUpoVR3hwpraH1VFWQHIXsMwJ5rYfC1FY0W3VM9CwXZDS6QTZJ4HM15PEnTKAnDcJYEcHUyHztpPp3FaTqL4ojH9wD-iFzEZMynaTjn0ZSnUcxT7uN-vGe0delYt4b0x6NTX20AmmxzfO5v9PCdB61dC6hzsspAloT3f7awejg" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Froto3w9tk2sszmwy2cib.png" alt="Bringing Your Own Data into Microsoft 365 Copilot (Without Breaking Security)" width="800" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hier liegt &lt;strong&gt;Security Trimming komplett bei dir&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. User-Kontext und Rollen
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/permissions.ts
export type UserContext = {
  email: string;
  roles: string[]; // e.g. ['EMPLOYEE', 'HR', 'ENGINEERING_MANAGER']
  groups: string[]; // e.g. ['project-phoenix', 'dept-engineering']
};

export async function getUserPermissions(email: string): Promise&amp;lt;UserContext | null&amp;gt; {
  const entry = await directoryLookup(email); // Implement: query Entra ID / your IAM
  if (!entry) return null;

  return {
    email,
    roles: entry.roles,
    groups: entry.groups
  };
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.2. ACL für externe Dokumente
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/acl.ts
import { UserContext } from './permissions';

export type DocumentAcl = {
  allowedRoles?: string[];
  allowedGroups?: string[];
  forbiddenRoles?: string[];
};

export type ExternalDoc = {
  id: string;
  title: string;
  url: string;
  content: string;
  acl: DocumentAcl;
};

function hasAccess(doc: ExternalDoc, user: UserContext): boolean {
  const { allowedRoles, allowedGroups, forbiddenRoles } = doc.acl;

  if (forbiddenRoles &amp;amp;&amp;amp; forbiddenRoles.some(r =&amp;gt; user.roles.includes(r))) {
    return false;
  }

  // default: visible to all employees if nothing is specified
  if (!allowedRoles &amp;amp;&amp;amp; !allowedGroups) {
    return true;
  }

  if (allowedRoles &amp;amp;&amp;amp; allowedRoles.some(r =&amp;gt; user.roles.includes(r))) {
    return true;
  }

  if (allowedGroups &amp;amp;&amp;amp; allowedGroups.some(g =&amp;gt; user.groups.includes(g))) {
    return true;
  }

  return false;
}

export function filterDocsByAcl(docs: ExternalDoc[], user: UserContext): ExternalDoc[] {
  return docs.filter(doc =&amp;gt; hasAccess(doc, user));
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.3. Agent-Endpoint mit Trimming und Query-Guard
&lt;/h3&gt;

&lt;p&gt;Jetzt der eigentliche Handler, den Copilot aufruft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/copilotAgent.ts
import { Request, Response } from 'express';
import { getUserPermissions } from './permissions';
import { searchSystemDocs } from './systemDocs';
import { buildAnswerFromDocs } from './rag';

interface AskRequest {
  question: string;
  userEmail: string;
}

interface AskResponse {
  answer: string;
  sources: { title: string; url: string }[];
}

// Simple guard against disallowed topics
function isForbiddenQuestion(question: string, userRoles: string[]): boolean {
  const lower = question.toLowerCase();

  // Example: HR-only topics
  const sensitivePatterns = [
    'salary',
    'compensation',
    'layoff',
    'termination list',
    'performance review'
  ];

  const isSensitive = sensitivePatterns.some(p =&amp;gt; lower.includes(p));

  if (!isSensitive) return false;

  // Only allow HR / C-level roles to ask these topics
  const privilegedRoles = ['HR', 'HR_ADMIN', 'C_LEVEL'];
  const isPrivileged = privilegedRoles.some(r =&amp;gt; userRoles.includes(r));

  return !isPrivileged;
}

export async function copilotAgentHandler(req: Request, res: Response) {
  const body = req.body as AskRequest;

  if (!body.question || !body.userEmail) {
    return res.status(400).json({ error: 'question and userEmail are required' });
  }

  const user = await getUserPermissions(body.userEmail);
  if (!user) {
    return res.status(403).json({ error: 'unknown_user' });
  }

  // 1) Block certain topics for non-privileged roles
  if (isForbiddenQuestion(body.question, user.roles)) {
    return res.json({
      answer: "I’m not allowed to answer this type of question for your role.",
      sources: []
    } satisfies AskResponse);
  }

  // 2) Query docs with ACL
  const docs = await searchSystemDocs(body.question, user);

  if (docs.length === 0) {
    return res.json({
      answer: `I couldn't find any documents you are allowed to see that answer "${body.question}".`,
      sources: []
    } satisfies AskResponse);
  }

  // 3) Build answer via LLM
  const { answer, usedDocs } = await buildAnswerFromDocs(body.question, docs);

  const response: AskResponse = {
    answer,
    sources: usedDocs.map(d =&amp;gt; ({ title: d.title, url: d.url }))
  };

  return res.json(response);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Damit hast du zwei Ebenen von Schutz:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Topic Guard&lt;/strong&gt; : bestimmte Fragen werden für normale Rollen gar nicht erst beantwortet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ACL-Filter&lt;/strong&gt; : selbst wenn die Frage erlaubt ist, kommen nur Dokumente durch, deren ACL zum User passt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Decision table: which path to use when
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Recommended path&lt;/th&gt;
&lt;th&gt;Security trimming&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docs already in SharePoint/OneDrive/Teams&lt;/td&gt;
&lt;td&gt;Native M365 (Graph)&lt;/td&gt;
&lt;td&gt;Graph enforces ACLs; keep SharePoint/Teams permissions tidy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External wiki / KB (Confluence, CMS) with mostly read-only content&lt;/td&gt;
&lt;td&gt;Graph connector&lt;/td&gt;
&lt;td&gt;You attach ACLs per item (&lt;code&gt;acl&lt;/code&gt;); Graph trims based on user token.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-prem file shares&lt;/td&gt;
&lt;td&gt;File share / Azure Files connector&lt;/td&gt;
&lt;td&gt;Connector maps original ACLs to AAD identities; Graph handles trimming.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live line-of-business data (status, metrics, actions)&lt;/td&gt;
&lt;td&gt;Custom agent + backend API&lt;/td&gt;
&lt;td&gt;Your backend enforces roles/groups + topic guards before every call.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Highly sensitive HR/legal data&lt;/td&gt;
&lt;td&gt;Separate, role-specific agents or no Copilot access&lt;/td&gt;
&lt;td&gt;Explicitly exclude from generic agents; only scoped tools for HR/legal.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  6. Guardrails I’d put in place
&lt;/h2&gt;

&lt;p&gt;Unabhängig vom Weg würde ich ein paar Regeln fest einbauen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kein God-Mode-Service-Account ohne weiteren Filter&lt;/strong&gt;
Wenn ein Agent mit einem Konto liest, das alles sieht, brauchst du zwingend ein ACL-Filter im Backend (wie oben).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging&lt;/strong&gt;
Logge (mindestens intern), welcher Agent für welchen User welche Systeme abgefragt hat – nicht unbedingt den kompletten Content, sondern Metadaten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;„No-go“-Daten explizit ausschließen&lt;/strong&gt;
Es ist okay zu sagen: „Bestimmte HR-/Legal- oder Security-Daten tauchen in Copilot nie auf.“ Bau diese Ausschlüsse bewusst ein.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start small&lt;/strong&gt;
Lieber zwei gut abgesicherte Datenquellen als zehn halbgar integrierte.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Fazit
&lt;/h2&gt;

&lt;p&gt;„Bring your own data“ für Microsoft 365 Copilot ist kein einziger Schalter, sondern eine Reihe von Entscheidungen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was gehört in M365 selbst?&lt;/li&gt;
&lt;li&gt;Was wird über Graph Connectors indexiert?&lt;/li&gt;
&lt;li&gt;Was braucht einen eigenen Agenten, der live mit Systemen redet?&lt;/li&gt;
&lt;li&gt;Und wo sagen wir bewusst: „Das bleibt außerhalb der Reichweite von Copilot“?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wenn du jede dieser Entscheidungen mit Security Trimming im Kopf triffst, kann Copilot viel näher an das kommen, was du in Meetings ständig hörst: „Frag doch mal jemanden, der sich auskennt.“ Nur dass „jemand“ diesmal ein Agent ist, der deine Daten kennt – aber nicht mehr, als er kennen darf.&lt;/p&gt;

</description>
      <category>m365copilot</category>
      <category>graphconnectors</category>
      <category>security</category>
      <category>integration</category>
    </item>
    <item>
      <title>Orchestrating SAP and Entra ID with MCP: A Practical Sync &amp; Insights Agent</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Sat, 07 Mar 2026 17:00:46 +0000</pubDate>
      <link>https://dev.to/saschadev/orchestrating-sap-and-entra-id-with-mcp-a-practical-sync-insights-agent-55pm</link>
      <guid>https://dev.to/saschadev/orchestrating-sap-and-entra-id-with-mcp-a-practical-sync-insights-agent-55pm</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjj2mzpcjek50cu7hh6d6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjj2mzpcjek50cu7hh6d6.png" alt="Orchestrating SAP and Entra ID with MCP: A Practical Sync &amp;amp; Insights Agent"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In many enterprises, SAP (especially SuccessFactors) is still the &lt;strong&gt;source of truth&lt;/strong&gt; for people and org data, while Entra ID (formerly Azure AD) is the &lt;strong&gt;front door&lt;/strong&gt; for apps and services.&lt;/p&gt;

&lt;p&gt;Bridging both worlds is usually done with brittle custom scripts, point-to-point sync jobs, or black-box connectors that are hard to debug.&lt;/p&gt;

&lt;p&gt;In this post I will show how to use the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; as a thin orchestration layer between SAP and Entra ID:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;to &lt;strong&gt;compare&lt;/strong&gt; what exists in both systems,&lt;/li&gt;
&lt;li&gt;to generate a &lt;strong&gt;delta report&lt;/strong&gt; ,&lt;/li&gt;
&lt;li&gt;and to provide &lt;strong&gt;safe hooks&lt;/strong&gt; for remediation (tickets, follow-ups) – without giving an agent god-mode access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will keep it concrete and code-backed, not just architecture diagrams.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What we want to achieve
&lt;/h2&gt;

&lt;p&gt;The goal is a small MCP server that exposes tools like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sap_list_users&lt;/code&gt; – fetch users from SAP/SuccessFactors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entra_list_users&lt;/code&gt; – fetch users from Entra ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;report_mismatches&lt;/code&gt; – compute and return deltas:&lt;/li&gt;
&lt;li&gt;users in SAP but not in Entra&lt;/li&gt;
&lt;li&gt;users in Entra but not in SAP&lt;/li&gt;
&lt;li&gt;users with mismatched attributes (e.g. department, manager)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An LLM/agent (Copilot or any MCP-aware client) can then ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Check if there are mismatches between SAP and Entra ID for the Engineering department and give me a summary plus CSV.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent does the orchestration, but all hard security and connectivity lives in your backend, not in the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. High-level architecture
&lt;/h2&gt;

&lt;p&gt;At a high level, the setup looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuxex400knzeeniboyd5y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuxex400knzeeniboyd5y.png" alt="Orchestrating SAP and Entra ID with MCP: A Practical Sync &amp;amp; Insights Agent"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The MCP server exposes tools, tools delegate to typed backend clients for SAP and Entra. The agent only sees high-level operations; it never touches raw credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Project setup
&lt;/h2&gt;

&lt;p&gt;We will build a minimal Node.js / TypeScript project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;mcp-sap-entra-agent
&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-sap-entra-agent
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;typescript ts-node @types/node axios
npm &lt;span class="nb"&gt;install&lt;/span&gt; @modelcontextprotocol/sdk
npx tsc &lt;span class="nt"&gt;--init&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Folder structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp-sap-entra-agent/
  src/
    config.ts
    sapClient.ts
    entraClient.ts
    mismatches.ts
    mcpServer.ts
  .env
  package.json
  tsconfig.json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Environment (&lt;code&gt;.env&lt;/code&gt;, never commit this):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SAP_BASE_URL="https://api.successfactors.eu/odata/v2"
SAP_USERNAME="..."
SAP_PASSWORD="..." # or OAuth token

ENTRA_TENANT_ID="..."
ENTRA_CLIENT_ID="..."
ENTRA_CLIENT_SECRET="..."

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Configuration helper
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SAP_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SAP_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SAP_USERNAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SAP_USERNAME&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SAP_PASSWORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SAP_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ENTRA_TENANT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ENTRA_TENANT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ENTRA_CLIENT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ENTRA_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ENTRA_CLIENT_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ENTRA_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;SAP_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;SAP_USERNAME&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;SAP_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[WARN] SAP config incomplete – SAP tools will not work until configured.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ENTRA_TENANT_ID&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ENTRA_CLIENT_ID&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ENTRA_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[WARN] Entra config incomplete – Entra tools will not work until configured.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. SAP client (SuccessFactors via OData)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/sapClient.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SAP_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SAP_USERNAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SAP_PASSWORD&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SapUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;department&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;listSapUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SapUser&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;SAP_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SAP_BASE_URL not configured&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SAP_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/User?$format=json&amp;amp;$top=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;$select=userId,email,firstName,lastName,department`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SAP_USERNAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SAP_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;department&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Entra ID client (Microsoft Graph)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/entraClient.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ENTRA_TENANT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ENTRA_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ENTRA_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EntraUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;department&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://login.microsoftonline.com/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ENTRA_TENANT_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/oauth2/v2.0/token`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ENTRA_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ENTRA_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scope&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://graph.microsoft.com/.default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grant_type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;listEntraUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EntraUser&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://graph.microsoft.com/v1.0/users?$top=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;$select=id,userPrincipalName,mail,displayName,department`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;u&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;department&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Computing mismatches
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/mismatches.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SapUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./sapClient&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;EntraUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./entraClient&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SyncMismatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MissingInEntra&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MissingInSap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AttributeMismatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sapUser&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;SapUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;entraUser&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;EntraUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;details&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;computeMismatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;sapUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SapUser&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;entraUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntraUser&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;SyncMismatch&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SyncMismatch&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entraByEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EntraUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entraByUpn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EntraUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entraUsers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailNorm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailNorm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;entraByEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailNorm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;entraByUpn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 1) SAP -&amp;gt; Entra&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;sapUsers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailNorm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntraUser&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailNorm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entraByEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailNorm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entraByUpn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MissingInEntra&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sapUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`No Entra user matching SAP userId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, email=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// compare attributes&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sapDept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entraDept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;department&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sapDept&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;entraDept&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sapDept&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;entraDept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AttributeMismatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sapUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;entraUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Department mismatch: SAP="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sapDept&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" vs ENTRA="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entraDept&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2) Entra -&amp;gt; SAP&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sapEmails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;sapUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sapUserIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;sapUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entraUsers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailNorm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailInSap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;emailNorm&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sapEmails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailNorm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idInSap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sapUserIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;upn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;emailInSap&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;idInSap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MissingInSap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;entraUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`No SAP user for Entra UPN=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, mail=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Wiring it into an MCP server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/mcpServer.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMcpServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ToolDefinition&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;listSapUsers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./sapClient&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;listEntraUsers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./entraClient&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;computeMismatches&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./mismatches&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolDefinition&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sap_list_users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;List users from SAP / SuccessFactors. Optional: limit.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maximum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sapUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;listSapUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sapUsers&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;entra_list_users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;List users from Entra ID (Azure AD). Optional: limit.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maximum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entraUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;listEntraUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entraUsers&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;report_mismatches&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Compare SAP and Entra ID users and return mismatches (missing users, attribute mismatches).&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;limitSap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maximum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;limitEntra&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maximum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sapUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;listSapUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limitSap&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entraUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;listEntraUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limitEntra&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mismatches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeMismatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sapUsers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entraUsers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;totalSap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sapUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;totalEntra&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entraUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;mismatchCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMcpServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[MCP] SAP/Entra agent listening on :3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;startServer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[MCP] Failed to start server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Hardening and next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Scope your Graph permissions carefully (no unnecessary Directory.Read.All in production).&lt;/li&gt;
&lt;li&gt;Add logging &amp;amp; auditing around which user triggered which comparison.&lt;/li&gt;
&lt;li&gt;Keep remediation (creating/updating users) as a separate, well-controlled flow.&lt;/li&gt;
&lt;li&gt;Consider adding department filters or project keys to narrow down the comparison.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a follow-up, you could plug this MCP server into an agent that not only generates the delta report, but also opens Jira tickets or compiles a weekly summary for your identity team.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>sap</category>
      <category>entra</category>
      <category>agents</category>
    </item>
    <item>
      <title>How to Enable Microsoft 365 eSignature in Your Tenant</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Fri, 06 Mar 2026 11:17:28 +0000</pubDate>
      <link>https://dev.to/saschadev/how-to-enable-microsoft-365-esignature-in-your-tenant-2a8b</link>
      <guid>https://dev.to/saschadev/how-to-enable-microsoft-365-esignature-in-your-tenant-2a8b</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrf3b71ml5k6hmo1uagq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrf3b71ml5k6hmo1uagq.png" alt="How to Enable Microsoft 365 eSignature in Your Tenant" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my &lt;a href="https://blog.bajonczak.com/security-trimming-with-microsoft-365-copilot-asking-the-right-data-in-the-right-context/" rel="noopener noreferrer"&gt;last post&lt;/a&gt; I looked at &lt;strong&gt;why&lt;/strong&gt; Microsoft 365 eSignature is interesting: you can request and collect electronic signatures directly in SharePoint, OneDrive and Word, without sending your documents off to a separate platform.&lt;/p&gt;

&lt;p&gt;This follow-up is for a different audience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microsoft 365 admins,&lt;/li&gt;
&lt;li&gt;SharePoint admins,&lt;/li&gt;
&lt;li&gt;and technically inclined people who get asked: “Can you please turn this on for us?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: this is the &lt;strong&gt;“how do I actually enable this in a tenant?”&lt;/strong&gt; post.&lt;/p&gt;

&lt;p&gt;I’ll keep it business-friendly at the top and concrete at the bottom, with screenshots replaced by clear descriptions and some PowerShell where it makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What you’re enabling – in business terms
&lt;/h2&gt;

&lt;p&gt;Before touching any admin portal, it helps to be able to explain in one sentence what you’re doing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We’re enabling a Microsoft 365 feature that lets people request and sign documents directly in SharePoint / OneDrive / Word, with usage billed via Azure.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Key points you can use when talking to stakeholders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No extra platform&lt;/strong&gt; for many scenarios – signing happens where the files already live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing providers can stay&lt;/strong&gt; – if you use Adobe Sign or DocuSign, you can plug them in behind the M365 eSignature UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pay-per-use&lt;/strong&gt; via Azure – no new fixed M365 plan, but a usage-based service you can monitor and cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same identity model&lt;/strong&gt; – signers are M365 users or guests, so you stay inside your existing compliance and audit framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once people are comfortable with that, you can move to the actual steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. High-level architecture
&lt;/h2&gt;

&lt;p&gt;Early on, I found this mental model helpful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph M365
    SP[SharePoint / OneDrive]
    W[Word]
  end

  M365 -- uses --&amp;gt; ESig[eSignature Service]
  ESig -- billed via --&amp;gt; AZ[Azure Subscription]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the tenant’s perspective:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users trigger sign requests from SharePoint / Word.&lt;/li&gt;
&lt;li&gt;Behind the scenes, the eSignature service runs and bills usage to an Azure subscription.&lt;/li&gt;
&lt;li&gt;Signed documents land back in your libraries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Prerequisites checklist
&lt;/h2&gt;

&lt;p&gt;Before doing anything else, make sure these boxes are ticked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure subscription&lt;/strong&gt; with a valid billing setup (credit card, CSP, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft 365 tenant&lt;/strong&gt; with SharePoint Online and OneDrive for Business.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Office apps for Enterprise&lt;/strong&gt; if you want the Word integration (desktop/web).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External collaboration policy&lt;/strong&gt; that allows guests, if you plan to have external signers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I strongly recommend doing the initial enablement in a &lt;strong&gt;test tenant&lt;/strong&gt; or at least on a test site collection before you roll this out to everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Linking Microsoft 365 to Azure pay-as-you-go
&lt;/h2&gt;

&lt;p&gt;The eSignature feature is part of the broader “pay-as-you-go services for Microsoft 365”. You need to link your M365 tenant to an Azure subscription so usage can be billed.&lt;/p&gt;

&lt;p&gt;High-level steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to the Azure portal as a subscription owner.&lt;/li&gt;
&lt;li&gt;Verify that you have a subscription ready for pay-as-you-go services (or create one).&lt;/li&gt;
&lt;li&gt;Sign in to the &lt;strong&gt;Microsoft 365 admin center&lt;/strong&gt; as a global admin.&lt;/li&gt;
&lt;li&gt;Navigate to the section for &lt;strong&gt;pay-as-you-go services&lt;/strong&gt; (typically under Settings → Org settings → SharePoint / SharePoint Premium).&lt;/li&gt;
&lt;li&gt;Choose the Azure subscription you want to link to your M365 tenant for document processing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once this link is in place, SharePoint/M365 can start using eSignature and other document processing features against that Azure subscription.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. Verifying with PowerShell (optional)
&lt;/h3&gt;

&lt;p&gt;If you like to double-check settings via script, you can use the SharePoint Online Management Shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Connect to your tenant
Connect-SPOService -Url https://&amp;lt;your-tenant&amp;gt;-admin.sharepoint.com

# Check if SharePoint Premium is enabled (naming may vary by SKU)
Get-SPOTenant | Select-Object IsSharePointPremiumEnabled

# Enable SharePoint Premium if required
Set-SPOTenant -IsSharePointPremiumEnabled $true

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always cross-check the exact property names with the latest Microsoft documentation; they sometimes change between previews and GA.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Guest access and sharing – don’t skip this
&lt;/h2&gt;

&lt;p&gt;Because eSignature uses secure share links and M365 identities, external signers need to exist as guests and be allowed to sign in.&lt;/p&gt;

&lt;p&gt;Two places to review:&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1. External collaboration settings (Entra ID / Azure AD)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;In the Entra ID portal, go to &lt;strong&gt;External identities → External collaboration settings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Confirm that guests are allowed.&lt;/li&gt;
&lt;li&gt;Check who is allowed to invite guests (admins only, members, specific roles).&lt;/li&gt;
&lt;li&gt;Review any domain allow/block lists – make sure you’re not blocking the domains you want to sign with.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5.2. SharePoint / OneDrive sharing policies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;In the SharePoint admin center, go to &lt;strong&gt;Policies → Sharing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;For the tenant and for the specific sites you will use, ensure at least “Existing guests” or “New and existing guests” are allowed.&lt;/li&gt;
&lt;li&gt;You do not need anonymous links for eSignature, and I’d recommend keeping them off for sensitive libraries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this, sign requests to external people will fail in confusing ways (“link doesn’t work”, “I can’t access the document”). Better to align with your security team upfront.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Enabling eSignature in the M365 admin experience
&lt;/h2&gt;

&lt;p&gt;With billing and guest access ready, you can finally flip the actual feature switch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0wqos7rbc3abzb5kklt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0wqos7rbc3abzb5kklt.png" alt="How to Enable Microsoft 365 eSignature in Your Tenant" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Microsoft 365 admin center, go to the &lt;strong&gt;SharePoint admin center&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Look for &lt;strong&gt;SharePoint Premium&lt;/strong&gt; or content services settings.&lt;/li&gt;
&lt;li&gt;Find the section for &lt;strong&gt;eSignature&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enable eSignature for your tenant and confirm it can use the linked Azure subscription.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your organisation already uses Adobe Sign or DocuSign and you want to keep them in the loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;connect your Adobe/DocuSign tenant as a provider in the eSignature settings,&lt;/li&gt;
&lt;li&gt;grant the required API permissions so M365 can orchestrate sign requests via that provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From a user’s perspective, they will see “Request signature” in M365; under the hood, the actual signature may still be processed by Adobe/DocuSign.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Where users will see eSignature once it’s enabled
&lt;/h2&gt;

&lt;p&gt;After the switches are on, you can validate the setup by looking in three places.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.1. SharePoint document libraries
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Go to a test document library.&lt;/li&gt;
&lt;li&gt;Upload a sample Word/PDF file.&lt;/li&gt;
&lt;li&gt;Open the context menu or command bar → you should see an option like “Request eSignature” or similar.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7.2. Word (desktop / web)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Open a document stored in SharePoint/OneDrive.&lt;/li&gt;
&lt;li&gt;Look for a “Sign” / “Request signatures” entry in the ribbon or File menu.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: this may depend on your Office build and licensing; make sure you test with an account that has the right SKU.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.3. Power Automate
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Open Power Automate and create a new flow.&lt;/li&gt;
&lt;li&gt;Search for eSignature-related actions or for your provider connector (Adobe Sign, DocuSign) if you integrated one.&lt;/li&gt;
&lt;li&gt;You should see actions to start a signing process, check status, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. A minimal end-to-end flow to prove it works
&lt;/h2&gt;

&lt;p&gt;Before you tell everyone “it’s live”, I’d build at least one simple end-to-end flow to prove the whole chain.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trigger: When a file is created in Library "Contracts/Pending"
Action: Get file metadata
Action: Start eSignature request (document = file, signers from a column or manual list)
Action: Wait until eSignature status = Completed
Action: Copy or move signed file to Library "Contracts/Signed"
Action: Post message in Teams channel "Legal" with link to the signed file

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn’t have to be your final production flow, but it gives you confidence that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;billing works,&lt;/li&gt;
&lt;li&gt;guest access is configured correctly,&lt;/li&gt;
&lt;li&gt;and your users will actually see signed files where you expect them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Admin checklist before rolling out broadly
&lt;/h2&gt;

&lt;p&gt;In tenant projects I like to ask a few questions before we roll eSignature out to everyone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do we know which document types should use M365 eSignature and which stay on specialised providers?&lt;/li&gt;
&lt;li&gt;Have we defined who can request signatures and from which sites/libraries?&lt;/li&gt;
&lt;li&gt;Is guest access configured in line with our security policy?&lt;/li&gt;
&lt;li&gt;Do we have monitoring in place for Azure usage/costs for eSignature?&lt;/li&gt;
&lt;li&gt;Do we know how to quickly disable eSignature if something unexpected happens?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once those are answered, the technical enablement is straightforward – and you can point your colleagues to the previous article for the “why” and this one for the “how”.&lt;/p&gt;

</description>
      <category>m365</category>
      <category>esignature</category>
      <category>administration</category>
      <category>howto</category>
    </item>
    <item>
      <title>Electronic Signatures in Microsoft 365: Using eSignature Without Leaving Your Tenant</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Thu, 05 Mar 2026 07:27:35 +0000</pubDate>
      <link>https://dev.to/saschadev/electronic-signatures-in-microsoft-365-using-esignature-without-leaving-your-tenant-2h29</link>
      <guid>https://dev.to/saschadev/electronic-signatures-in-microsoft-365-using-esignature-without-leaving-your-tenant-2h29</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2mgt2iyb8vynqx8cbzw2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2mgt2iyb8vynqx8cbzw2.png" alt="Electronic Signatures in Microsoft 365: Using eSignature Without Leaving Your Tenant" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watching people in 2026 still print out Word documents, sign them by hand, scan them and send them back by email feels a bit surreal. We have video calls with AI live captions, but contracts still travel as PDFs that bounce between inboxes.&lt;/p&gt;

&lt;p&gt;For a long time the default answer was: “Just use DocuSign / Adobe Sign / whatever, upload the PDF and let people sign there.” That works. But it also means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;another platform, another login,&lt;/li&gt;
&lt;li&gt;extra subscriptions just for signatures,&lt;/li&gt;
&lt;li&gt;and documents leaving your Microsoft 365 environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since late 2025, there’s another option that a lot of people seem to miss: &lt;strong&gt;Microsoft 365 eSignature&lt;/strong&gt;. It lets you request and collect electronic signatures directly in M365 – and it can even sit on top of existing providers like Adobe Sign and DocuSign.&lt;/p&gt;

&lt;p&gt;In this post I’ll walk through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what M365 eSignature actually is,&lt;/li&gt;
&lt;li&gt;what you need to use it, and where the limits are,&lt;/li&gt;
&lt;li&gt;a simple, practical signing flow with Power Automate,&lt;/li&gt;
&lt;li&gt;and how I’d think about security and identity when you modernise your signing process.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Microsoft 365 eSignature actually is
&lt;/h2&gt;

&lt;p&gt;eSignature is a feature in the SharePoint Premium / Microsoft 365 ecosystem that lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request signatures,&lt;/li&gt;
&lt;li&gt;sign documents,&lt;/li&gt;
&lt;li&gt;and store the signed versions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;directly inside&lt;/em&gt; Microsoft 365:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SharePoint document libraries,&lt;/li&gt;
&lt;li&gt;OneDrive,&lt;/li&gt;
&lt;li&gt;and the desktop/web versions of Word (Enterprise).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You shouldn’t have to upload your contract to a third-party website just to get a legally valid electronic signature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On top of that, if your company already uses &lt;strong&gt;Adobe Acrobat Sign&lt;/strong&gt; or &lt;strong&gt;DocuSign&lt;/strong&gt; , you don’t have to throw them away. M365 eSignature can use them as providers under the hood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;users stay in the M365 UI,&lt;/li&gt;
&lt;li&gt;your existing eSignature provider still handles the actual signing workflow and compliance,&lt;/li&gt;
&lt;li&gt;and you avoid a tool zoo from the end-user perspective.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Requirements and the guest-account catch
&lt;/h2&gt;

&lt;p&gt;The basic requirements to use eSignature are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an &lt;strong&gt;Azure subscription with billing enabled&lt;/strong&gt; (pay-as-you-go for document processing),&lt;/li&gt;
&lt;li&gt;Enterprise Office apps if you want to start signing directly from Word.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pricing is usage-based; you pay per document/signature transaction. Microsoft has a dedicated pricing page for that on Learn.&lt;/p&gt;

&lt;p&gt;There’s one important catch that the docs sometimes mention only briefly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;People who sign via M365 eSignature need to be &lt;strong&gt;users or guests&lt;/strong&gt; in your tenant.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internal staff: no issue, they’re already in Azure AD / Entra ID.&lt;/li&gt;
&lt;li&gt;External signers: need a guest account (B2B) to access the document and sign it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For some organisations that’s totally fine – they already collaborate with partners via guests in Teams/SharePoint. For others, guest access is tightly locked down or historically disabled, so this becomes an organisational and security conversation before it becomes a technical one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple signing flow in M365
&lt;/h2&gt;

&lt;p&gt;Let’s look at a practical scenario:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We have a contract as a Word document in SharePoint. We want it signed by one or more people. Once it’s signed, it should live in a ‘Signed Contracts’ library and relevant people should be notified.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On a high level, the flow looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbnqneam2tomcffzo9ps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbnqneam2tomcffzo9ps.png" alt="Electronic Signatures in Microsoft 365: Using eSignature Without Leaving Your Tenant" width="800" height="41"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: start directly from Word / SharePoint
&lt;/h3&gt;

&lt;p&gt;This is the simplest option:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author saves the document in a specific SharePoint library.&lt;/li&gt;
&lt;li&gt;In the UI, they choose something like “Request eSignature”.&lt;/li&gt;
&lt;li&gt;They select signers (internal users and/or guests).&lt;/li&gt;
&lt;li&gt;M365 eSignature sends out the sign requests and tracks completion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final signed document ends up back in the same place (or a configured destination), with an audit trail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: embed it into a Power Automate process
&lt;/h3&gt;

&lt;p&gt;If you want to treat signing as just one step in a longer business process, Power Automate is the natural place to glue things together.&lt;/p&gt;

&lt;p&gt;Very simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trigger: When a file is created or modified in Library "Contracts"
Action: Get file metadata
Action: Start eSignature request (document = file, signers = metadata.Signers)
Action: Wait for eSignature status = Completed
Action: Copy signed document to Library "Signed Contracts"
Action: Post message in Teams channel "Legal" with link to signed file

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on your license and the eSignature provider you use (native M365 vs. Adobe/DocuSign connector), the exact actions differ, but the pattern stays the same: document in, signature request out, signed document back in, plus notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity and security: who may see and sign what?
&lt;/h2&gt;

&lt;p&gt;As soon as signatures are involved, identity and security move to the front of the stage.&lt;/p&gt;

&lt;p&gt;A few principles I keep in mind:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Signatures are only as strong as your identity
&lt;/h3&gt;

&lt;p&gt;The value of an electronic signature depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how strong the identity verification is,&lt;/li&gt;
&lt;li&gt;how good your audit trail is (who signed what, when, from where),&lt;/li&gt;
&lt;li&gt;and whether you can show that the document didn’t change after signing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using M365 identities – including guests – is not a workaround, it’s part of that chain of trust. If you’re serious about compliance, you want signers to be tied to proper identities, not anonymous links.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. “Can see” and “can sign” are different roles
&lt;/h3&gt;

&lt;p&gt;Someone being allowed to view a file in SharePoint doesn’t automatically mean they should be allowed to sign it.&lt;/p&gt;

&lt;p&gt;In practice it helps to distinguish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;readers (can view the document),&lt;/li&gt;
&lt;li&gt;approvers (can give internal approval, e.g. via Power Automate approvals),&lt;/li&gt;
&lt;li&gt;signers (can legally sign the document).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you design your libraries and flows, it’s worth taking five minutes to map these roles to groups/permissions instead of letting “whoever can open the file” also be in the signer list.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Don’t build God-mode Flows
&lt;/h3&gt;

&lt;p&gt;There’s a similar anti-pattern here as with Copilot connectors: using a single service account that can see and sign everything.&lt;/p&gt;

&lt;p&gt;Where possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use dedicated service identities with scoped permissions (e.g. only certain libraries),&lt;/li&gt;
&lt;li&gt;avoid giving Flows blanket rights to every contract library in the organisation,&lt;/li&gt;
&lt;li&gt;log who requested a signature for which document and which signers were added.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s very easy to accidentally turn “streamlined signing” into “anyone with access to the Flow can make anyone sign anything”. You want to avoid that.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this fits with external providers like Adobe/DocuSign
&lt;/h2&gt;

&lt;p&gt;In a lot of organisations, Adobe Acrobat Sign or DocuSign are already in place for critical contracts, with legal and procurement being comfortable with their processes.&lt;/p&gt;

&lt;p&gt;In that case, I wouldn’t see M365 eSignature as a replacement so much as a new front door:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;make the user experience more consistent (requests from SharePoint/Word instead of from a separate portal),&lt;/li&gt;
&lt;li&gt;keep signed docs in your existing SharePoint structures,&lt;/li&gt;
&lt;li&gt;and let Adobe/DocuSign continue doing what they’re already doing well in the background.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For “lighter” internal documents, you might decide the native M365 eSignature path is enough. For high-stakes, high-risk contracts, you keep the dedicated provider and just surface it through M365.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick note on legal aspects
&lt;/h2&gt;

&lt;p&gt;I’m not a lawyer, so this is not legal advice. But roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Electronic signatures are recognised in many jurisdictions, with different levels (simple, advanced, qualified).&lt;/li&gt;
&lt;li&gt;Which level you need depends on the type of document and local regulations.&lt;/li&gt;
&lt;li&gt;Large providers (including Microsoft + integrated partners) typically have detailed guidance on which of their options meet which legal standards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical takeaway for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;don’t over-engineer simple internal approvals – eSignature is often good enough there,&lt;/li&gt;
&lt;li&gt;for critical, regulated use cases, involve Legal and stay on the supported path of your chosen provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My take: where M365 eSignature makes sense
&lt;/h2&gt;

&lt;p&gt;For me, Microsoft 365 eSignature is most interesting in three situations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Internal agreements and routine contracts&lt;/strong&gt;
Stuff like NDAs, internal approvals, standard vendor contracts, where the main pain is the ping-pong of PDFs and email.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teams that already live in M365&lt;/strong&gt;
If your users spend most of their day in Teams, SharePoint and Outlook, keeping the signing flow in that world removes friction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organisations with a mix of needs&lt;/strong&gt;
You can use native eSignature for “lightweight” cases and still integrate your heavyweight provider for the contracts that really matter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The main thing I’d keep in mind is the same theme that came up in my Copilot posts: identity and security are not an afterthought.&lt;/p&gt;

&lt;p&gt;If you take the time to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clean up who is allowed to see and sign what,&lt;/li&gt;
&lt;li&gt;design your libraries and Flows with clear roles,&lt;/li&gt;
&lt;li&gt;and avoid god-mode service accounts,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then M365 eSignature can turn a surprisingly stubborn part of your process – “print, sign, scan” – into something that finally fits the rest of your digital workspace.&lt;/p&gt;

</description>
      <category>m365</category>
      <category>esignature</category>
      <category>sharepoint</category>
      <category>powerautomate</category>
    </item>
    <item>
      <title>Security Trimming with Microsoft 365 Copilot: Asking the Right Data in the Right Context</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Thu, 05 Mar 2026 06:10:47 +0000</pubDate>
      <link>https://dev.to/saschadev/security-trimming-with-microsoft-365-copilot-asking-the-right-data-in-the-right-context-1nlg</link>
      <guid>https://dev.to/saschadev/security-trimming-with-microsoft-365-copilot-asking-the-right-data-in-the-right-context-1nlg</guid>
      <description>&lt;p&gt;The more I work with Microsoft 365 Copilot, the less I think about prompts – and the more I think about permissions.&lt;/p&gt;
&lt;p&gt;It’s one thing to have an AI that can summarise public docs. It’s a very different thing to plug it into your company’s real data and let people ask whatever they want.&lt;/p&gt;
&lt;p&gt;At that point, the question isn’t “Can Copilot answer this?” but:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;“Should Copilot answer this – for this user – right now?”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In this post I want to talk about &lt;strong&gt;security trimming&lt;/strong&gt; in the context of M365 Copilot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what it actually means,&lt;/li&gt;
&lt;li&gt;where people are currently cutting corners,&lt;/li&gt;
&lt;li&gt;and how I’d implement a practical, generic pattern when you connect external systems like Confluence or custom APIs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ll keep it concrete and code-backed, not just policy talk.&lt;/p&gt;
&lt;h2 id="1-why-security-trimming-is-not-optional-with-copilot"&gt;1. Why security trimming is not optional with Copilot&lt;/h2&gt;
&lt;p&gt;When people roll out Copilot in a hurry, the conversation often sounds like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Let’s connect it to everything, then we see what happens.”&lt;/li&gt;
&lt;li&gt;“It’s just summarising documents, how bad can it be?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pretty bad.&lt;/p&gt;
&lt;p&gt;Most organisations have data that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;only a tiny group should see, or&lt;/li&gt;
&lt;li&gt;should never be mashed together by a generic assistant.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CEO docs: M&amp;amp;A plans, board minutes, confidential strategy decks, early product plans.&lt;/li&gt;
&lt;li&gt;HR data: future layoff lists, performance reviews, salary and bonus details.&lt;/li&gt;
&lt;li&gt;Legal and compliance content: investigations, sensitive contracts, whistleblower reports.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your Copilot integration ignores permissions, all of that becomes a “nice” answer to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“What are our upcoming layoffs?”&lt;/li&gt;
&lt;li&gt;“How much does Person X make?”&lt;/li&gt;
&lt;li&gt;“What legal issues are we currently dealing with?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s not a bug, that’s a breach with extra steps.&lt;/p&gt;
&lt;p&gt;So let’s be clear: &lt;strong&gt;security trimming is not a nice-to-have in Copilot land, it’s the core of whether you can safely use it on real data at all.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="2-what-%E2%80%9Csecurity-trimming%E2%80%9D-actually-means-here"&gt;2. What “security trimming” actually means here&lt;/h2&gt;
&lt;p&gt;In this context, security trimming means:&lt;/p&gt;
&lt;blockquote&gt;Every piece of data used to answer a Copilot question must be filtered according to the &lt;strong&gt;current user’s identity and permissions&lt;/strong&gt;.&lt;/blockquote&gt;
&lt;p&gt;There are two layers to think about:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inside the Microsoft 365 world&lt;/strong&gt;When you use Microsoft Graph (SharePoint, OneDrive, Exchange), the platform already understands ACLs and user tokens. If you do it right, security trimming comes “for free”.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outside Microsoft 365&lt;/strong&gt;When you connect external systems like Confluence, Jira, SAP, databases, you are back in DIY land. You need to bring your own security trimming.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Most of the scary stories I see are from the second category.&lt;/p&gt;
&lt;h2 id="3-how-copilot-graph-handle-permissions-by-default"&gt;3. How Copilot + Graph handle permissions by default&lt;/h2&gt;
&lt;p&gt;On the M365 side, the picture looks roughly like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  U[User] --&amp;gt; C[Copilot]
  C --&amp;gt; P[Plugin / Agent]
  P --&amp;gt; G[Microsoft Graph]
  G --&amp;gt; D[(M365 Data)]

  subgraph M365
    G
    D
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When your plugin/agent calls Graph with a &lt;strong&gt;delegated user token&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Graph checks what the user is allowed to see (SharePoint, OneDrive, Teams, mailboxes, etc.).&lt;/li&gt;
&lt;li&gt;You get back only the documents, messages and items that user has access to.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Security trimming happens at the &lt;strong&gt;Graph level&lt;/strong&gt;. That’s good. You still need to be careful which scopes you request, but you’re not manually filtering file lists in your own code.&lt;/p&gt;
&lt;p&gt;The problem starts when people think they get the same behaviour automatically for everything else.&lt;/p&gt;
&lt;h2 id="4-the-dangerous-gap-external-systems-without-trimming"&gt;4. The dangerous gap: external systems without trimming&lt;/h2&gt;
&lt;p&gt;As soon as you build your own HTTP connector or call third-party APIs directly from an agent, you can easily sidestep all that nice trimming.&lt;/p&gt;
&lt;p&gt;Typical anti-pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your Copilot plugin talks to your own backend.&lt;/li&gt;
&lt;li&gt;Your backend calls Confluence/Jira/SQL with a&lt;a&gt; service account&lt;/a&gt; that can see everything.&lt;/li&gt;
&lt;li&gt;You return whatever is found, regardless of who asked.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now imagine a user who normally has no access to CEO documents asking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Show me all upcoming restructuring plans.”&lt;/li&gt;
&lt;li&gt;“What salary bands do we have for senior engineers?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your backend happily queries everything with a technical account and returns it to Copilot, you just built a very friendly internal data exfiltration tool.&lt;/p&gt;
&lt;p&gt;The key rule is:&lt;/p&gt;
&lt;blockquote&gt;Agents and connectors must treat &lt;strong&gt;user identity&lt;/strong&gt; as a first-class input and enforce access checks before they ever touch external data.&lt;/blockquote&gt;
&lt;h2 id="5-a-practical-pattern-passing-user-context-and-enforcing-acls"&gt;5. A practical pattern: passing user context and enforcing ACLs&lt;/h2&gt;
&lt;p&gt;Let’s look at a simple pattern I like to use for external systems.&lt;/p&gt;
&lt;p&gt;High-level flow:&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.bajonczak.com%2Fcontent%2Fimages%2F2026%2F03%2Fimage-3.png" alt="" width="800" height="146"&gt;&lt;p&gt;Here the Mermaid code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  U[User] --&amp;gt; C[Copilot]
  C --&amp;gt; A[AskExternalData / Agent]
  A --&amp;gt; B[Backend]
  B --&amp;gt; AUTH[Auth / Directory]
  B --&amp;gt; EXT[External System]
  EXT --&amp;gt; B
  B --&amp;gt; C
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Important pieces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copilot passes user context&lt;/strong&gt; to your backend (at least an email or user ID).&lt;/li&gt;
&lt;li&gt;Your backend calls an &lt;strong&gt;auth/directory service&lt;/strong&gt; to resolve permissions (groups, roles, allowed projects).&lt;/li&gt;
&lt;li&gt;Your backend only queries the external system for what the user is allowed to see, or filters results accordingly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s make that concrete.&lt;/p&gt;
&lt;h3 id="51-api-contract"&gt;5.1. API contract&lt;/h3&gt;
&lt;p&gt;Define an API where the agent sends the question plus user context:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Request payload from Copilot plugin
interface AskExternalRequest {
  question: string;
  userEmail: string;
  projectKey?: string;
}

interface AskExternalResponse {
  answer: string;
  sources: { title: string; url: string }[];
  missingInfo: boolean;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="52-express-handler-with-security-trimming-hook"&gt;5.2. Express handler with security trimming hook&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/secureAsk.ts
import { Request, Response } from 'express';
import { getUserPermissions } from './permissions';
import { searchExternalDocs } from './externalDocs';
import { buildAnswerFromDocs } from './rag';

export async function secureAskHandler(req: Request, res: Response) {
  const body = req.body as AskExternalRequest;

  if (!body.question || !body.userEmail) {
    return res.status(400).json({ error: 'question and userEmail are required' });
  }

  try {
    // 1) Resolve user permissions from your directory/IAM
    const userPerms = await getUserPermissions(body.userEmail);
    if (!userPerms) {
      return res.status(403).json({ error: 'unknown_user' });
    }

    // 2) Search external docs WITH permissions
    const docs = await searchExternalDocs(body.question, userPerms, body.projectKey);

    if (docs.length === 0) {
      const answer = `I couldn't find any documents you have access to that answer "${body.question}".`;
      const response: AskExternalResponse = {
        answer,
        sources: [],
        missingInfo: true
      };
      return res.json(response);
    }

    // 3) Build an answer using an LLM
    const { answer, usedDocs } = await buildAnswerFromDocs(body.question, docs);

    const response: AskExternalResponse = {
      answer,
      sources: usedDocs.map(d =&amp;gt; ({ title: d.title, url: d.url })),
      missingInfo: false
    };

    return res.json(response);
  } catch (err) {
    console.error('secureAsk error', err);
    return res.status(500).json({ error: 'internal_error' });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="53-a-simple-permission-model"&gt;5.3. A simple permission model&lt;/h3&gt;
&lt;p&gt;You can model permissions in many ways. Here’s a very simple example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/permissions.ts
export type UserContext = {
  email: string;
  roles: string[];   // e.g. ['EMPLOYEE', 'HR', 'ENGINEERING_MANAGER']
  groups: string[];  // e.g. ['project-phoenix', 'dept-engineering']
};

// In reality this might talk to Azure AD, Okta, your own directory, ...
export async function getUserPermissions(email: string): Promise&amp;lt;UserContext | null&amp;gt; {
  // Placeholder: load from your IAM
  const entry = await directoryLookup(email);
  if (!entry) return null;

  return {
    email,
    roles: entry.roles,
    groups: entry.groups
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="6-implementing-generic-security-trimming-for-external-content"&gt;6. Implementing generic security trimming for external content&lt;/h2&gt;
&lt;p&gt;For external documents (Confluence pages, database rows, etc.) you need a way to represent who is allowed to see what.&lt;/p&gt;
&lt;p&gt;A very generic model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export type DocumentAcl = {
  allowedRoles?: string[];  // e.g. ['HR', 'CEO']
  allowedGroups?: string[]; // e.g. ['project-phoenix']
};

export type ExternalDoc = {
  id: string;
  title: string;
  url: string;
  content: string;
  acl: DocumentAcl;
};

function hasAccess(doc: ExternalDoc, user: UserContext): boolean {
  const { allowedRoles, allowedGroups } = doc.acl;

  if (!allowedRoles &amp;amp;&amp;amp; !allowedGroups) {
    // default: visible to all employees
    return true;
  }

  if (allowedRoles &amp;amp;&amp;amp; allowedRoles.some(r =&amp;gt; user.roles.includes(r))) {
    return true;
  }

  if (allowedGroups &amp;amp;&amp;amp; allowedGroups.some(g =&amp;gt; user.groups.includes(g))) {
    return true;
  }

  return false;
}

export function filterDocsByAcl(docs: ExternalDoc[], user: UserContext): ExternalDoc[] {
  return docs.filter(doc =&amp;gt; hasAccess(doc, user));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then in your search function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/externalDocs.ts
import { ExternalDoc, filterDocsByAcl } from './acl';
import { UserContext } from './permissions';

export async function searchExternalDocs(query: string, user: UserContext, projectKey?: string): Promise&amp;lt;ExternalDoc[]&amp;gt; {
  // 1) Query the external system (e.g. Confluence, SQL, custom API)
  const rawDocs = await rawSearchDocs(query, projectKey);

  // 2) Apply ACL-based filtering
  const visibleDocs = filterDocsByAcl(rawDocs, user);

  return visibleDocs;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;High-level, the flow then looks like this:&lt;br&gt;&lt;/p&gt;
&lt;a href="https://mermaid.live/edit?ref=blog.bajonczak.com#pako:eNpNkdFugjAUhl-lOddoEKkIF0uQYbJEE6MuWVa8aOAIZNCaUjKd8d1XSuJ213zf37-n7R1yWSBEcG7kd15xpclmnwlC3tl7h-pEJpMXkrBEXupG6tNgEstidsC8V0jiEsUoYitWbMXzLxSFZSvLdul-y0rUQ-UOVVt3XS1F9y-RfhxZetWoBG_IAbnKK2sNH1v_ouu3zTHdszjZkHXdmC02ONJx3EyAA605h9eFudp98BnoClvMIDLLAs-8b3QGmXiYKO-1PNxEDtGZNx060F8KrvG15qXi7ZNeuIDoDleIJkEwDRZhSIOZ61Mazh24Gep703A5mwfLxdJ1fc-li4cDP1KaCndK_dCn8yAMXONpSG3fp5Va9aYei1pLtR2_w_6KA0r2ZfUcoFTDfca0Mi-MKpG90BDNPO_xC-R5i1Q" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.bajonczak.com%2Fcontent%2Fimages%2F2026%2F03%2Fimage-4.png" alt="" width="800" height="246"&gt;&lt;/a&gt;&lt;h2 id="7-concrete-example-secure-confluence-connector"&gt;7. Concrete example: secure Confluence connector&lt;/h2&gt;
&lt;p&gt;Let’s connect this back to Confluence, similar to my previous “Ask the Company” post.&lt;/p&gt;
&lt;p&gt;Instead of querying Confluence with a technical account and dumping all results into an LLM, we:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;resolve the user’s allowed spaces or labels,&lt;/li&gt;
&lt;li&gt;limit the Confluence search to those spaces, or&lt;/li&gt;
&lt;li&gt;filter pages after retrieval using ACL metadata.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In a simple setup, you might maintain a mapping like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/projectSpaces.ts
const projectSpaceMap: Record&amp;lt;string, string&amp;gt; = {
  'project-phoenix': 'PHX',
  'project-orion': 'ORI'
};

export function getAllowedSpacesForUser(user: UserContext): string[] {
  const spaces = new Set&amp;lt;string&amp;gt;();

  for (const group of user.groups) {
    const spaceKey = projectSpaceMap[group];
    if (spaceKey) spaces.add(spaceKey);
  }

  // Add global "company" space for all employees if you want
  spaces.add('COMPANY');

  return Array.from(spaces);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then use it in your Confluence search:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { getAllowedSpacesForUser, UserContext } from './permissions';

export async function searchConfluenceSecure(query: string, user: UserContext): Promise&amp;lt;ConfluencePage[]&amp;gt; {
  const spaces = getAllowedSpacesForUser(user);

  const cqlParts = [`text ~ "${query.replace(/"/g, '\\"')}"`];
  if (spaces.length &amp;gt; 0) {
    const spaceFilter = spaces.map(s =&amp;gt; `space = "${s}"`).join(' OR ');
    cqlParts.push(`(${spaceFilter})`);
  }

  const cql = cqlParts.join(' AND ');

  // ... same as before, but using the restricted CQL
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the agent will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;only search spaces the user is mapped to,&lt;/li&gt;
&lt;li&gt;and won’t “accidentally” surface CEO or HR spaces that live elsewhere.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="8-sensitive-data-you-probably-shouldn%E2%80%99t-pipe-through"&gt;8. Sensitive data you probably shouldn’t pipe through&lt;/h2&gt;
&lt;p&gt;Even with good trimming, there are categories of data I’d think twice about running through a general Copilot agent at all:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;individual salary and compensation details,&lt;/li&gt;
&lt;li&gt;future layoff or restructuring plans,&lt;/li&gt;
&lt;li&gt;logs and reports from ongoing investigations,&lt;/li&gt;
&lt;li&gt;deeply personal HR/health information.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A few strategies that help:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicitly exclude&lt;/strong&gt; certain systems or folders from general-purpose agents.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;separate, highly scoped agents&lt;/strong&gt; for HR or legal, only available to specific roles.&lt;/li&gt;
&lt;li&gt;Be careful with &lt;strong&gt;training data&lt;/strong&gt; if you use logs to improve models – keep sensitive content out of those loops.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Security trimming is not only about “who can read the file”. It’s also about “which AI flows are allowed to touch it and combine it with other information”.&lt;/p&gt;
&lt;h2 id="9-developer-checklist-before-you-connect-something-to-copilot"&gt;9. Developer checklist before you connect something to Copilot&lt;/h2&gt;
&lt;p&gt;If I had to put this into a blunt checklist, it would look like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can I reliably identify the user in my backend (email, ID, token)?&lt;/li&gt;
&lt;li&gt;Do I use delegated permissions where possible, instead of a god‑mode service account?&lt;/li&gt;
&lt;li&gt;Where are permissions for this data actually defined (AD, custom DB, app), and do I check them?&lt;/li&gt;
&lt;li&gt;Are there data classes I should exclude entirely from generic agents?&lt;/li&gt;
&lt;li&gt;Do I log which agent delivered which data to which user, for auditing?&lt;/li&gt;
&lt;li&gt;Is there a simple way to disable or limit an agent quickly if something goes wrong?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="10-closing-thoughts-copilot-is-only-as-safe-as-your-connectors"&gt;10. Closing thoughts: Copilot is only as safe as your connectors&lt;/h2&gt;
&lt;p&gt;Microsoft 365 and Graph do a lot of the heavy lifting for you on the M365 side. If you stay within that world and use delegated permissions correctly, security trimming works in your favour.&lt;/p&gt;
&lt;p&gt;The moment you step outside – Confluence, SAP, custom APIs, databases – you’re back in your usual role: architect, not just user.&lt;/p&gt;
&lt;p&gt;For me, the principle is simple:&lt;/p&gt;
&lt;blockquote&gt;If I wouldn’t give a human service account read access to all of this without restrictions, I definitely shouldn’t do it for an AI agent either.&lt;/blockquote&gt;
&lt;p&gt;Copilot is powerful, but it doesn’t magically know what your company considers sensitive. That’s still your job. Security trimming is the line between “useful internal assistant” and “polite data leak on demand”.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>microsoft</category>
      <category>security</category>
    </item>
    <item>
      <title>Building an 'Ask the Company' Agent in Microsoft 365 Copilot</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Mon, 02 Mar 2026 12:00:18 +0000</pubDate>
      <link>https://dev.to/saschadev/building-an-ask-the-company-agent-in-microsoft-365-copilot-33n6</link>
      <guid>https://dev.to/saschadev/building-an-ask-the-company-agent-in-microsoft-365-copilot-33n6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtw06hy3yf7abglyobnf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtw06hy3yf7abglyobnf.png" alt="Building an 'Ask the Company' Agent in Microsoft 365 Copilot" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’ve been playing more and more with Microsoft 365 Copilot lately, and one pattern keeps coming back:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The interesting part is not “answer my email”, it’s “answer questions about how our company works”.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every organisation has the same type of question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“How do we do this here?”&lt;/li&gt;
&lt;li&gt;“Where is that documented?”&lt;/li&gt;
&lt;li&gt;“Have we done this before?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answers usually live somewhere in Confluence, SharePoint or Teams documents. But most of that knowledge is locked behind search, folder structures and tribal memory.&lt;/p&gt;

&lt;p&gt;In this post I want to sketch how I’d build an &lt;strong&gt;“Ask the Company” agent in Microsoft 365 Copilot&lt;/strong&gt; that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;answers questions about a project,&lt;/li&gt;
&lt;li&gt;pulls information from Confluence,&lt;/li&gt;
&lt;li&gt;and notices when something is missing – so it can help you document it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ll go through three pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the architecture idea,&lt;/li&gt;
&lt;li&gt;a simple backend with sample code (Node/TypeScript),&lt;/li&gt;
&lt;li&gt;and how Copilot can use it as an agent.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. The idea: “Ask the Company” inside M365
&lt;/h2&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In M365 Copilot you expose a new ability: &lt;strong&gt;Ask the Company&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A user types: “How do we handle deployments for Project Phoenix?”&lt;/li&gt;
&lt;li&gt;Copilot calls your own backend (“CompanyAgent API”) with the question plus user/project context.&lt;/li&gt;
&lt;li&gt;The backend:&lt;/li&gt;
&lt;li&gt;searches Confluence for relevant pages,&lt;/li&gt;
&lt;li&gt;extracts the important passages,&lt;/li&gt;
&lt;li&gt;builds an answer,&lt;/li&gt;
&lt;li&gt;and flags when there is no or not enough documentation.&lt;/li&gt;
&lt;li&gt;If nothing is found, the agent suggests:&lt;/li&gt;
&lt;li&gt;creating a new Confluence page draft for this topic,&lt;/li&gt;
&lt;li&gt;based on what the user just asked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent’s role is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read&lt;/strong&gt; : fetch knowledge from Confluence,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer&lt;/strong&gt; : compose a clear answer for Copilot,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learn&lt;/strong&gt; : detect gaps and help to close them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. The backend: a simple Node/TypeScript example
&lt;/h2&gt;

&lt;p&gt;Let’s build a minimal backend that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exposes a &lt;code&gt;/ask&lt;/code&gt; endpoint,&lt;/li&gt;
&lt;li&gt;searches Confluence via REST,&lt;/li&gt;
&lt;li&gt;and returns an answer plus a &lt;code&gt;missingInfo&lt;/code&gt; flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.1 Basic Express setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/server.ts
import express from 'express';
import bodyParser from 'body-parser';
import { searchConfluence, getConfluencePageExcerpt } from './confluence';
import { buildAnswerFromPages } from './rag';

const app = express();
app.use(bodyParser.json());

type AskRequest = {
  question: string;
  projectKey?: string;
  userEmail?: string;
};

type AskResponse = {
  answer: string;
  sources: { title: string; url: string }[];
  missingInfo: boolean;
};

app.post('/ask', async (req, res) =&amp;gt; {
  const body = req.body as AskRequest;

  if (!body.question) {
    return res.status(400).json({ error: 'question is required' });
  }

  try {
    // 1) Search Confluence
    const pages = await searchConfluence(body.question, body.projectKey);

    if (pages.length === 0) {
      const answer = `I couldn’t find any documentation about "${body.question}" in Confluence. ` +
        `It might make sense to create a page for this topic.`;
      const response: AskResponse = {
        answer,
        sources: [],
        missingInfo: true
      };
      return res.json(response);
    }

    // 2) Fetch excerpts / content
    const excerpts = await Promise.all(
      pages.map(p =&amp;gt; getConfluencePageExcerpt(p.id))
    );

    // 3) Build an answer
    const { answer, usedPages } = await buildAnswerFromPages(body.question, excerpts);

    const response: AskResponse = {
      answer,
      sources: usedPages.map(p =&amp;gt; ({ title: p.title, url: p.url })),
      missingInfo: false
    };

    return res.json(response);
  } catch (err) {
    console.error('Error in /ask', err);
    return res.status(500).json({ error: 'internal_error' });
  }
});

const port = process.env.PORT || 3000;
app.listen(port, () =&amp;gt; {
  console.log(`CompanyAgent API listening on port ${port}`);
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.2 Confluence search and excerpts
&lt;/h3&gt;

&lt;p&gt;Confluence offers a REST API that you can call with CQL (Confluence Query Language). Here’s a simplified helper module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/confluence.ts
import fetch from 'node-fetch';

const CONFLUENCE_BASE_URL = process.env.CONFLUENCE_BASE_URL!;
const CONFLUENCE_USER = process.env.CONFLUENCE_USER!;
const CONFLUENCE_TOKEN = process.env.CONFLUENCE_TOKEN!;

type ConfluencePage = {
  id: string;
  title: string;
  url: string;
};

export async function searchConfluence(query: string, projectKey?: string): Promise&amp;lt;ConfluencePage[]&amp;gt; {
  const cqlParts = [`text ~ "${query.replace(/"/g, '\\"')}"`];
  if (projectKey) {
    // Example: filter by space
    cqlParts.push(`space = "${projectKey}"`);
  }
  const cql = cqlParts.join(' AND ');

  const url = new URL('/rest/api/search', CONFLUENCE_BASE_URL);
  url.searchParams.set('cql', cql);
  url.searchParams.set('limit', '5');

  const res = await fetch(url.toString(), {
    headers: {
      'Authorization': 'Basic ' + Buffer.from(`${CONFLUENCE_USER}:${CONFLUENCE_TOKEN}`).toString('base64'),
      'Accept': 'application/json'
    }
  });

  if (!res.ok) {
    console.error('Confluence search error', await res.text());
    return [];
  }

  const data = await res.json() as any;
  const results = data.results ?? [];

  return results.map((r: any) =&amp;gt; ({
    id: r.content.id,
    title: r.content.title,
    url: `${CONFLUENCE_BASE_URL}/pages/${r.content.id}`
  }));
}

export async function getConfluencePageExcerpt(pageId: string): Promise&amp;lt;{ id: string; title: string; url: string; text: string }&amp;gt; {
  const url = `${CONFLUENCE_BASE_URL}/rest/api/content/${pageId}?expand=body.storage`;

  const res = await fetch(url, {
    headers: {
      'Authorization': 'Basic ' + Buffer.from(`${CONFLUENCE_USER}:${CONFLUENCE_TOKEN}`).toString('base64'),
      'Accept': 'application/json'
    }
  });

  if (!res.ok) {
    console.error('Confluence get page error', await res.text());
    throw new Error('confluence_error');
  }

  const data = await res.json() as any;
  const html = data.body.storage.value as string;
  const text = stripHtml(html).slice(0, 5000);

  return {
    id: data.id,
    title: data.title,
    url: `${CONFLUENCE_BASE_URL}/pages/${data.id}`,
    text
  };
}

function stripHtml(html: string): string {
  return html.replace(/&amp;lt;[^&amp;gt;]+&amp;gt;/g, ' ').replace(/\s+/g, ' ').trim();
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.3 Building an answer with an LLM
&lt;/h3&gt;

&lt;p&gt;Next we need something that takes the found texts and the user’s question and produces an answer. In a real system you’d likely use RAG with vector search and a local or cloud LLM.&lt;/p&gt;

&lt;p&gt;Here’s a minimal example assuming you have a &lt;code&gt;callLLM()&lt;/code&gt; function somewhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/rag.ts
import { callLLM } from './llmClient';

type PageExcerpt = {
  id: string;
  title: string;
  url: string;
  text: string;
};

export async function buildAnswerFromPages(question: string, pages: PageExcerpt[]): Promise&amp;lt;{ answer: string; usedPages: PageExcerpt[] }&amp;gt; {
  const prompt = `
You are an internal documentation assistant. Answer the question based ONLY on the information below.
If the docs don’t contain a clear answer, say so.

Question:
${question}

Docs:
${pages.map((p, i) =&amp;gt; `[#${i + 1}] ${p.title}\n${p.text}`).join('\n\n')}
`;

  const answer = await callLLM(prompt);

  // Simple heuristic: assume all pages were used
  return { answer, usedPages: pages };
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With just these three modules you have a very simple but end‑to‑end working “Ask the Company” backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /ask&lt;/code&gt; receives a question,&lt;/li&gt;
&lt;li&gt;looks up relevant docs in Confluence,&lt;/li&gt;
&lt;li&gt;asks an LLM to summarise them,&lt;/li&gt;
&lt;li&gt;and tells you if nothing was found.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Letting Copilot talk to the agent
&lt;/h2&gt;

&lt;p&gt;For Copilot to use this backend, you need to expose it as a function Copilot can call – for example via Copilot Studio, a Graph connector, or an HTTP plugin-style integration.&lt;/p&gt;

&lt;p&gt;The conceptual model is always the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you describe what your backend can do,&lt;/li&gt;
&lt;li&gt;Copilot decides when to call it, based on the user’s request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified JSON description for an HTTP-style tool could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "name": "companyDocumentation",
  "description": "Answer questions about internal projects by searching Confluence",
  "functions": [
    {
      "name": "askCompany",
      "description": "Get an answer to a company/project-related question from Confluence",
      "parameters": {
        "type": "object",
        "properties": {
          "question": {
            "type": "string",
            "description": "The user's question in plain language"
          },
          "projectKey": {
            "type": "string",
            "description": "Optional project/space key used to narrow the Confluence search"
          }
        },
        "required": ["question"]
      }
    }
  ]
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From Copilot’s perspective, a conversation might look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User: “How do we handle hotfix deployments for Project Phoenix?”&lt;/li&gt;
&lt;li&gt;Copilot:&lt;/li&gt;
&lt;li&gt;recognises that this is an internal process question,&lt;/li&gt;
&lt;li&gt;calls &lt;code&gt;askCompany&lt;/code&gt; with &lt;code&gt;question&lt;/code&gt; + &lt;code&gt;projectKey = "PHOENIX"&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;receives &lt;code&gt;{ answer, sources, missingInfo }&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;and builds a response such as:&lt;/li&gt;
&lt;li&gt;“According to our docs: …” plus links to the sources,&lt;/li&gt;
&lt;li&gt;or: “I couldn’t find any documentation; we should probably create some.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;missingInfo&lt;/code&gt; is true, you can add a second function like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;createConfluencePageDraft(title, content)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copilot could then propose a draft documentation page based on the question and what is known so far, so that someone can refine and publish it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What’s missing in a real implementation?
&lt;/h2&gt;

&lt;p&gt;The prototype above deliberately skips a few hard but important topics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication and permissions.&lt;/strong&gt;
The agent should act on behalf of the signed‑in user, respect Confluence permissions, and make sure it doesn’t surface pages the user shouldn’t see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disambiguation.&lt;/strong&gt;
If the search returns multiple topics (“deployments for App A vs App B”), the agent should ask follow‑up questions instead of guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback into documentation.&lt;/strong&gt;
You could log which questions are asked most often and whether there’s good documentation for them – great input for documentation sprints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But as a starting point, the pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copilot → HTTP agent → Confluence + LLM → answer + missingInfo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;is already surprisingly powerful.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Why this is where Copilot gets interesting
&lt;/h2&gt;

&lt;p&gt;The part of Microsoft 365 Copilot that excites me is not that it can summarise emails or rephrase text – that’s useful, but generic.&lt;/p&gt;

&lt;p&gt;It gets interesting when Copilot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;has access to &lt;strong&gt;your systems&lt;/strong&gt; (Confluence, Jira, SharePoint, line-of-business APIs),&lt;/li&gt;
&lt;li&gt;can talk to &lt;strong&gt;your agents&lt;/strong&gt; ,&lt;/li&gt;
&lt;li&gt;and can answer questions like “How do &lt;em&gt;we&lt;/em&gt; do X?” instead of just “What does the internet say about X?”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The “Ask the Company” agent here is a sketch, but it shows the basic idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI reads what you’ve already documented,&lt;/li&gt;
&lt;li&gt;answers questions from that,&lt;/li&gt;
&lt;li&gt;and highlights where your documentation has gaps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the kind of pattern I expect to become normal in M365 Copilot over the next years – and the kind of thing I’d rather experiment with now than wait until someone else ships it for me.&lt;/p&gt;

</description>
      <category>m365copilot</category>
      <category>agents</category>
      <category>confluence</category>
      <category>integration</category>
    </item>
    <item>
      <title>Amazfit Bip 6 vs Garmin Venu 3: Which Long-Battery Smartwatch Makes More Sense?</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Sun, 01 Mar 2026 07:34:13 +0000</pubDate>
      <link>https://dev.to/saschadev/amazfit-bip-6-vs-garmin-venu-3-which-long-battery-smartwatch-makes-more-sense-39d0</link>
      <guid>https://dev.to/saschadev/amazfit-bip-6-vs-garmin-venu-3-which-long-battery-smartwatch-makes-more-sense-39d0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkslxyqydhoaktw71yrt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkslxyqydhoaktw71yrt.png" alt="Amazfit Bip 6 vs Garmin Venu 3: Which Long-Battery Smartwatch Makes More Sense?" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Battery life is the reason I keep coming back to the Amazfit Bip 6. Most smartwatches want more attention than I do – daily charging, constant nudging, lots of features I don’t need. The Bip 6 is one of the first that feels like a normal watch again: you put it on and mostly forget about it.&lt;/p&gt;

&lt;p&gt;To put that into context, I wanted to compare it to a very popular Garmin in a similar “everyday + fitness” category: the &lt;strong&gt;Garmin Venu 3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is a short, practical comparison – not a lab review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick specs (high level)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://amzn.to/4r9kHt1?ref=blog.bajonczak.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Amazfit Bip 6&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Focus: budget-friendly smartwatch with health tracking&lt;/li&gt;
&lt;li&gt;Battery: roughly 7–14 days depending on usage&lt;/li&gt;
&lt;li&gt;Display: bright colour screen, simple UI&lt;/li&gt;
&lt;li&gt;Tracking: steps, heart rate, sleep, basic sports, GPS&lt;/li&gt;
&lt;li&gt;Price: significantly cheaper than most Garmin models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjqg5olip5h2x5hqzper.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjqg5olip5h2x5hqzper.png" alt="Amazfit Bip 6 vs Garmin Venu 3: Which Long-Battery Smartwatch Makes More Sense?" width="679" height="729"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://amzn.to/4tZRKlI?ref=blog.bajonczak.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Garmin Venu 3&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Focus: health and fitness watch with smartwatch features&lt;/li&gt;
&lt;li&gt;Battery: up to around 14 days in smartwatch mode, ~26 hours GPS (in reviews)&lt;/li&gt;
&lt;li&gt;Display: AMOLED, very good visibility&lt;/li&gt;
&lt;li&gt;Tracking: extensive sports profiles, advanced health metrics, strong ecosystem&lt;/li&gt;
&lt;li&gt;Price: clearly higher, closer to premium segment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3l95vf8aozbsw8u971u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3l95vf8aozbsw8u971u.png" alt="Amazfit Bip 6 vs Garmin Venu 3: Which Long-Battery Smartwatch Makes More Sense?" width="800" height="998"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Battery life: both are in the "don’t think about it every day" club
&lt;/h2&gt;

&lt;p&gt;The Bip 6 is impressive because it delivers about a week of battery life without trying too hard. If you’re not hammering GPS every day, it can push towards two weeks. That means you charge it when you notice it’s low – not as a daily ritual.&lt;/p&gt;

&lt;p&gt;The Venu 3, like most modern Garmins, also does very well here. Reviews talk about roughly 14 days in normal smartwatch mode and plenty of GPS time for runs and rides. It’s easy to cover 1–2 weeks on a charge if you’re not doing ultra‑distance training every day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; both watches are in a different league from devices that barely last two days. If you care about battery, you’re safe with either – with the Bip 6 giving you that feeling at a much lower price.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everyday use: simple companion vs. rich ecosystem
&lt;/h2&gt;

&lt;p&gt;The way I see it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bip 6&lt;/strong&gt; is the “simple companion”: steps, sleep, heart rate, basic workouts, notifications. Easy enough to recommend to non‑techy people without turning into their support hotline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Venu 3&lt;/strong&gt; is the “serious health tracker”: deeper metrics, better training analysis, strong Garmin Connect ecosystem, more detail for runners, cyclists and people who like to look at graphs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you just want to move more, sleep better and not charge your watch all the time, the Bip 6 hits a great sweet spot. If you’re training with intent and already live in Garmin land, the Venu 3 will feel more like a proper tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who I’d recommend each watch to
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Amazfit Bip 6&lt;/strong&gt; is ideal for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;people who want a low‑maintenance watch that lasts a long time,&lt;/li&gt;
&lt;li&gt;casual users who care about steps, sleep and occasional workouts,&lt;/li&gt;
&lt;li&gt;family and friends where you don’t want to introduce a lot of complexity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Garmin Venu 3&lt;/strong&gt; is ideal for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;people who train regularly and want deeper health and performance data,&lt;/li&gt;
&lt;li&gt;anyone already using Garmin devices (bike computer, HR strap, etc.),&lt;/li&gt;
&lt;li&gt;users who are willing to pay more for better analytics and a mature ecosystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My personal bias
&lt;/h2&gt;

&lt;p&gt;Personally, I’m very happy with the Bip 6 because it solves my main frustration: battery life and “too much smartwatch”. For my everyday mix of desk work, walking, light workouts and sleep tracking, it’s exactly enough.&lt;/p&gt;

&lt;p&gt;If I ever go back into a focused training phase with clear goals (marathon, structured intervals), I’d look more seriously at a Garmin again – but I’d do it knowing that I’m paying for the extra analysis and ecosystem, not for the battery alone.&lt;/p&gt;

&lt;p&gt;For now, the Bip 6 stays on my wrist most days, and the charger stays in the drawer a lot longer than I’m used to from other watches.&lt;/p&gt;

</description>
      <category>smartwatch</category>
      <category>amazfit</category>
      <category>garmin</category>
      <category>batterylife</category>
    </item>
  </channel>
</rss>
