<?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>Ubiquiti U7-LR review: WiFi 7 with strong range, but no 6 GHz</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Sun, 17 May 2026 17:23:09 +0000</pubDate>
      <link>https://dev.to/saschadev/ubiquiti-u7-lr-review-wifi-7-with-strong-range-but-no-6-ghz-2pmd</link>
      <guid>https://dev.to/saschadev/ubiquiti-u7-lr-review-wifi-7-with-strong-range-but-no-6-ghz-2pmd</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%2Fimages.svc.ui.com%2F%3Fu%3Dhttps%253A%252F%252Fcdn.ecomm.ui.com%252Fproducts%252F7455fa2b-3074-47a0-b82f-a2cd701d4a8f%252F1ade42c7-b70d-4af6-892d-5174c8d7f2d3.png%26q%3D75%26w%3D1080" 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%2Fimages.svc.ui.com%2F%3Fu%3Dhttps%253A%252F%252Fcdn.ecomm.ui.com%252Fproducts%252F7455fa2b-3074-47a0-b82f-a2cd701d4a8f%252F1ade42c7-b70d-4af6-892d-5174c8d7f2d3.png%26q%3D75%26w%3D1080" alt="Ubiquiti U7-LR review: WiFi 7 with strong range, but no 6 GHz" width="1080" height="771"&gt;&lt;/a&gt;&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%2Ftzyb7xedsn3u220ysgkb.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%2Ftzyb7xedsn3u220ysgkb.png" alt="Ubiquiti U7-LR review: WiFi 7 with strong range, but no 6 GHz" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Image: Ubiquiti / official product image&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: This article contains affiliate links. If you buy through one of these links, I may earn a small commission. The price stays the same for you.&lt;/p&gt;

&lt;p&gt;The Ubiquiti U7-LR is one of those access points that looks like an obvious upgrade at first glance. WiFi 7, Long Range, UniFi, 2.5 GbE, PoE. If you already run UniFi at home or in a small office, it sounds like an easy yes.&lt;/p&gt;

&lt;p&gt;I would still not buy it blindly.&lt;/p&gt;

&lt;p&gt;The U7-LR is interesting, but there is one detail you should know before ordering it: it is a WiFi 7 access point without 6 GHz. That does not make it a bad product. It just changes what you should expect from it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://amznto/3PsNXhx?ref=blog.bajonczak.com" rel="noopener noreferrer"&gt;View the Ubiquiti U7-LR on Amazon&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick verdict
&lt;/h2&gt;

&lt;p&gt;The Ubiquiti U7-LR makes the most sense if you already use UniFi and want a modern access point with good range, a 2.5 GbE uplink and PoE. For a house, a larger apartment, a small office, a medical practice or a studio, it can be a very solid choice.&lt;/p&gt;

&lt;p&gt;But if you hear "WiFi 7" and automatically expect 6 GHz, look closer. The U7-LR uses 2.4 GHz and 5 GHz. It does not have a 6 GHz radio. For many setups that is fine. For a full high-end WiFi 7 setup, a tri-band model may be the better fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the U7-LR offers
&lt;/h2&gt;

&lt;p&gt;According to Ubiquiti's official specifications, the U7-LR comes with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Ubiquiti U7-LR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WiFi standard&lt;/td&gt;
&lt;td&gt;WiFi 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frequency bands&lt;/td&gt;
&lt;td&gt;2.4 GHz and 5 GHz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 GHz band&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 GHz data rate&lt;/td&gt;
&lt;td&gt;up to 4.3 Gbps at 160 MHz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.4 GHz data rate&lt;/td&gt;
&lt;td&gt;up to 688 Mbps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spatial streams&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uplink&lt;/td&gt;
&lt;td&gt;1x 2.5 GbE RJ45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power&lt;/td&gt;
&lt;td&gt;PoE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum power consumption&lt;/td&gt;
&lt;td&gt;14 W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stated coverage&lt;/td&gt;
&lt;td&gt;up to 160 m² / 1,750 ft²&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stated client count&lt;/td&gt;
&lt;td&gt;300+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mounting&lt;/td&gt;
&lt;td&gt;Ceiling or wall&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is a strong spec sheet for this class of access point. The 2.5 GbE port matters because a regular gigabit uplink can become a bottleneck on newer access points. The 160 MHz channel width on 5 GHz is also nice to have, assuming your environment allows it.&lt;/p&gt;

&lt;p&gt;And that last part matters. In an apartment building with lots of neighboring networks, 160 MHz is not always the best idea. More channel width can mean more interference. In a detached house or a small office, it may work well. In a crowded area, stability can be more useful than a pretty speed test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main catch: WiFi 7, but no 6 GHz
&lt;/h2&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%2Fimages.svc.ui.com%2F%3Fu%3Dhttps%253A%252F%252Fcdn.ecomm.ui.com%252Fproducts%252F7455fa2b-3074-47a0-b82f-a2cd701d4a8f%252F9d75fc29-da2a-4ddd-adb8-7229958e6461.png%26q%3D75%26w%3D1080" 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%2Fimages.svc.ui.com%2F%3Fu%3Dhttps%253A%252F%252Fcdn.ecomm.ui.com%252Fproducts%252F7455fa2b-3074-47a0-b82f-a2cd701d4a8f%252F9d75fc29-da2a-4ddd-adb8-7229958e6461.png%26q%3D75%26w%3D1080" alt="Ubiquiti U7-LR review: WiFi 7 with strong range, but no 6 GHz" width="1080" height="771"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Image: Ubiquiti / official product image&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the part that can cause confusion. WiFi 7 does not automatically mean that a device supports the 6 GHz band.&lt;/p&gt;

&lt;p&gt;The U7-LR is a dual-band access point. It uses 2.4 GHz and 5 GHz. That can be a sensible choice for range and compatibility, since many devices still live on those bands anyway. But if your main reason for upgrading to WiFi 7 is 6 GHz, this is not the access point you are looking for.&lt;/p&gt;

&lt;p&gt;I would not think of the U7-LR as the ultimate WiFi 7 playground. I would think of it as a modern UniFi access point focused on range, 5 GHz performance and everyday reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should consider the Ubiquiti U7-LR?
&lt;/h2&gt;

&lt;p&gt;The U7-LR is a good fit if you already use UniFi or want to build a UniFi network properly. The appeal is not just the access point itself. It is the UniFi Network platform around it.&lt;/p&gt;

&lt;p&gt;Multiple SSIDs, VLANs, guest networks, roaming, band steering, client isolation and decent visibility into your network are the reasons people buy UniFi in the first place.&lt;/p&gt;

&lt;p&gt;Good use cases include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a house or larger apartment with a centrally mounted access point&lt;/li&gt;
&lt;li&gt;a small office with employees and a guest WiFi network&lt;/li&gt;
&lt;li&gt;a medical practice, agency, studio or shop&lt;/li&gt;
&lt;li&gt;a holiday apartment where you want managed WiFi instead of a random consumer router&lt;/li&gt;
&lt;li&gt;an existing UniFi setup that needs a WiFi 7 upgrade&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these environments, the U7-LR is more interesting than a typical consumer router. Not because it magically makes your internet faster, but because it fits into a cleaner network setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should probably skip it?
&lt;/h2&gt;

&lt;p&gt;If you are looking for a router that connects directly to your DSL, cable or fiber line, the U7-LR is the wrong product. It is not a router. It is not a modem. It is an access point.&lt;/p&gt;

&lt;p&gt;You need the rest of the network around it: a router or gateway, ideally a PoE switch or a suitable PoE injector, and some way to manage UniFi Network.&lt;/p&gt;

&lt;p&gt;I would probably skip it if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you do not want to use UniFi&lt;/li&gt;
&lt;li&gt;6 GHz is a must-have for you&lt;/li&gt;
&lt;li&gt;you do not have PoE or do not want to add it&lt;/li&gt;
&lt;li&gt;you expect an all-in-one home router&lt;/li&gt;
&lt;li&gt;you only need WiFi for one small room&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;UniFi is not impossibly complicated, but it is still more of a system than a plug-and-forget consumer box. That is either the whole point, or it is unnecessary overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What customer reviews suggest
&lt;/h2&gt;

&lt;p&gt;On Amazon Germany, the Ubiquiti U7-LR currently sits at around 4.2 out of 5 stars with 78 global ratings. The positive reviews mostly mention good range, stable connections and easy setup inside an existing UniFi environment.&lt;/p&gt;

&lt;p&gt;Some reviews are very short, which is normal for Amazon. Still, the pattern is useful: people who already know UniFi seem to get along with the U7-LR quickly. Several buyers describe it as a reliable access point that does exactly what they expected.&lt;/p&gt;

&lt;p&gt;There is also some criticism. One international review points out that the U7-LR is not a tri-band access point. Another mentions that 2.4 GHz performance did not meet expectations compared with 5 GHz. That lines up with the main caveat above: do not read "WiFi 7" and assume you are getting every premium WiFi 7 feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  PoE and 2.5 GbE are not optional details
&lt;/h2&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%2Fimages.svc.ui.com%2F%3Fu%3Dhttps%253A%252F%252Fcdn.ecomm.ui.com%252Fproducts%252F7455fa2b-3074-47a0-b82f-a2cd701d4a8f%252Ff3a549f7-2a60-4c4e-811b-158bb34dbf29.png%26q%3D75%26w%3D1080" 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%2Fimages.svc.ui.com%2F%3Fu%3Dhttps%253A%252F%252Fcdn.ecomm.ui.com%252Fproducts%252F7455fa2b-3074-47a0-b82f-a2cd701d4a8f%252Ff3a549f7-2a60-4c4e-811b-158bb34dbf29.png%26q%3D75%26w%3D1080" alt="Ubiquiti U7-LR review: WiFi 7 with strong range, but no 6 GHz" width="1080" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Image: Ubiquiti / official product image&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The U7-LR is powered via PoE. In a clean network installation, that is great. One Ethernet cable handles data and power. For casual living-room use, it can be an extra hurdle.&lt;/p&gt;

&lt;p&gt;If your switch does not provide PoE, you need a suitable PoE injector. You should also think about the uplink. The access point has a 2.5 GbE port, and ideally the switch port behind it should support 2.5 GbE as well.&lt;/p&gt;

&lt;p&gt;That does not mean gigabit is useless. For many internet connections it is still enough. But if you are buying a WiFi 7 access point, it makes sense to check whether the rest of your network can keep up.&lt;/p&gt;

&lt;h2&gt;
  
  
  U7-LR or another UniFi access point?
&lt;/h2&gt;

&lt;p&gt;The U7-LR is attractive if range and modern 5 GHz performance matter more to you than 6 GHz. If you already own several WiFi 7 clients with 6 GHz support and want to use that band, compare it with a proper tri-band access point before buying.&lt;/p&gt;

&lt;p&gt;If you want to cover a house or small office with stable UniFi WiFi, the U7-LR looks like a sensible option. I would not read the "300+ clients" figure as a realistic recommendation for hundreds of active devices hammering the network at once. It is more a sign that Ubiquiti designed this for more than three phones and a TV.&lt;/p&gt;

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

&lt;p&gt;What I like about the U7-LR is that it knows what it is. It is not trying to be a flashy consumer router with a friendly app and a dozen marketing claims. It is a UniFi access point. That is the reason to buy it.&lt;/p&gt;

&lt;p&gt;The Long Range name and the WiFi 7 label make it appealing, but the missing 6 GHz radio belongs in every buying decision. If you know that and it fits your setup, the U7-LR is a very interesting upgrade.&lt;/p&gt;

&lt;p&gt;If you simply want "full WiFi 7", I would compare alternatives first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you buy it?
&lt;/h2&gt;

&lt;p&gt;I would buy the Ubiquiti U7-LR if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you already use UniFi&lt;/li&gt;
&lt;li&gt;you want a ceiling or wall mounted access point&lt;/li&gt;
&lt;li&gt;you can provide PoE properly&lt;/li&gt;
&lt;li&gt;you care about strong 5 GHz performance and range&lt;/li&gt;
&lt;li&gt;6 GHz is not required for your setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would not buy it if you actually need a router, or if 6 GHz is the main reason you are upgrading.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://amzn.to/3PsNXhx?ref=blog.bajonczak.com" rel="noopener noreferrer"&gt;View the Ubiquiti U7-LR on Amazon&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: This article is not based on my own hands-on lab test. The assessment is based on Ubiquiti's official specifications, the Amazon product page and publicly visible customer reviews.&lt;/p&gt;

</description>
      <category>ubiquiti</category>
      <category>unifi</category>
      <category>networking</category>
      <category>affiliate</category>
    </item>
    <item>
      <title>Teufel Rockster Air 2 review: powerful portable sound, but should you buy it over the JBL PartyBox Stage 320?</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Sun, 17 May 2026 17:21:45 +0000</pubDate>
      <link>https://dev.to/saschadev/teufel-rockster-air-2-review-powerful-portable-sound-but-should-you-buy-it-over-the-jbl-partybox-17hi</link>
      <guid>https://dev.to/saschadev/teufel-rockster-air-2-review-powerful-portable-sound-but-should-you-buy-it-over-the-jbl-partybox-17hi</guid>
      <description>&lt;h1&gt;
  
  
  Teufel Rockster Air 2 review: powerful portable sound, but should you buy it over the JBL PartyBox Stage 320?
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Transparency note: the Amazon links in this article are affiliate links. If you buy through them, I may earn a commission at no extra cost to you. I have not run a long-term hands-on test of these speakers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you are looking for a serious portable Bluetooth speaker, the &lt;strong&gt;Teufel Rockster Air 2&lt;/strong&gt; and the &lt;strong&gt;JBL PartyBox Stage 320&lt;/strong&gt; sit in a similar “big party speaker” category, but they are not really built for exactly the same buyer.&lt;/p&gt;

&lt;p&gt;The Teufel feels more like a compact mobile PA system: loud, flexible, long-lasting, and ready for microphones, instruments, small events, speeches, DJs, karaoke, and outdoor setups. The JBL is more obviously a party speaker: easier to move around, cheaper, flashier, and probably the better fit for most casual buyers.&lt;/p&gt;

&lt;p&gt;So which one should you actually buy?&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick verdict
&lt;/h2&gt;

&lt;p&gt;If I had to recommend one speaker for most people, I would pick the &lt;strong&gt;JBL PartyBox Stage 320&lt;/strong&gt; because it is cheaper, easier to transport, has a built-in light show, and has very strong public ratings.&lt;/p&gt;

&lt;p&gt;But if you care more about &lt;strong&gt;battery life, PA-style flexibility, microphone/instrument inputs, and a more professional event setup&lt;/strong&gt;, the &lt;strong&gt;Teufel Rockster Air 2&lt;/strong&gt; is the more serious device.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Buy the Teufel Rockster Air 2&lt;/strong&gt; if you want a portable event speaker with long battery life and flexible inputs: &lt;a href="https://amzn.to/3PJNCXS?ref=blog.bajonczak.com" rel="noopener noreferrer"&gt;check the Teufel Rockster Air 2 on Amazon&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buy the JBL PartyBox Stage 320&lt;/strong&gt; if you want the better value party speaker for most private parties: &lt;a href="https://amzn.to/4fqubxx?ref=blog.bajonczak.com" rel="noopener noreferrer"&gt;check the JBL PartyBox Stage 320 on Amazon&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Teufel Rockster Air 2 at a glance
&lt;/h2&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%2F47t7azacfhpmuwd4jqs0.jpg" 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%2F47t7azacfhpmuwd4jqs0.jpg" alt="Teufel Rockster Air 2" width="609" height="1094"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Teufel Rockster Air 2&lt;/strong&gt; is a portable Bluetooth event speaker with a clear focus on power and flexibility. According to the Amazon listing, it offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bluetooth 5.0 with aptX, aptX HD and AAC&lt;/li&gt;
&lt;li&gt;Up to &lt;strong&gt;58 hours&lt;/strong&gt; of battery life at medium volume&lt;/li&gt;
&lt;li&gt;Up to &lt;strong&gt;31 hours&lt;/strong&gt; at maximum volume in Eco mode&lt;/li&gt;
&lt;li&gt;A removable LiFePO4 battery&lt;/li&gt;
&lt;li&gt;Microphone, instrument and AUX inputs&lt;/li&gt;
&lt;li&gt;USB-C power bank function&lt;/li&gt;
&lt;li&gt;35 mm tripod flange&lt;/li&gt;
&lt;li&gt;Up to &lt;strong&gt;103 dB RMS&lt;/strong&gt; and &lt;strong&gt;115 dB peak&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Support for stereo setups with two Rockster Air 2 units&lt;/li&gt;
&lt;li&gt;XLR-based linking for larger setups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That specification list tells you quite a lot about the target audience. This is not just a speaker for playing a playlist in the kitchen. It is meant for people who need loud, mobile, reasonably serious sound.&lt;/p&gt;

&lt;p&gt;At the time I checked the listing, the Teufel Rockster Air 2 was shown at around &lt;strong&gt;€529.99&lt;/strong&gt; with a rating of around &lt;strong&gt;4.5 out of 5 stars&lt;/strong&gt; from about &lt;strong&gt;100 reviews&lt;/strong&gt;. Amazon prices and review counts can change, so treat those numbers as a snapshot rather than a fixed fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I like about the Teufel Rockster Air 2
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The battery life is the standout feature
&lt;/h3&gt;

&lt;p&gt;The biggest argument for the Teufel is battery life. Up to 58 hours at medium volume is excellent for this class of speaker. Even the claimed 31 hours at maximum volume in Eco mode is impressive.&lt;/p&gt;

&lt;p&gt;For garden parties, small events, club rooms, outdoor workouts, sports events or speeches, this matters. A speaker that still has power left after a long day is simply less annoying to own.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It is closer to a mobile PA than a normal party speaker
&lt;/h3&gt;

&lt;p&gt;The Teufel has microphone, instrument and AUX inputs. That makes it much more flexible than a simple Bluetooth party box. You can use it for karaoke, announcements, a guitar, small performances or DJ-style setups.&lt;/p&gt;

&lt;p&gt;That is where the Rockster Air 2 starts to justify its higher price. If you only want music from your phone, you may not need this. If you want a more flexible event speaker, it becomes much more interesting.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The battery is removable
&lt;/h3&gt;

&lt;p&gt;A removable LiFePO4 battery is a practical long-term advantage. Batteries age. A product with a replaceable battery is usually a safer buy than one where the whole device becomes less useful once the battery degrades.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Better codec support than many party speakers
&lt;/h3&gt;

&lt;p&gt;Bluetooth with aptX, aptX HD and AAC is a nice detail. It does not magically turn a party speaker into a studio monitor, but it does show that Teufel is paying attention to audio quality and not just loudness.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do not like about the Teufel Rockster Air 2
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. It is expensive
&lt;/h3&gt;

&lt;p&gt;At roughly €530 at the time of checking, the Teufel is not an impulse purchase. It costs noticeably more than the JBL PartyBox Stage 320.&lt;/p&gt;

&lt;p&gt;That price only makes sense if you actually need the Teufel’s strengths: battery life, input flexibility, PA-style use, and very loud output.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It lacks the fun factor of a classic party box
&lt;/h3&gt;

&lt;p&gt;The Teufel looks and feels more serious. That can be a good thing, but it also means you do not get the obvious party features of the JBL: the synchronized light show, the flashy design language, and the “turn it on and the room gets a vibe” effect.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Transport looks less convenient than the JBL
&lt;/h3&gt;

&lt;p&gt;The JBL PartyBox Stage 320 has wheels and a telescopic handle. That is boring on paper but hugely useful in real life. Big speakers are always heavier and more annoying to move than people expect.&lt;/p&gt;

&lt;p&gt;If you move your speaker often, the JBL has a clear practical advantage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The JBL PartyBox Stage 320 as the main alternative
&lt;/h2&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%2Fschnupkxk50ut8nmcu3i.jpg" 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%2Fschnupkxk50ut8nmcu3i.jpg" alt="JBL PartyBox Stage 320" width="522" height="1281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;JBL PartyBox Stage 320&lt;/strong&gt; is the more mainstream party speaker. It is built around JBL Pro Sound, two 6.5-inch woofers, two 1-inch tweeters, a music-synchronized light show, wheels, and a telescopic handle.&lt;/p&gt;

&lt;p&gt;According to the Amazon listing, it offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Up to &lt;strong&gt;18 hours&lt;/strong&gt; of battery life&lt;/li&gt;
&lt;li&gt;10-minute quick charge for about 2 additional hours of playback&lt;/li&gt;
&lt;li&gt;AI Sound Boost for real-time audio optimization&lt;/li&gt;
&lt;li&gt;Built-in light show&lt;/li&gt;
&lt;li&gt;Wheels and telescopic handle&lt;/li&gt;
&lt;li&gt;Auracast support for connecting multiple compatible JBL speakers&lt;/li&gt;
&lt;li&gt;Replaceable battery, sold separately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the time I checked the listing, it was shown at around &lt;strong&gt;€420&lt;/strong&gt; with a rating of around &lt;strong&gt;4.8 out of 5 stars&lt;/strong&gt; from about &lt;strong&gt;730 reviews&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teufel Rockster Air 2 vs JBL PartyBox Stage 320
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Teufel Rockster Air 2&lt;/th&gt;
&lt;th&gt;JBL PartyBox Stage 320&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Approx. price at time of checking&lt;/td&gt;
&lt;td&gt;€529.99&lt;/td&gt;
&lt;td&gt;€420&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Battery life&lt;/td&gt;
&lt;td&gt;Up to 58 hours&lt;/td&gt;
&lt;td&gt;Up to 18 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main strength&lt;/td&gt;
&lt;td&gt;PA-style flexibility and long runtime&lt;/td&gt;
&lt;td&gt;Party features and value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;Portable, but less comfort-focused&lt;/td&gt;
&lt;td&gt;Wheels and telescopic handle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light show&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microphone/instrument use&lt;/td&gt;
&lt;td&gt;Stronger focus&lt;/td&gt;
&lt;td&gt;More party-oriented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-speaker setup&lt;/td&gt;
&lt;td&gt;Stereo / XLR options&lt;/td&gt;
&lt;td&gt;Auracast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Events, speeches, karaoke, outdoor use, semi-pro setups&lt;/td&gt;
&lt;td&gt;Private parties, garden parties, casual users&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which one should you buy?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Buy the Teufel Rockster Air 2 if…
&lt;/h3&gt;

&lt;p&gt;You should choose the &lt;strong&gt;Teufel Rockster Air 2&lt;/strong&gt; if you want a speaker that can do more than just play music loudly.&lt;/p&gt;

&lt;p&gt;It is the better fit if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need very long battery life&lt;/li&gt;
&lt;li&gt;You want microphone or instrument inputs&lt;/li&gt;
&lt;li&gt;You might use the speaker for speeches or small events&lt;/li&gt;
&lt;li&gt;You care about a more PA-like setup&lt;/li&gt;
&lt;li&gt;You want a removable battery&lt;/li&gt;
&lt;li&gt;You want to place it on a tripod&lt;/li&gt;
&lt;li&gt;You may later expand into stereo or linked speaker setups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In that case, the higher price makes sense: &lt;a href="https://amzn.to/3PJNCXS?ref=blog.bajonczak.com" rel="noopener noreferrer"&gt;Teufel Rockster Air 2 on Amazon&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Buy the JBL PartyBox Stage 320 if…
&lt;/h3&gt;

&lt;p&gt;You should choose the &lt;strong&gt;JBL PartyBox Stage 320&lt;/strong&gt; if you mostly want a powerful, fun and practical party speaker.&lt;/p&gt;

&lt;p&gt;It is the better fit if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want the better price-performance ratio&lt;/li&gt;
&lt;li&gt;You care about easy transport&lt;/li&gt;
&lt;li&gt;You like the built-in light show&lt;/li&gt;
&lt;li&gt;You mainly use Bluetooth playback&lt;/li&gt;
&lt;li&gt;You want a proven party speaker with lots of public feedback&lt;/li&gt;
&lt;li&gt;You do not need 50+ hours of battery life&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most buyers, this is probably the smarter purchase: &lt;a href="https://amzn.to/4fqubxx?ref=blog.bajonczak.com" rel="noopener noreferrer"&gt;JBL PartyBox Stage 320 on Amazon&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My final recommendation
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Teufel Rockster Air 2&lt;/strong&gt; is the more serious and flexible product. It is the speaker I would look at for events, long outdoor days, microphone use, karaoke, small performances, DJ-style setups or situations where battery life really matters.&lt;/p&gt;

&lt;p&gt;But the &lt;strong&gt;JBL PartyBox Stage 320&lt;/strong&gt; is the speaker I would recommend to most people. It costs less, is easier to move, has a more obvious party feature set, and has stronger public review momentum.&lt;/p&gt;

&lt;p&gt;So my practical recommendation is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best overall for most people:&lt;/strong&gt; &lt;a href="https://amzn.to/4fqubxx?ref=blog.bajonczak.com" rel="noopener noreferrer"&gt;JBL PartyBox Stage 320&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for more serious mobile event use:&lt;/strong&gt; &lt;a href="https://amzn.to/3PJNCXS?ref=blog.bajonczak.com" rel="noopener noreferrer"&gt;Teufel Rockster Air 2&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your use case is “I want a great speaker for birthdays, garden parties and casual events”, get the JBL. If your use case is “I need a portable speaker that can also behave like a small PA system”, get the Teufel.&lt;/p&gt;

</description>
      <category>affiliate</category>
      <category>productreview</category>
      <category>audio</category>
      <category>buyingguide</category>
    </item>
    <item>
      <title>A practical SAP agent in Azure AI Foundry: OData in, governed answer out</title>
      <dc:creator>Der Sascha</dc:creator>
      <pubDate>Thu, 14 May 2026 12:40:48 +0000</pubDate>
      <link>https://dev.to/saschadev/a-practical-sap-agent-in-azure-ai-foundry-odata-in-governed-answer-out-81h</link>
      <guid>https://dev.to/saschadev/a-practical-sap-agent-in-azure-ai-foundry-odata-in-governed-answer-out-81h</guid>
      <description>&lt;p&gt;Most enterprise agent demos cheat at the exact point where things get interesting.&lt;/p&gt;

&lt;p&gt;They show a nice chat window. They connect it to a toy API. The agent calls a function, gets a clean JSON response, and everyone nods. Then you try the same pattern against a real SAP system and suddenly the demo has to deal with authorization, OData filters, weird field names, data minimization, audit logs, and the uncomfortable fact that "ask the model to query SAP" is not an architecture.&lt;/p&gt;

&lt;p&gt;This post is the version I would actually start with.&lt;/p&gt;

&lt;p&gt;The scenario is intentionally boring: a support or operations user asks why a customer order is blocked. The agent should look up a small, approved slice of SAP data, summarize the situation, and suggest the next action. No direct database access. No model-generated OData. No magic prompt that says "be secure" and hopes for the best.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the solution
&lt;/h2&gt;

&lt;p&gt;I would split the system into four parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Azure AI Foundry hosts the agent and handles the conversation.&lt;/li&gt;
&lt;li&gt;The agent gets one narrow tool, for example &lt;code&gt;get_order_status&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A backend service owns the SAP integration and calls an approved OData endpoint.&lt;/li&gt;
&lt;li&gt;Identity, logging, and policy live outside the prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent is allowed to ask a question. The tool is allowed to fetch a specific business object. The SAP layer is allowed to enforce the ugly but necessary rules.&lt;/p&gt;

&lt;p&gt;That separation matters. If the model can invent arbitrary OData queries, it can also invent expensive, broad, or unauthorized queries. If the backend only exposes a small function with typed parameters, the blast radius is much smaller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example architecture
&lt;/h2&gt;

&lt;p&gt;A minimal production-ish flow 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;User
  -&amp;gt; Teams / web app / Copilot extension
  -&amp;gt; Azure AI Foundry agent
  -&amp;gt; function tool: get_order_status(order_id)
  -&amp;gt; integration API
  -&amp;gt; SAP OData endpoint / SAP BTP destination / API Management
  -&amp;gt; sanitized JSON back to the agent
  -&amp;gt; short answer + next action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like putting Azure API Management or a small integration API between Foundry and SAP. It gives you one place for throttling, logging, allow lists, correlation IDs, and request validation. You can also swap the SAP backend later without teaching the agent a new trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SAP side: keep it boring
&lt;/h2&gt;

&lt;p&gt;Here is a deliberately small Python client for an SAP OData endpoint. The important part is not the HTTP library. The important part is that the caller cannot pass arbitrary filters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# sap_client.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;lifecycle_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;delivery_block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;credit_block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;net_value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SapClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SAP_ODATA_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SAP_TECH_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SAP_TECH_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id must be a numeric SAP sales order id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$select&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SalesOrder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SoldToPartyName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OverallSDProcessStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DeliveryBlockReason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CreditBlockReason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TotalNetAmount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TransactionCurrency&lt;/span&gt;&lt;span class="sh"&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="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SalesOrder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;customer_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SoldToPartyName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;lifecycle_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OverallSDProcessStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;delivery_block&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DeliveryBlockReason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;credit_block&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CreditBlockReason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;net_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TotalNetAmount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TransactionCurrency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&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;A real implementation would probably use OAuth, principal propagation, SAP BTP destinations, or an API Management policy instead of a technical user. Fine. The same rule still applies: the agent should not build the SAP query. Your integration layer should.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool boundary
&lt;/h2&gt;

&lt;p&gt;Now wrap the SAP client in a tiny tool function. This is also the place where I would remove fields the user should not see.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tools.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sap_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SapClient&lt;/span&gt;

&lt;span class="n"&gt;sap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SapClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return a sanitized status summary for one SAP sales order.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_order_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;blocks&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="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivery_block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;blocks&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivery&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivery_block&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credit_block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;blocks&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credit_block&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lifecycle_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lifecycle_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;net_value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;net_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&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;Notice what is missing: pricing conditions, margin, bank details, free text notes, internal partner data, and anything else that tends to leak into "just give the AI access" projects.&lt;/p&gt;

&lt;p&gt;The model gets the minimum amount of data needed to answer the business question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering the tool in Azure AI Foundry
&lt;/h2&gt;

&lt;p&gt;The current Azure AI Foundry agent pattern is straightforward: define a function tool, create an agent version, send a user prompt, execute the requested function call in your app, and submit the tool output back to the model.&lt;/p&gt;

&lt;p&gt;The sketch below follows that shape. It is not meant to be pasted blindly into production, but it shows the moving parts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# foundry_agent.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;azure.ai.projects&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AIProjectClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;azure.ai.projects.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FunctionTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PromptAgentDefinition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tool&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;azure.identity&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DefaultAzureCredential&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai.types.responses.response_input_param&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FunctionCallOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ResponseInputParam&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_order_status&lt;/span&gt;

&lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AIProjectClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AZURE_AI_PROJECT_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;DefaultAzureCredential&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_openai_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;conversation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;get_order_status_tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FunctionTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_order_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Get the sanitized status of one SAP sales order by numeric order id.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Numeric SAP sales order id, for example 4711000420&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Tool&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="n"&gt;get_order_status_tool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;agent_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sap-order-status-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;definition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;PromptAgentDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4.1-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You help operations users understand SAP sales order status. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use the provided tool when an order id is present. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Do not ask for or expose sensitive personal, payroll, banking, or margin data. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer briefly. If a block exists, explain the likely owner and next action.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&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="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Why is sales order 4711000420 blocked?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;extra_body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_reference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_reference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tool_outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ResponseInputParam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function_call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_order_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_order_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tool_outputs&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="nc"&gt;FunctionCallOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function_call_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&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="n"&gt;final_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool_outputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;extra_body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_reference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_reference&lt;/span&gt;&lt;span class="sh"&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A possible answer might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sales order 4711000420 is blocked because it has a credit block.
The order value is 18,420 EUR for Contoso Retail GmbH.
Next action: ask credit management to review the customer exposure before release.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the right level of boring. The agent did not browse SAP. It did not decide which table to query. It called one approved capability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add correlation IDs before you add more tools
&lt;/h2&gt;

&lt;p&gt;Before I would add a second or third SAP tool, I would add tracing.&lt;/p&gt;

&lt;p&gt;Every request should carry a correlation ID from the UI to Foundry, from Foundry to the integration API, and from the integration API to SAP or API Management logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# api.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_order_status&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/orders/{order_id}/status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;order_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x_correlation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;x_correlation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing X-Correlation-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_order_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# In production, log this as structured telemetry.
&lt;/span&gt;    &lt;span class="c1"&gt;# Never log secrets or full SAP payloads.
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correlation_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x_correlation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_order_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocks&lt;/span&gt;&lt;span class="sh"&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="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part that gets skipped in demos and then hurts later. When a user says "the agent told me the wrong thing," you need to reconstruct what it saw, which tool it called, what SAP returned, and which policy version was active.&lt;/p&gt;

&lt;p&gt;Without that, you are debugging vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the avatar fits
&lt;/h2&gt;

&lt;p&gt;If you are building a Casandra-style avatar on top of this, I would keep the avatar layer dumb.&lt;/p&gt;

&lt;p&gt;The avatar can make the interaction feel nicer. It can speak, explain, ask follow-up questions, and show the answer in a more human way. But it should not own the SAP permissions, the OData query, or the policy decisions.&lt;/p&gt;

&lt;p&gt;A clean split 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;Avatar / UI:
  - captures the user request
  - shows status and confidence
  - asks for missing order id
  - renders the final answer

Foundry agent:
  - decides whether a tool call is needed
  - turns tool output into an explanation
  - follows response policy

Integration backend:
  - validates input
  - calls SAP
  - trims data
  - logs access
  - enforces authorization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes the avatar replaceable. Today it is a web avatar. Tomorrow it might be Teams, Copilot, a mobile app, or a voice interface. The SAP contract stays the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The policies I would enforce early
&lt;/h2&gt;

&lt;p&gt;I would not wait for an enterprise governance board to invent a 40-page document. Start with five rules in code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only approved tools can access SAP.&lt;/li&gt;
&lt;li&gt;Tools must have typed schemas and &lt;code&gt;additionalProperties: false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The model never receives raw SAP payloads.&lt;/li&gt;
&lt;li&gt;Every tool call gets a user id, tenant/context id, and correlation id.&lt;/li&gt;
&lt;li&gt;Tool output is logged as metadata, not as full business data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those five rules already avoid a lot of trouble.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small test that catches a big mistake
&lt;/h2&gt;

&lt;p&gt;Here is the kind of test I would add before showing the agent to anyone outside the team:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# test_tools.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_order_status&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_rejects_non_numeric_order_ids&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;get_order_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4711; $filter=NetValue gt 0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_tool_does_not_return_internal_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeSap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OrderStatus&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Contoso Retail GmbH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lifecycle_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blocked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivery_block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credit_block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Credit exposure exceeded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;net_value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;18420.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EUR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;margin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;internal_note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Do not expose this&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})()&lt;/span&gt;

    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;
    &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;FakeSap&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_order_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4711000420&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;margin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;internal_note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test like this is not glamorous. Good. Glamour is usually where agent projects start lying to themselves.&lt;/p&gt;

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

&lt;p&gt;For SAP integration, Azure AI Foundry becomes interesting when you stop treating it as a chatbot builder and start treating it as an orchestration layer with strict tool contracts.&lt;/p&gt;

&lt;p&gt;The model should explain. Your backend should decide what data exists, who can see it, and how it is fetched.&lt;/p&gt;

&lt;p&gt;That is less flashy than "natural language over SAP," but it is much closer to something I would trust in a real environment.&lt;/p&gt;




&lt;p&gt;Originally published at &lt;a href="https://blog.bajonczak.com/a-practical-sap-agent-in-azure-ai-foundry-odata-in-governed-answer-out/" rel="noopener noreferrer"&gt;https://blog.bajonczak.com/a-practical-sap-agent-in-azure-ai-foundry-odata-in-governed-answer-out/&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>sap</category>
      <category>ai</category>
      <category>odata</category>
    </item>
    <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>
  </channel>
</rss>
