<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[without.systems]]></title><description><![CDATA[without.systems]]></description><link>https://without.systems</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1729711404532/95932cd1-e2a1-432a-9573-dae60e924d12.png</url><title>without.systems</title><link>https://without.systems</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 16 Apr 2026 23:34:57 GMT</lastBuildDate><atom:link href="https://without.systems/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The OutSystems Forward Deployed Engineer]]></title><description><![CDATA[It’s no secret that software delivery is undergoing a fundamental transformation. While the shift from waterfall to agile is now widely adopted, organizations face yet another evolutionary leap with t]]></description><link>https://without.systems/the-outsystems-forward-deployed-engineer</link><guid isPermaLink="true">https://without.systems/the-outsystems-forward-deployed-engineer</guid><category><![CDATA[outsystems]]></category><category><![CDATA[Forward-Deployed Engineer]]></category><category><![CDATA[Low Code]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Thu, 26 Feb 2026 09:46:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754157171130/bd1bd88e-3a13-4624-aa31-037888d950b5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It’s no secret that software delivery is undergoing a fundamental transformation. While the shift from waterfall to agile is now widely adopted, organizations face yet another evolutionary leap with the rise of low-code platforms and, increasingly, AI-assisted AppGen platforms.</p>
<p>But this isn’t simply about new tools—it’s about sustained, structural changes in how teams work, how quickly stakeholders are involved, and how rapidly organizations can deliver value.</p>
<p>Each technological shift has seen <strong>team sizes shrink</strong>, <strong>feedback iterations with stakeholders shorten</strong>, and <strong>time-to-market accelerate</strong>.</p>
<p>At the center of this new delivery model is the <strong>Forward Deployed Engineer</strong>, uniquely positioned for projects where the business <strong>outcome is clear</strong> but the <strong>route is still being charted</strong>—or for companies that have defined a compelling digital vision but can’t operationalize it through traditional delivery methods</p>
<h1>What is a Forward Deployed Engineer</h1>
<p>The <strong>Forward Deployed Software Engineer (FDSE)</strong> model was pioneered by Palantir Technologies, renowned for solving complex, data-driven problems by embedding engineers on-site within client environments. Palantir demonstrated that for flexible, powerful platforms, “ease of use” is not enough—true value comes only when expert engineers tailor solutions to real-world, customer-specific contexts.</p>
<p>Other technology leaders quickly followed. ServiceNow employs FDSEs to deploy enterprise AI, Salesforce for its AgentForce platform, AMD to operationalize AI hardware, and OpenAI for production AI model integrations. As enterprise platforms, particularly in AI and data, become more powerful, the main obstacle becomes not technology, but its effective application within a customer’s unique operational landscape.</p>
<p>FDSEs meet this challenge head-on. Their role is:</p>
<ul>
<li><p><strong>Full technical ownership:</strong> The FDSE becomes the top authority for every customer deployment, architecting and iterating with autonomy.</p>
</li>
<li><p><strong>Strategic enablement:</strong> Like a startup CTO, the FDSE drives results, adapts on the fly, and navigates ambiguity.</p>
</li>
<li><p><strong>Deep integration:</strong> These engineers become operational partners, understanding business processes firsthand and iterating solutions live for maximum impact.</p>
</li>
</ul>
<p>In practice, the FDSE is an archetype for high-uncertainty transformation. With today’s OutSystems and low-code architectures, their skillset is more important and accessible than ever, especially for organizations unable to specify requirements or adapt to traditional agile models.</p>
<h1>My Personal Perspective</h1>
<p>When I stepped into the Low-Code world—specifically when I started developing with OutSystems in 2017 and became familiar with the OutSystems Delivery Process—I quickly realized that, to succeed as a service provider in Germany, an alternative delivery model was required.</p>
<p>While OutSystems, like most modern platforms, encourages agile practices and time-boxed iterations, the practicalities within the German market—particularly among the <strong>Mittelstand</strong>, our economic backbone comprised predominantly of privately and often family-owned businesses—are often different.</p>
<p>When building my team, I looked out for <strong>individuals with a wide technological perspective</strong>, <strong>quick-thinking</strong> abilities, and above all, the willingness and <strong>skill to work directly with business stakeholders.</strong> In essence, these were Forward Deployed Engineers before that label became widespread. At the time, I called this blended role “Solution Engineer.”</p>
<p>The reasoning was straightforward. For many <strong>Mittelstand</strong> companies, the concept of agile projects, sprints, or even time-boxed results is almost unknown.</p>
<p>The notion of investing in custom-developed software—as opposed to tailoring an existing standard product—is itself a profound paradigm shift. I recognized early on, that introducing these companies to a new way of working while also delivering their first applications would create unnecessary friction and potentially jeopardize project outcomes.</p>
<p>This insight confirmed for me the importance of a delivery model where technically capable, versatile engineers engage side-by-side with business users—breaking down barriers, minimizing the burden of change, and advancing digital innovation on the customer’s terms.</p>
<p>You could say that this was the Forward Deployed approach in practice—before the terminology existed.</p>
<h1>From Linear to Modular: The Evolution of Software Delivery</h1>
<p>Historically, organizations relied on the <strong>waterfall</strong> approach—large, specialized teams progressing through rigid phases, with feedback and business validation often arriving only at the end.</p>
<p>With <strong>agile</strong>, not only did cross-functional teams become the norm, but smaller teams worked in shorter timeboxes, providing more frequent opportunities for stakeholder input and business alignment.</p>
<p><strong>Low-code</strong> took these trends even further, reducing the headcount needed for production-ready applications, compressing feedback cycles, and dramatically decreasing time-to-market with visual modeling and modular reuse.</p>
<p>The latest evolution—AI-assisted <strong>AppGen</strong>—amplifies these trends, enabling lean teams to generate and evolve working solutions through near real-time dialogue with stakeholders.</p>
<img src="https://cdn.hashnode.com/uploads/covers/640a1abd3e0c5ff6cf7f8746/ea58cbcc-6df3-4997-b65e-fff98cad802a.png" alt="" style="display:block;margin:0 auto" />

<h1><strong>What Actually Gets Better as We Move Up the Stack</strong></h1>
<p>At each stage, four trends remain notably consistent:</p>
<ul>
<li><p><strong>Team sizes decrease</strong> as processes and technology enhance collaboration and execution.</p>
</li>
<li><p><strong>Feedback loops with stakeholders shorten</strong> significantly, reducing cycles from months to weeks, days, or even hours.</p>
</li>
<li><p><strong>Time-to-market speeds up</strong>, allowing organizations to deliver measurable value and adapt more quickly.</p>
</li>
<li><p><strong>The cost-risk-benefit barrier is lowered</strong>, enabling consideration of digital solutions that were previously too risky to develop.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/640a1abd3e0c5ff6cf7f8746/78741c37-a26e-41ae-bbb7-afd4e1e1981e.png" alt="" style="display:block;margin:0 auto" />

<p>AppGen makes these benefits concrete—not by shifting development to business users, but by equipping skilled engineers to work hand-in-hand with the business. The <strong>OutSystems Forward Deployed Engineer</strong> can rapidly prototype, engage directly with stakeholders, and deliver incremental value—even when requirements remain ambiguous or unrefined—a perfect fit for organizations that have a strong vision but can’t translate it into classic delivery.</p>
<div>
<div>💡</div>
<div>Each advancement—agile, low-code, AppGen—makes it possible for smaller, more collaborative teams to iterate rapidly with stakeholders and deliver value faster, even as details evolve or classic agile proves impractical.</div>
</div>

<h1><strong>Why the OutSystems Forward Deployed Engineer Is a Capability-Driven Role</strong></h1>
<p>Modern delivery is about more than solving isolated problems; it’s about building reusable capabilities in the face of incomplete requirements or emerging specifications. This demands a role that can bridge vision and execution, even when “how” is unclear or agile methods are unsuitable.</p>
<p>With every evolution in delivery, the <strong>OutSystems Forward Deployed Engineer</strong> leverages:</p>
<ul>
<li><p><strong>Smaller, more versatile teams</strong> for rapid collaboration.</p>
</li>
<li><p><strong>Shorter, more frequent feedback cycles</strong> for validation.</p>
</li>
<li><p><strong>Faster time-to-market</strong> as modular capabilities are assembled on the fly.</p>
</li>
<li><p><strong>The ability to deliver even when requirements are high-level or incomplete.</strong></p>
</li>
</ul>
<h1><strong>Beyond Experimental Projects: When Vision Outpaces Specification or Agile Isn’t Feasible</strong></h1>
<p>While experimental initiatives—where the outcome is clear but the implementation path is undefined—are a natural fit for <strong>OutSystems Forward Deployed Engineers</strong>, these are not the only contexts where this role thrives.</p>
<p>Many organizations can articulate a strategic digital vision (“we want a new customer portal” or “an integrated process experience”) but are unable to produce the detailed specifications required for classic or agile delivery. Others, because of their market, structure, or culture, may find agile frameworks, sprints, and backlogs difficult to sustain—yet the imperative to transform remains.</p>
<div>
<div>💡</div>
<div><strong>In both settings—where the ‘what’ is clear but the ‘how’ is uncertain—the OutSystems Forward Deployed Engineer becomes indispensable.</strong> This role converts business intent into software via prototyping, direct collaboration, and real-world refinement, bypassing the documentation paralysis that chokes traditional approaches. Feedback from stakeholders is nearly instant, value appears quickly, and solutions emerge alongside evolving requirements.</div>
</div>

<h1><strong>How Capabilities Are Developed and Delivered</strong></h1>
<p>The delivery workflow is optimized for speed and engagement:</p>
<ul>
<li><p><strong>Lean, empowered teams—often a single OutSystems Forward Deployed Engineer with SMEs—collaborate from vision, not just requirements.</strong></p>
</li>
<li><p><strong>Rapid, frequent feedback guarantees each iteration stays relevant.</strong></p>
</li>
<li><p><strong>Time-to-market stays short as solutions iterate with business needs and discoveries.</strong></p>
</li>
</ul>
<h1><strong>Supporting the Model: The Lean Center of Excellence</strong></h1>
<p>A lean Center of Excellence supports reuse, governance, and rapid scaling—further reducing necessary team size and time-to-market. AI-guided discovery of assets makes each delivery faster and more robust over time.</p>
<h1><strong>The Organizational Payoff: Compounding Returns</strong></h1>
<p>Organizations progressing on this journey see profound changes: <em>teams shrink, stakeholder feedback is immediate, and time-to-market accelerates dramatically</em>. This model delivers even in high-change or ambiguous environments, creating a scalable and repeatable approach to capability-driven transformation.</p>
<h1><strong>Defining the OutSystems Forward Deployed Engineer</strong></h1>
<p>An OutSystems Forward Deployed Engineer:</p>
<ul>
<li><p>Thrives in ambiguity—able to operate from vision, not just requirements.</p>
</li>
<li><p>Leads or participates in <em>small, nimble teams</em> within business operations.</p>
</li>
<li><p>Delivers rapid, collaborative feedback cycles to refine solutions live.</p>
</li>
<li><p>Translates business intent into reusable digital capabilities—even without classic requirements or agile discipline.</p>
</li>
<li><p>Assumes end-to-end technical and business responsibility—often acting as principal driver of success.</p>
</li>
</ul>
<h1><strong>Conclusion: Shorter, Faster, More Flexible Paths to Value</strong></h1>
<p>With an <strong>OutSystems Forward Deployed Engineer</strong>, organizations are no longer constrained by their ability to write detailed specifications or fit into traditional agile frameworks. Digital solutions—and business value—begin to flow thanks to smaller, multidisciplinary teams, rapid and continuous stakeholder feedback, and dramatically accelerated time-to-market. This model does not require clients to fully adopt or internalize new delivery methodologies overnight, nor does it demand an immediate cultural shift toward agile processes that may be unfamiliar or impractical.</p>
<p>Instead, the <strong>OutSystems Forward Deployed Engineer meets the business exactly where it is</strong>. By embedding with business stakeholders and iteratively developing solutions directly in their context, this role <strong>minimizes the friction and "change tax"</strong> that often derails digital transformation before it can start. Every iteration is an opportunity for learning, alignment, and tangible results—<strong>reducing operational risk</strong> and <strong>ensuring solutions are both technically robust and business-ready</strong>.</p>
<p>As transformative technologies like low-code, AI-assisted development, and modular digital capabilities become more prevalent, the OutSystems Forward Deployed Engineer stands out as a strategic enabler of continuous innovation. Whether an organization’s challenge is ambiguity in requirements, unfamiliarity with agile, or the simple need to move fast without heavy overhead, this role is the bridge from digital vision to working, scalable solutions.</p>
<p>Ultimately, value today flows not from documentation or theoretical roadmaps, but from the ability to <strong>turn intent into reality</strong>—<strong>side by side with the business</strong>, through smaller teams, shorter feedback loops, and a relentless focus on rapid, user-centered delivery. For organizations ready to accelerate and de-risk their digital journey, embedding an OutSystems Forward Deployed Engineer isn’t just a delivery tactic—it’s a competitive advantage.</p>
]]></content:encoded></item><item><title><![CDATA[Bridge the Gap: Enabling OutSystems 11 Users to Access OutSystems Developer Cloud Applications]]></title><description><![CDATA[At the OutSystems NextStep Experience (ONE) conference 2025 in Lisbon, OutSystems announced that every O11 customer can add an extra OutSystems Developer Cloud environment to their subscription. Most importantly, OutSystems 11 no longer has an end-of...]]></description><link>https://without.systems/bridge-the-gap-outsystems-11-users-to-outsystems-developer-cloud</link><guid isPermaLink="true">https://without.systems/bridge-the-gap-outsystems-11-users-to-outsystems-developer-cloud</guid><category><![CDATA[outsystems]]></category><category><![CDATA[OpenID Connect]]></category><category><![CDATA[Low Code]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Sun, 08 Feb 2026 05:47:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766558759520/c1d1e3f6-55fc-4635-9fd7-ad03e50394fb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At the OutSystems NextStep Experience (<strong>ONE</strong>) conference 2025 in Lisbon, OutSystems announced that every O11 customer can add an extra OutSystems Developer Cloud environment to their subscription. Most importantly, OutSystems 11 no longer has an end-of-life date.</p>
<p>OutSystems puts a lot of effort into making interoperability as easy as possible. As of now (Feb 2026), it's already possible to connect OutSystems 11 data (Entities) to ODC for both reading and writing, with more features on the way. Watch the <a target="_blank" href="https://www.outsystems.com/webinars/platform-interoperability/">Platform Interoperability on-demand webinar</a> for an overview.</p>
<p>One of the still upcoming features is Identity Integration, which will let OutSystems 11 users sign in to ODC applications. Meanwhile, community members have already started asking on the forum about the easiest way to connect users with ODC.</p>
<p>That's why I created a component, essentially a Mini Identity Provider, for OutSystems 11. You can use it right away to connect your OutSystems 11 user base to ODC by adding a new Identity Provider in ODC Portal.</p>
<h1 id="heading-when-you-should-care">When You Should Care</h1>
<p>This article and the Forge component are relevant to you if you're using the default username/password authentication method in the OutSystems 11 Users Provider. This includes any custom modifications you've made.</p>
<p>It doesn't apply if you're already using an external Identity Provider like Microsoft Entra, Auth0, or others.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I was quite surprised that so many O11 environments use the built-in default authentication method instead of an external, professional identity provider. I might be strict in this view, but I can't think of any situation where this makes sense. You either miss out on many features of a dedicated solution, like single sign-on, multi-factor authentication, conditional access, anomaly detection, and more, or you'd have to build it yourself (which is possible, of course).</div>
</div>

<p>If you are using OutSystems 11's default authentication and want your users to sign in to ODC with their O11 account, this article and component might be useful for you, even after OutSystems releases the official identity integration. OutSystems might not cover every possible edge case or your specific identity integration needs right away. In that case, you can use the component to tailor it to your needs and later transition smoothly to the official integration.</p>
<p>Besides all this, you may learn some implementation details about <a target="_blank" href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow">OAuth 2.0 Authorization Code flow</a>, which can be valuable in many other situations.</p>
<h1 id="heading-overview">Overview</h1>
<p>The ODC Identity Bridge application consists of three modules:</p>
<ul>
<li><p><strong>OpenIDBridge</strong> - This web module provides a sample Login (<strong>Authorize</strong>) screen with only a username and password.</p>
<p>  The direct URL of the <strong>Authorize</strong> screen is the URL returned by the ODC <strong>GetExternalLoginURL</strong> client action. The user is redirected to this URL to authenticate.</p>
</li>
<li><p><strong>OpenIDBridgeAPI</strong> - This service module is a core implementation of an OpenID Connect/OAuth 2.0 compatible Identity Provider. The module exposes REST operations for ODC to discover endpoints, like the <strong>Authorize</strong> endpoint and exchange tokens.</p>
</li>
<li><p><strong>OpenIDBridgeUtility</strong> - This code module provides only a single action, <strong>UrlDecode</strong>. You likely already have a similar utility action in your environment and may want to replace it.</p>
</li>
</ul>
<h1 id="heading-prerequisites">Prerequisites</h1>
<p>Before we dive further into the details, let's set up the necessary configurations to get <strong>ODC Identity Bridge</strong> up and running.</p>
<ul>
<li><p>Install <a target="_blank" href="https://www.outsystems.com/forge/component-documentation/22939/odc-identity-bridge-o11/0">ODC Identity Bridge</a> from Forge.</p>
</li>
<li><p>In <strong>O11 Service Center</strong>, we need to configure some <strong>Site Properties</strong> of the <strong>OpenIDBridgeAPI</strong> module.</p>
</li>
<li><p>In the <strong>ODC Portal</strong>, we need to add an <strong>Identity Provider</strong> configuration.</p>
</li>
</ul>
<p>Open the <strong>OpenIDBridgeAPI</strong> module in Service Center and click on the <strong>Site Properties</strong> tab. First, set a value for <strong>ClientId</strong> and <strong>ClientSecret</strong>. You can think of them as a service account's username and password that ODC uses to identify itself to the OpenID Bridge Identity Provider.</p>
<ul>
<li><p><strong>ClientId</strong> - This can be any text string, such as "<em>odc-dev-env</em>" or just a UUID.</p>
</li>
<li><p><strong>ClientSecret</strong> - Enter a strong password here.</p>
</li>
</ul>
<p>Leave the Site Properties tab open as we need to revisit it shortly. Open another tab and browse to your <strong>ODC Portal</strong> and click on <strong>Manage - Identity providers</strong>.</p>
<p>Click <strong>Add provider - OpenID Connect</strong>.</p>
<ul>
<li><p><strong>Provider name</strong> - Choose a name for this identity provider, like <em>OutSystems 11 Development</em>.</p>
</li>
<li><p><strong>Discovery endpoint</strong> - Use the URL: <code>https://&lt;Your O11 environment domain name&gt;/OpenIDBridgeAPI/rest/Oauth/Discovery</code>. After clicking on Get Details, the additional configuration details will appear on the right.</p>
</li>
<li><p><strong>Client ID</strong> - Enter the Client ID you set in the Site Properties of the <strong>OpenIDBridgeAPI</strong> module in Service Center.</p>
</li>
<li><p><strong>Client secret (secret value)</strong> - Enter the Client secret you set in the Site Properties of the <strong>OpenIDBridgeAPI</strong> module in Service Center.</p>
</li>
<li><p><strong>PKCE</strong> - Select <strong>None</strong>.</p>
</li>
<li><p><strong>Organization user email verification</strong> - Select <strong>Trust all user emails as verified</strong>.</p>
</li>
</ul>
<p>Under <strong>Claim mapping</strong>:</p>
<ul>
<li><strong>Username</strong> - Set to <strong>preferred_username</strong>.</li>
</ul>
<p>Leave all other settings as they are and click <strong>Save</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766727933045/1ed92184-9796-4f05-9ea0-d88a664933a1.png" alt class="image--center mx-auto" /></p>
<p>Next, click on <strong>Assign</strong> and link this Identity Provider configuration to applications in your development stage.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You will need a separate Identity Provider configuration for each of your stages.</div>
</div>

<p>Finally, click on the <strong>Redirect URLs</strong> tab and expand Apps in Development. Copy the value of the <strong>Login URL</strong>.</p>
<p>In the O11 Service Center, go to the Site Properties of the <strong>OpenIDBridgeAPI</strong> module and paste the <strong>Login URL</strong> value into the <strong>RedirectUri</strong> property.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766728557849/0a335321-eeda-4d8c-8d3b-638c97281708.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-authentication-flow">Authentication Flow</h1>
<p>With ODC Identity Bridge installed and configured lets look on how the overall authentication flow works.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770477123756/32f2d18a-0106-4c27-b7f5-7d93fd8bcaa4.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>An unauthenticated user tries to access an ODC application.</p>
</li>
<li><p>A <strong>SecurityException</strong> is thrown and handled in <strong>Common - OnException</strong>.</p>
</li>
<li><p>Your handler executes the <strong>GetExternalLoginURL</strong> client action to obtain the Authorize endpoint URL of the ODC Identity Bridge application in OutSystems 11.</p>
</li>
<li><p>Your handler redirects the user to this URL to sign in with O11 user credentials.</p>
</li>
<li><p>After signing in, the ODC Identity Bridge first redirects the user back to the <strong>RedirectURI</strong> of the ODC Identity Broker. This redirect includes an <strong>Authorization Code (code)</strong>.</p>
</li>
<li><p>The ODC Identity Broker uses this code to request an <strong>identity and access token</strong> from the Token endpoint of the ODC Identity Bridge application.</p>
</li>
<li><p>It then decodes the Identity token, finds a user account or creates a new one, and authenticates the user.</p>
</li>
<li><p>Finally, it redirects the user back to the application they requested.</p>
</li>
</ul>
<p>You already know that having a user account in ODC doesn't automatically grant access to an application. The user needs additional role assignments. There are several ways to ensure a user has roles assigned when signing in to ODC.</p>
<ul>
<li><p>Pre-provision all user accounts from O11 to ODC using a <strong>Timer</strong> that creates or updates user accounts in ODC and assigns roles.</p>
</li>
<li><p>Use ODC <strong>Group Mappings</strong> to apply application roles based on attributes of a user's Identity Token.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I recommend using Group Mappings instead of a sync. Timed synchronizations often cause delays that could lead to issues, while group mappings are (more) immediate. The ODC Identity Bridge components make it easy to add any number of additional attributes to the Identity Token for automatic role assignment. Please read my article <a target="_self" href="https://without.systems/odc-end-user-roles-management-pattern">OutSystems Developer Cloud End-User Roles, Groups and Group Mappings</a> for details on how to configure Group Mappings.</div>
</div>

<p>Please refer to the OutSystems Documentation <a target="_blank" href="https://success.outsystems.com/documentation/outsystems_developer_cloud/user_management/managing_authorization_and_authentication_for_end_users/">Managing authorization and authentication for end-users - ODC Documentation</a> for detailed instructions on adding external identity provider sign-in to applications.</p>
<h2 id="heading-odc-identity-bridge">ODC Identity Bridge</h2>
<p>Let us look at some of the implementation details of the ODC Identity Bridge application. In Service Studio open the OpenIDBridgeAPI module.</p>
<h2 id="heading-service-actions">Service Actions</h2>
<p>ODC Identity Bridge exposes two service actions that are used in the sample Authorize screen in the OpenIDBridge web module.</p>
<p><strong>Bridge_ValidateAuthorizationRequest</strong></p>
<p>Validates the parameters that are sent by the ODC Identity Broker when redirecting a user to the Authorize endpoint (your Login screen in OutSystems 11). This action returns validation errors that are useful to troubleshoot the integration.</p>
<p><strong>Bridge_AuthorizeUser</strong></p>
<p>This service action is executed after a user submits its credentials in the Authorize screen of the OpenIDBridge web module.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770528516563/1ed51295-ff66-4887-87aa-4cd5ae463695.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>User_Login</strong> - Performs a username/password login using the User_Login action of the OutSystems 11 user provider.</p>
</li>
<li><p><strong>AuthCode</strong> - Generates a random authorization code.</p>
</li>
<li><p><strong>AuthorizationRequest_Create</strong> - Saves the authorization code along with the user identifier and additional authentication request details to the database.</p>
</li>
<li><p><strong>CallbackUri</strong> - Constructs the URL to which the browser must be redirected. This URL request includes the authorization code.</p>
</li>
</ul>
<h2 id="heading-discovery-endpoint">Discovery Endpoint</h2>
<p>In <strong>Logic - Integrations - REST - Oauth</strong> inspect the exposed Discovery operation. This endpoint serves an OpenID Connect Discovery Document that looks like this</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"issuer"</span>: <span class="hljs-string">"https://&lt;your environment&gt;"</span>,
    <span class="hljs-attr">"authorization_endpoint"</span>: <span class="hljs-string">"https://&lt;your environment&gt;/OpenIDBridge/Authorize"</span>,
    <span class="hljs-attr">"token_endpoint"</span>: <span class="hljs-string">"https://&lt;your environment&gt;/OpenIDBridgeAPI/rest/Oauth/Token"</span>,
    <span class="hljs-attr">"token_endpoint_auth_methods_supported"</span>: [
        <span class="hljs-string">"client_secret_post"</span>
    ],
    <span class="hljs-attr">"jwks_uri"</span>: <span class="hljs-string">"https://&lt;your environment&gt;/OpenIDBridgeAPI/rest/Oauth/Keys"</span>,
    .... more options
}
</code></pre>
<p>The action flow simply builds the values for the discovery document. The important ones are:</p>
<ul>
<li><p><strong>authorization_endpoint</strong> - This is the full URL to your OutSystems 11 Login Page. This value must be changed if you roll your own Login screen.</p>
</li>
<li><p><strong>token_endpoint</strong> - This endpoint is used by the ODC Identity Broker to exchange an authorization code for ID and access tokens.</p>
</li>
<li><p><strong>jwks_uri</strong> - This endpoint provides the public key used by the ODC Identity Bridge to sign both ID and access tokens.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The Discovery document informs ODC where to locate the individual endpoints. The discovery URL is the first thing you configure when adding a new Identity Provider in ODC Studio.</div>
</div>

<h2 id="heading-token-endpoint">Token Endpoint</h2>
<p>Next, examine the <strong>Token</strong> operation. This endpoint is used by the ODC Identity Broker to exchange an authorization code for ID and access tokens. After the user successfully signs in, the authorization code is sent to the RedirectURI of the configured Identity Provider in ODC Studio as part of the query string.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770526564690/c01b8c60-a936-41ba-8ac0-b7f1f1c5dfa5.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>ParseCodeExchangeForm</strong> - The Token endpoint uses a URL-encoded form, and this action converts the form content into a structured format.</p>
</li>
<li><p><strong>GetAuthorizationRequestByCode</strong> - Retrieves the authentication details saved during user login (<strong>Bridge_AuthorizeUser</strong>)</p>
</li>
<li><p><strong>CreateIdentityToken</strong> - Creates and signs a JWT Identity Token using the popular JWT component.</p>
</li>
<li><p><strong>CreateAccessToken</strong> - Even though ODC only uses the Identity token to authenticate a user, an access token must always be returned. This action creates and signs a JWT Access Token.</p>
</li>
</ul>
<h2 id="heading-keys">Keys</h2>
<p>This operation serves the public key that matches the private key used to sign tokens from <strong>Data - Resources - Keys</strong> and returns it. See <strong>Roll Your Own Keys</strong> for details on how to exchange the signing key.</p>
<h2 id="heading-adding-additional-claims">Adding Additional Claims</h2>
<p>To use ODC's Group Mapping, you can add extra key-value pairs (claims) to the Identity Token. Modify the <strong>CreateIdentityToken</strong> and add additional claims to the <strong>LocalClaims</strong> local variable.</p>
<h2 id="heading-roll-your-own-keys">Roll Your Own Keys</h2>
<p>ODC Identity Bridge has already included a public/private key pair to sign identity and access tokens, but you should replace the default with your own keys as soon as possible.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The following commands use OpenSSL. On Windows, you can use the <a target="_self" href="https://slproweb.com/products/Win32OpenSSL.html">Shining Light OpenSSL</a> distribution.</div>
</div>

<p>Run the following commands create a private and public key in PEM format.</p>
<pre><code class="lang-bash">openssl genrsa -out private.pem 2048
openssl rsa -<span class="hljs-keyword">in</span> private.pem -pubout -out public.pem
</code></pre>
<p>Next, you need to convert the public key (public.pem) to a JSON Web Key (JWK). The simplest way is to use an online converter like <a target="_blank" href="https://pem2jwk.vercel.app/">https://pem2jwk.vercel.app/</a> and save the JSON result to a file named public.jwk.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770528238208/9b54e53d-7701-4ec3-8b17-8712ddb22ada.png" alt class="image--center mx-auto" /></p>
<p>You can use any converter, but ensure that the resulting JSON document follows this structure:</p>
<pre><code class="lang-bash">{
    <span class="hljs-string">"keys"</span>: [
        {
            <span class="hljs-string">"kty"</span>: <span class="hljs-string">"RSA"</span>,
            <span class="hljs-string">"n"</span>: <span class="hljs-string">"&lt;n&gt;"</span>,
            <span class="hljs-string">"e"</span>: <span class="hljs-string">"AQAB"</span>,
            <span class="hljs-string">"ext"</span>: <span class="hljs-literal">true</span>,
            <span class="hljs-string">"kid"</span>: <span class="hljs-string">"o11public"</span>,
            <span class="hljs-string">"alg"</span>: <span class="hljs-string">"RS256"</span>,
            <span class="hljs-string">"use"</span>: <span class="hljs-string">"sig"</span>
        }
    ]
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Important</strong>: Make sure to set the key name to <strong>o11public</strong>.</div>
</div>

<p>Finally, in <strong>Data - Resources - Keys</strong>, replace <code>private.pem</code> with your custom private key and the JWK conversion result with <code>public.jwk</code>.</p>
<h1 id="heading-summary">Summary</h1>
<p>By following this tutorial, you have successfully set up a working identity bridge between OutSystems 11 and OutSystems Developer Cloud.</p>
<p>You installed and configured <strong>ODC Identity Bridge</strong> in your O11 environment that exposes an <strong>OpenID Connect / OAuth 2.0 Identity Provider</strong>, and registered it in the ODC Portal. As a result, users from your existing OutSystems 11 user base can now sign in to ODC applications using their O11 credentials.</p>
<p>Thank you for reading. I hope you enjoyed it and that I've explained the important parts clearly. If not, please let me know 😊 Your feedback is greatly appreciated.</p>
<p>Follow me on LinkedIn to receive notifications whenever I publish something new.</p>
]]></content:encoded></item><item><title><![CDATA[Entra ID Proxy for OutSystems Developer Cloud]]></title><description><![CDATA[Developing applications with OutSystems Developer Cloud that integrate with the Microsoft Graph API is straightforward if you use application permissions. In your action flow, you post your Application (client) Id, Secret, and required scopes to the ...]]></description><link>https://without.systems/entra-id-proxy-for-outsystems-developer-cloud</link><guid isPermaLink="true">https://without.systems/entra-id-proxy-for-outsystems-developer-cloud</guid><category><![CDATA[outsystems]]></category><category><![CDATA[Entra ID]]></category><category><![CDATA[OAuth2]]></category><category><![CDATA[OpenID Connect]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Mon, 24 Nov 2025 15:13:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763490905435/eaaf4b3a-b373-4567-9992-4ddf894634d6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Developing applications with OutSystems Developer Cloud that integrate with the <a target="_blank" href="https://learn.microsoft.com/en-us/graph/overview">Microsoft Graph API</a> is straightforward if you use <strong>application permissions</strong>. In your action flow, you post your <strong>Application (client) Id</strong>, <strong>Secret</strong>, and required <strong>scopes</strong> to the Token endpoint to get an access token. Then, you <strong>use that access token to interact with Graph API resources</strong>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Application Permissions -</strong> Allow an app to act <strong>on its own</strong>, without a user. The app gets organization-wide access to resources defined by the permission and requires <strong>admin consent</strong>. This is commonly used for background services or daemons.</div>
</div>

<p>However, in many situations, you want your application to interact with the Graph API <strong>on behalf of the logged-in user</strong>, allowing access only to the resources the user is allowed to use. Additionally, some Graph API resources <strong>are only accessible on behalf of a user</strong>. To access these, you need to perform an OpenID Authorization Code flow, where the user signs in to Entra using the browser to get an access token that represents the user's permissions.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Delegated Permissions -</strong> Allow an app to act <strong>on behalf of a signed-in user</strong>. The app can only access resources that the user has permission to, and it requires user or admin consent. This is commonly used in interactive apps where a user is present.</div>
</div>

<p>In ODC, you can add Microsoft Entra as an Identity Provider, allowing users to sign in to your applications with their Entra account. <a target="_blank" href="https://success.outsystems.com/documentation/outsystems_developer_cloud/user_management/configure_authentication_with_external_identity_providers/">You can set it up in just a few minutes by</a>:</p>
<ul>
<li><p>Registering and configuring an application in your Microsoft Entra tenant.</p>
</li>
<li><p>Configuring a new Identity Provider in the ODC Portal using your registration details.</p>
</li>
<li><p>Enabling the Identity Provider in your applications as an additional or sole authentication method.</p>
</li>
</ul>
<p>But unfortunately, ODC does not let you retrieve the <strong>original Microsoft Entra access token</strong> needed to access Graph API resources. Instead, ODC uses the <strong>identity token</strong> from Microsoft Entra to map (or create) a user in your ODC environment. The original identity and access tokens retrieved from Entra are not kept.</p>
<p>One option is to start another <strong>Authorization Code flow</strong> in your application to request an access token. However, this isn't a great user experience. The user already signs in with their Entra credentials and then has to authenticate again just for you to get the access token.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">If you're interested in developing your own Authorization Code flow in OutSystems, please watch my webinar recording <a target="_self" href="https://youtu.be/2cSsg5ws1H4">Microsoft Graph API with OutSystems Delegated Permissions</a>. If your end-users sign in with an Identity Provider other than Entra, you will also find my article <a target="_self" href="https://itnext.io/acquire-and-link-multiple-oauth-tokens-to-outsystems-users-for-delegated-access-b2ba74ca78a0">Acquire and Link multiple OAuth Tokens to OutSystems users for delegated access | ITNEXT</a> helpful.</div>
</div>

<p>The other option, which is the focus of this article, is to implement an <strong>Entra ID proxy</strong>. This proxy acts as an Identity Provider for your applications, forwards requests to Microsoft Entra, and <strong>caches the tokens</strong> retrieved from Microsoft Entra for you to use.</p>
<p><strong>Entry ID Proxy</strong> is available on <strong>OutSystems Forge</strong> for OutSystems Developer Cloud.</p>
<h1 id="heading-entra-id-proxy-overview">Entra ID Proxy Overview</h1>
<p>Entra ID Proxy is an OutSystems application that needs to be set up as an <strong>Identity Provider</strong> in the ODC Portal. It provides three REST API Endpoints:</p>
<ul>
<li><p><strong>openid-configuration</strong> - This retrieves the original OpenID discovery document from your registered Entra application and changes the authorize and token endpoint values to point to the Proxy instead of the Microsoft Entra endpoints.</p>
</li>
<li><p><strong>authorize</strong> - This proxies authorize requests and redirects the user's browser to the Microsoft Entra authorize endpoint.</p>
</li>
<li><p><strong>token</strong> - This endpoint not only proxies requests to the Entra token endpoint but also caches the identity and access tokens from Microsoft Entra after the authorization code exchange.</p>
</li>
</ul>
<p>Let's take a high-level look at how the <strong>Entra ID Proxy</strong> interacts with an OutSystems application, the platform's Identity Management, and Microsoft Entra.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763652083075/388942e4-af9d-4652-88f0-972f647bf95e.png" alt="Entra ID Proxy Sequence Diagram" class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>ODC Identity Broker</strong> is the Identity Management component of OutSystems Developer Cloud. It manages Identity Providers and handles the entire authentication process for the platform and the applications you create.</div>
</div>

<h1 id="heading-prerequisites">Prerequisites</h1>
<p>After downloading the Entra ID Proxy application, you first need to register an application in your Entra tenant, as explained in the documentation: <a target="_blank" href="https://success.outsystems.com/documentation/outsystems_developer_cloud/user_management/configure_authentication_with_external_identity_providers/add_microsoft_entra_id_for_use_as_external_identity_provider/">Add Microsoft Entra ID for use as an external identity provider - ODC Documentation</a>.</p>
<h2 id="heading-entra-id-application">Entra ID Application</h2>
<p>Ensure you have assigned the following delegated permissions to your application registration:</p>
<ul>
<li><p><strong>openid</strong> - This scope allows OutSystems to retrieve the identity token used to map or create a user.</p>
</li>
<li><p><strong>email</strong> - Enables the <strong>email</strong> and <strong>email_verified</strong> claim in the identity token.</p>
</li>
<li><p><strong>profile</strong> - Adds general profile information, such as the name, to the identity token.</p>
</li>
<li><p><strong>offline_access</strong> - This scope lets the proxy request a refresh token along with the identity and access token. The Proxy application needs this token to automatically refresh the access token after it expires, without requiring the user to log in again.</p>
</li>
</ul>
<p>Additionally, add any extra Graph API permissions, such as <em>Mail.Read</em> and <em>Mail.Send</em>, for the resources you want to use. Your API permission screen should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763716656417/4ccbfb9b-ee2e-4bf0-89d2-1e84b4820540.png" alt class="image--center mx-auto" /></p>
<p>From your Entra application you need the following informations copied for the next steps</p>
<ul>
<li><p>Application (client) Id</p>
</li>
<li><p>Client Secret</p>
</li>
<li><p>Directory (tenant) Id</p>
</li>
<li><p>API / Permissions name of the additional delegated permissions you added</p>
</li>
</ul>
<h2 id="heading-entra-id-proxy-settings">Entra ID Proxy Settings</h2>
<p>Next, we will configure the Entra ID Proxy application settings. In the ODC Portal, select the Entra ID Proxy application and set up the following:</p>
<ul>
<li><p><strong>EntraTenantId</strong> - Directory (tenant) Id of your registered Entra application</p>
</li>
<li><p><strong>EntraScope</strong> - Space-delimited Graph API Scopes (<strong>not</strong> openid, email, profile, and offline_access)</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Unfortunately, at the time of writing this article, OutSystems does not accept <strong>scope names with a dot</strong> in them. Therefore, it is necessary to configure the Graph scopes here, and Entra ID Proxy will add them during the Authorization Code flow.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763718043793/bf7cb7f6-5d22-44b2-a735-3eb0943a07cb.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-identity-provider">Identity Provider</h2>
<p>Finally, we configure <strong>Entra ID Proxy</strong> as an Identity Provider.</p>
<p>For the Discovery endpoint, instead of using Microsoft Entra's discovery endpoint directly, you configure the <strong>Entra ID Proxy</strong> Discovery endpoint. The URL looks like this:</p>
<p><code>https://&lt;FQN of your stage&gt;/EntraIDProxy/rest/OAuth2/well-known/openid-configuration</code></p>
<ul>
<li><p><strong>Client ID</strong> - Paste the Application (client) Id value from the Entra application registration.</p>
</li>
<li><p><strong>Client Secret</strong> - Paste the generated client secret value from the Entra application registration.</p>
</li>
<li><p><strong>Scope Mapping</strong> - The scopes <strong>openid</strong>, <strong>email</strong>, and <strong>profile</strong> are already configured by default. Add another entry: <strong>offline_access</strong>.</p>
</li>
</ul>
<p>Leave all other values as they are and save the configuration. Then, assign the new Identity Provider to the stage and applications.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Remember to add the generated <em>Redirect URI</em> and <em>Logout URL</em> to your Entra application registration as explained in <a target="_self" href="https://success.outsystems.com/documentation/outsystems_developer_cloud/user_management/configure_authentication_with_external_identity_providers/add_microsoft_entra_id_for_use_as_external_identity_provider/">Add Microsoft Entra ID for use as an external identity provider - ODC Documentation</a>.</div>
</div>

<p>With all the prerequisites completed, you should now be able to sign in to applications using the newly configured Identity Provider.</p>
<h1 id="heading-retrieving-an-access-token">Retrieving an Access Token</h1>
<p>Entra ID Proxy provides two <strong>service actions</strong> that let you retrieve an access token:</p>
<ul>
<li><p><strong>Entra_GetAccessTokenByUpn</strong> - This returns an access token for a given Universal Principal Name, typically the user's email address.</p>
</li>
<li><p><strong>Entra_GetAccessTokenByObjectId</strong> - This returns an access token for a given Object Identifier, which is the unique identifier of a user object in Entra.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You might consider mapping the Object Identifier (<strong>oid</strong>) from Entra to the username in OutSystems Developer Cloud and, instead of mapping the email claim, map the Universal Principal Name (<strong>upn</strong>) to the email.</div>
</div>

<h2 id="heading-implementation">Implementation</h2>
<p>Both service actions call the server action <code>GetAccessTokenByUpnOrObjectId</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763878611710/bfef0894-0666-4c01-ad90-8d001bc907ea.png" alt class="image--center mx-auto" /></p>
<p>This action performs the following steps:</p>
<ul>
<li><p>Attempts to retrieve the cache entry from the Token entry using either the Universal Principal Name or Object Identifier. If no entry exists, the action returns with <code>IsSuccess = False</code>.</p>
</li>
<li><p>Deserializes the binary payload, which contains the access token and refresh token, of the found entry.</p>
</li>
<li><p>Depending on whether the access token is still valid or has expired, it either returns the access token directly from the cache or tries to refresh the token before returning it.</p>
</li>
</ul>
<h2 id="heading-retrieving-an-access-token-in-your-applications">Retrieving an Access Token in your Applications</h2>
<p>In your application create a wrapper server action <code>GetAccessToken</code> that performs the following steps.</p>
<ul>
<li><p>Lookup the user record from the User entity filtered to GetUserId().</p>
</li>
<li><p>Call the <strong>Entra_GetAccessTokenByUpn</strong> service action</p>
</li>
<li><p>Return the success indicator <strong>IsSuccess</strong> and the access token</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763879527200/f11eb8ae-16d4-4945-a5bf-11d149feb59c.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You should pass the <strong>IsSuccess </strong>indicator up to your frontend application. If it is False, trigger a Security Exception to start a new user sign-in flow.</div>
</div>

<h1 id="heading-remarks-on-the-token-store">Remarks on the Token Store</h1>
<p>The <strong>Token</strong> entity in <strong>Entra ID Proxy</strong> stores the following information</p>
<ul>
<li><p><strong>Audience</strong> - Extracted from aud claim of the access token.</p>
</li>
<li><p><strong>Issuer</strong> - Extracted from the iss claim of the access token. This identifies your application registration.</p>
</li>
<li><p><strong>IssuedOn</strong> - Extracted from the iat claim and specifies when the access token was issued.</p>
</li>
<li><p><strong>ExpiresOn</strong> - Extracted from the exp claim and specifies when the access token expires. By default Entra access tokens expire after 1 hour.</p>
</li>
<li><p><strong>Subject</strong> - Extracted from the sub claim. Unique identifier of the user or principal in Entra. Typically the same value as ObjectId, but not guaranteed.</p>
</li>
<li><p><strong>ObjectId</strong> - Extracted from the oid claim. Unique Identifier of the user or principal in Entra. Use this instead of Subject.</p>
</li>
<li><p><strong>Payload</strong> - Access and Refresh token as binary data.</p>
</li>
<li><p><strong>ClientId</strong> - Application (client) Id for your application registration.</p>
</li>
<li><p><strong>ClientSecret</strong> - Client Secret of your application registration.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Important</strong>: The version of <strong>Entra ID Proxy</strong> available on the Forge stores some sensitive data unencrypted in the <strong>Token </strong>entity, including the <strong>Client Secret</strong> and the <strong>access and refresh token</strong> values. In a production environment, I strongly recommend using either <a target="_self" href="https://without.systems/application-level-encryption-in-outsystems">application-level encryption</a> or moving this sensitive data to a secrets management system like <a target="_self" href="https://developer.hashicorp.com/vault">HashiCorp Vault</a>. I am using the latter.</div>
</div>

<h1 id="heading-summary">Summary</h1>
<p>In this article, I'm introducing my OutSystems Forge component, <strong>Entra ID Proxy</strong>. <strong>Entra ID Proxy</strong> acts as an Identity Provider proxy in the OutSystems Developer Cloud and stores access tokens retrieved from Microsoft Entra after user authentication. These access tokens can be used to interact with the Microsoft Graph API with delegated permissions, allowing your application to interact with the Graph API on behalf of a user and their permissions. The access token can also be used to get more user information through the user info endpoint or the User resource in the Graph API.</p>
<p>Thank you for reading. I hope you enjoyed it and that I've explained the important parts clearly. If not, please let me know 😊 Your feedback is greatly appreciated.</p>
<p>Follow me on LinkedIn to receive notifications whenever I publish something new.</p>
]]></content:encoded></item><item><title><![CDATA[RulesEspresso - A Simple Rules Engine for OutSystems Developer Cloud]]></title><description><![CDATA[RulesEspresso is one of the components I published on OutSystems Developer Cloud Forge. It is a simple yet powerful rules engine that lets you define and execute business rules at runtime. While this approach isn't suitable for every use case, it off...]]></description><link>https://without.systems/rulesespresso-rules-engine-for-outsystems-developer-cloud</link><guid isPermaLink="true">https://without.systems/rulesespresso-rules-engine-for-outsystems-developer-cloud</guid><category><![CDATA[rulesengine]]></category><category><![CDATA[outsystems]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Tue, 17 Jun 2025 06:38:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749968180871/74e05760-6ef9-4a04-a96d-b75e82f1476a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>RulesEspresso</strong> is one of the components I published on <strong>OutSystems Developer Cloud Forge</strong>. It is a simple yet powerful rules engine that lets you define and execute business rules at runtime. While this approach isn't suitable for every use case, it offers several advantages over embedding business rules directly in the application code. The biggest benefit is that you can change business rules without redeploying the application, unlike logic built in <strong>ODC Studio</strong>. You can even let an application user create or modify business rules.</p>
<p>Let's consider the following scenario.</p>
<p>You are asked to implement a simple product discount. You need to create a discount scale that offers a discount based on membership status.</p>
<ul>
<li><p>Bronze status members receive 2%</p>
</li>
<li><p>Silver status members receive 5%</p>
</li>
<li><p>Gold status members receive 10%</p>
</li>
</ul>
<p>Such a discount calculation can easily be implemented in ODC with an entity containing the discounts for each membership level. However, these discount calculations can change quickly over time, and changes could include:</p>
<ul>
<li><p>Discounts should not be applied if the product is already discounted.</p>
</li>
<li><p>A discount should only be applied if the product's value exceeds a certain amount.</p>
</li>
<li><p>A discount should only be applied if at least three items of the product are purchased.</p>
</li>
<li><p>or a combination of the above……</p>
</li>
</ul>
<p>Besides these permanent or semi-permanent changes, you might also encounter temporary changes, like additional discounts during a specific timeframe or even a complete change in the discount calculation if the company updates their membership program.</p>
<p>In short, implementing business rules directly within an application can become a burden and you may even risk losing money if you can't adapt to changes quickly enough. Externalizing business rules is the solution, as it allows you to define and modify business rules at runtime without changing server actions and redeploying the application.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Of course, even with the most advanced external rules definition, there is always a chance that you will still need to adjust the code. Business rules engines <strong>simply reduce the risk</strong> of needing to change the code.</div>
</div>

<h1 id="heading-demo-application"><strong>Demo Application</strong></h1>
<p>This article features a demo application called "<strong>RulesEspresso Demo</strong>," available on <strong>ODC Forge</strong>.</p>
<ul>
<li><p>In the ODC Portal, navigate to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>RulesEspresso Demo</strong>".</p>
</li>
<li><p>Click on <strong>Install</strong>.</p>
</li>
</ul>
<p>This demo relies on:</p>
<ul>
<li><strong>RulesEspresso</strong> - This external logic library is the rules engine implementation you can use in your own applications.</li>
</ul>
<h1 id="heading-rulesespresso-introduction">RulesEspresso Introduction</h1>
<p>RulesEspresso is a <strong>simple</strong> implementation of a business rules engine. At a glance it allows you to</p>
<ul>
<li><p>Provide a flat property <strong>JSON document</strong> containing data as the <strong>rules evaluation context</strong>.</p>
</li>
<li><p>Include an optional <strong>JSON Schema</strong> for data validation.</p>
</li>
<li><p>Define one or more rules using <strong>C# expressions</strong> to evaluate data conditions, returning either true or false.</p>
</li>
<li><p>Optional <strong>result C# expressions</strong> for each rule to add <strong>calculated properties to the rules evaluation context</strong> if the rule expression evaluates to true.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">While this approach is suitable for many use-case scenarios, it has its limitations. In more complex situations, you might want to consider integrating with a mature business rules products (BRMS) like <a target="_self" href="https://www.ibm.com/products/operational-decision-manager">IBM Operational Decision Manager</a> or <a target="_self" href="https://docs.redhat.com/en/documentation/red_hat_decision_manager">Redhat Decision Manager</a>.</div>
</div>

<h2 id="heading-parameters">Parameters</h2>
<p>RulesEspresso external logic library contains a single action <strong>EvaluateRules</strong> with the following input parameters</p>
<p><strong>dataObject</strong></p>
<p>This parameter accepts a JSON document with key/value pairs that can be used in rule definitions. It serves as the context for a single run of <strong>EvaluateRules</strong>. To generate the <strong>dataObject</strong> input, you can:</p>
<ul>
<li><p>Create a structure with attributes.</p>
</li>
<li><p>Use the structure as a local variable in a server action flow and assign values to it.</p>
</li>
<li><p>Serialize the variable using the <strong>JSON Serialize</strong> action - with <strong>Serialize Default Values</strong> set to <strong>Yes</strong>!.</p>
</li>
<li><p>Use the serialized result as the value for the <strong>dataObject</strong> input parameter.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">At the time of writing, RulesEspresso only supports flat JSON documents, which means you cannot use nested objects or arrays.</div>
</div>

<p><strong>ruleDefinitions</strong></p>
<p>This parameter accepts a list of individual rules. Each rule consists of:</p>
<ul>
<li><p><strong>RuleName</strong> - Any text. Each rule is evaluated separately, and you can use the rule name to identify it in the result of the Evaluate Rules action.</p>
</li>
<li><p><strong>EvaluationExpression</strong> - A boolean C# expression. This is where you specify a condition for the rule evaluation. Attributes of the dataObject are referred to by their names, and you can use C# methods within the conditions, such as DateTime.Now.</p>
</li>
<li><p><strong>ResultExpression</strong> - An optional C# expression that is evaluated if the EvaluationExpression is true. The ResultExpression can return a static or computed value, adding to the rules evaluation context, allowing you to use the value in subsequent rules.</p>
</li>
<li><p><strong>ResultPropertyName</strong> - Required if ResultExpression is set. This is the property name under which the result of ResultExpression should be added to the rules evaluation context.</p>
</li>
</ul>
<p><strong>jsonSchema</strong></p>
<p>Takes an optional JSON schema to validate the dataObject. This validation is done only once, applying only to the input parameter and not to any ResultExpression values added during rules evaluation. The JSON schema is an excellent way to ensure that all necessary input parameters for rules evaluation are provided and have the correct values.</p>
<h2 id="heading-example">Example</h2>
<p>Let us do an example. We want to pass the total value of a purchase busket, along with a membership status and calculate a discount based on that:</p>
<ul>
<li><p>Bronze status members receive 2%</p>
</li>
<li><p>Silver status members receive 5%</p>
</li>
<li><p>Gold status members receive 10%</p>
</li>
</ul>
<p>Our <strong>dataObject</strong> parameter looks like this</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"totalValue"</span>: <span class="hljs-number">7450.00</span>,
  <span class="hljs-attr">"memberStatus"</span>: <span class="hljs-string">"Silver"</span>
}
</code></pre>
<p>The <strong>ruleDefinitions</strong></p>
<p>Rule 1:</p>
<ul>
<li><p><strong>RuleName</strong>: IsBronze</p>
</li>
<li><p><strong>EvaluationExpression</strong>: memberStatus == “Bronze”</p>
</li>
<li><p><strong>ResultExpression</strong>: totalValue * 2 / 100</p>
</li>
<li><p><strong>ResultPropertyName</strong>: discount</p>
</li>
</ul>
<p>Rule 2:</p>
<ul>
<li><p><strong>RuleName</strong>: IsSilver</p>
</li>
<li><p><strong>EvaluationExpression</strong>: memberStatus == “Silver”</p>
</li>
<li><p><strong>ResultExpression</strong>: totalValue * 5 / 100</p>
</li>
<li><p><strong>ResultPropertyName</strong>: discount</p>
</li>
</ul>
<p>Rule 3:</p>
<ul>
<li><p><strong>RuleName</strong>: IsGold</p>
</li>
<li><p><strong>EvaluationExpression</strong>: memberStatus == “Gold”</p>
</li>
<li><p><strong>ResultExpression</strong>: totalValue * 10 / 100</p>
</li>
<li><p><strong>ResultPropertyName</strong>: discount</p>
</li>
</ul>
<p>These rules evaluate the value of memberStatus and calculate the discount.</p>
<p>Finally, we are adding a <strong>jsonSchema</strong> to ensure that memberStatus is either Bronze, Silver, or Gold, and that totalValue is greater than 0.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"https://json-schema.org/draft/2020-12/schema"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
  <span class="hljs-attr">"properties"</span>: {
    <span class="hljs-attr">"totalValue"</span>: {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"number"</span>,
      <span class="hljs-attr">"exclusiveMinimum"</span>: <span class="hljs-number">0</span>
    },
    <span class="hljs-attr">"memberStatus"</span>: {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
      <span class="hljs-attr">"enum"</span>: [<span class="hljs-string">"Bronze"</span>, <span class="hljs-string">"Silver"</span>, <span class="hljs-string">"Gold"</span>]
    }
  },
  <span class="hljs-attr">"required"</span>: [<span class="hljs-string">"totalValue"</span>, <span class="hljs-string">"memberStatus"</span>],
  <span class="hljs-attr">"additionalProperties"</span>: <span class="hljs-literal">false</span>
}
</code></pre>
<p>You can try it out in the demo application and the Last Evaluation Result should look like this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750141311713/22e26841-31d6-413d-a447-879ae416642e.png" alt class="image--center mx-auto" /></p>
<p>Change the values and add some extra rule expressions.</p>
<h2 id="heading-result">Result</h2>
<p>After evaluation, the results are returned in the <strong>ValidationResult</strong> output parameter:</p>
<ul>
<li><p><strong>IsValid</strong> - This boolean value is true if all rules evaluated to true.</p>
</li>
<li><p><strong>Document</strong> - Contains the rules evaluation context JSON document, including all added result properties.</p>
</li>
<li><p><strong>RuleResults</strong> - An array of individual rule results. There is one entry for each rule evaluated, with the RuleName and a boolean IsValid property indicating whether the rule evaluated to true. You can use these individual entries to selectively take action within an action flow based on the result of a rule.</p>
</li>
</ul>
<h1 id="heading-summary">Summary</h1>
<p>This article introduces the <strong>RulesEspresso</strong> external logic library for OutSystems Developer Cloud. <strong>RulesEspresso</strong> is a simple rules engine that lets you externalize business rules instead of hard-coding them into OutSystems applications. While it handles many use cases, for more complex scenarios, you might want to consider a Business Rules Management Solution. These solutions offer many extra features, including custom code integration and enterprise-level business rules evaluation, unlike <strong>RulesEspresso</strong>, which is designed for application-centric rules evaluation.</p>
<p>I hope you enjoyed it and would appreciate your feedback.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[Declarative Forms with OutSystems Developer Cloud]]></title><description><![CDATA[Building forms in OutSystems is incredibly fast. You simply define a structure, add a form element to the canvas, and drag and drop the individual fields into the form. This is excellent for static forms linked to data structures, but only developers...]]></description><link>https://without.systems/declarative-forms-with-outsystems-developer-cloud</link><guid isPermaLink="true">https://without.systems/declarative-forms-with-outsystems-developer-cloud</guid><category><![CDATA[outsystems]]></category><category><![CDATA[dynamic forms]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Mon, 16 Jun 2025 09:23:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749565593722/3eda2708-3efb-4bea-926c-704307659854.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building forms in OutSystems is incredibly fast. You simply define a structure, add a form element to the canvas, and drag and drop the individual fields into the form. This is excellent for static forms linked to data structures, but only developers can create these forms, and after they're built and tested, the application must be redeployed to production.</p>
<p>In some situations, you might need to build a form dynamically, along with its data model, or even let an end-user define a form and its elements while using the application. Assessments or surveys are good examples of such user-defined forms. Additionally, you could consider dynamically creating a form from an AI model operation result.</p>
<p>In this article, we explore how to implement my <strong>Runtime Forms</strong> component available on ODC Forge. The library allows you to dynamically describe and render a form. This approach is also called declarative forms, where you specify <em>what</em> the form should look like and <em>how</em> it should behave in a configuration (in this case, a structure), instead of placing individual input elements on the canvas like with static forms.</p>
<h1 id="heading-demo-application">Demo Application</h1>
<p>This article includes a demo application called "<strong>Runtime Forms Demo</strong>," available on <strong>ODC Forge</strong>.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>Runtime Forms Demo</strong>".</p>
</li>
<li><p>Click on <strong>Install.</strong></p>
</li>
</ul>
<p>This demo depends on:</p>
<ul>
<li><strong>Runtime Forms</strong> - This is the main library you can use and customize for your own applications. It offers a single widget to render a declarative form.</li>
</ul>
<h1 id="heading-runtime-forms-introduction">Runtime Forms Introduction</h1>
<p>The library you downloaded from ODC Forge includes a single UI widget for rendering a declarative form. Think of it as a starting point that you can improve and customize for your own needs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749634527687/b3b44795-981b-424e-9599-cd5d4ee771b6.png" alt class="image--center mx-auto" /></p>
<p>It supports the following features:</p>
<ul>
<li><p>Renders various input elements based on parameters (<strong>Fields</strong>) that describe them.</p>
</li>
<li><p>Populates data (<strong>Data</strong>) into input elements from a JSON document.</p>
</li>
<li><p>Validates the form on submission using HTML5 validation attributes.</p>
</li>
<li><p>Sends form data as a JSON document upon submission (<strong>OnSubmit</strong>).</p>
</li>
</ul>
<h2 id="heading-supported-form-elements">Supported Form Elements</h2>
<p>The form elements you can use with the widget are limited to the following input types:</p>
<p><strong>Text Inputs</strong></p>
<ul>
<li><p><strong>Text</strong> - A single-line text field. This is the default value for an input element.</p>
</li>
<li><p><strong>Textarea</strong> - A multi-line text field.</p>
</li>
<li><p><strong>Password</strong> - A single-line text field where the input is obscured to protect sensitive information.</p>
</li>
<li><p><strong>Email</strong> - A field specifically for entering one or more email addresses. Validation ensures that the text entered is a valid e-mail address.</p>
</li>
<li><p><strong>Search</strong> - A single-line text field for entering search queries. It may include a control to clear the entered text.</p>
</li>
<li><p><strong>Tel</strong> - A field for entering a telephone number. Unlike email, it doesn't enforce a specific syntax, but it may trigger a numeric keypad on mobile devices.</p>
</li>
<li><p><strong>Url</strong> - A field for entering a URL. The browser may provide validation to ensure the text is in a valid URL format.</p>
</li>
</ul>
<p><strong>Date and Time Inputs</strong></p>
<ul>
<li><p><strong>date</strong> - A control for entering a date (year, month, and day).</p>
</li>
<li><p><strong>time</strong> - A control for entering a time (hour and minute).</p>
</li>
<li><p><strong>datetime-local</strong> - A control for entering both a date and time, without time zone information.</p>
</li>
<li><p><strong>month</strong> - A control for entering a month and year.</p>
</li>
<li><p><strong>week</strong> - A control for entering a week and year.</p>
</li>
</ul>
<p><strong>Numeric Inputs</strong></p>
<ul>
<li><strong>number</strong> - A field for entering a number.</li>
</ul>
<p><strong>Selection Inputs</strong></p>
<ul>
<li><p><strong>radio</strong> - A radio button that allows the user to select only one option from a group of choices.</p>
</li>
<li><p><strong>checkbox</strong> - A checkbox that allows the user to enable or disable.</p>
</li>
</ul>
<p>All input elements are displayed in the order specified by the <strong>Fields</strong> input parameter of the widget.</p>
<h2 id="heading-field-configurations-and-validations">Field Configurations and Validations</h2>
<p>Input field configurations and validations are divided into two parts: a common set of configuration elements that apply to all supported form elements, and type-specific settings that apply only to individual form element types.</p>
<p><strong>Common</strong></p>
<p>The following settings apply to all types:</p>
<ul>
<li><p><strong>Name</strong> - The <strong>name</strong> attribute of the form element. This name also becomes the attribute name of the JSON data document you receive when you submit the form.</p>
</li>
<li><p><strong>Type</strong> - The input element's <strong>type</strong> attribute. The <strong>FieldType</strong> static entity provides the supported element types as described above.</p>
</li>
<li><p><strong>Label</strong> - If present, the widget adds an extra <code>&lt;label&gt;</code> tag for the form element.</p>
</li>
<li><p><strong>Text</strong> - If present, this text is displayed right above the form element. You can use it to add extra descriptions or guidance for the form element.</p>
</li>
<li><p><strong>Placeholder</strong> - If present, it sets the <strong>placeholder</strong> attribute of the form element.</p>
</li>
<li><p><strong>IsRequired</strong> - If set to <strong>True</strong>, it adds the <strong>required</strong> attribute to the form element, making user input necessary.</p>
</li>
<li><p><strong>IsDisabled</strong> - If set to <strong>True</strong>, it adds the <strong>disabled</strong> attribute to the form element. When the form is submitted, data from disabled elements is excluded.</p>
</li>
<li><p><strong>IsReadOnly</strong> - If set to <strong>True</strong>, it adds the <strong>readonly</strong> attribute to the form element. Data from readonly elements is included when the form is submitted.</p>
</li>
<li><p><strong>HasAutoFocus</strong> - Sets the <strong>autofocus</strong> attribute for the form element. Ensure that only one element has <strong>autofocus</strong>.</p>
</li>
<li><p><strong>TabIndex</strong> - Defaults to 0. When set, this configures the tab order of the element.</p>
</li>
</ul>
<p><strong>Type-specific</strong></p>
<p>Type-specific settings are found in the <strong>Config</strong> object. Depending on the chosen input type, the following settings apply.</p>
<p>For <strong>text</strong>, <strong>textarea</strong>, <strong>password</strong>, <strong>email</strong>, <strong>search</strong>, <strong>tel</strong> and <strong>url</strong> the following settings apply:</p>
<ul>
<li><p><strong>MinLength</strong> - Specifies the minimum number of characters the user must enter for the input to be considered valid.</p>
</li>
<li><p><strong>MaxLength</strong> - Specifies the maximum number of characters the user can enter into the input field.</p>
</li>
<li><p><strong>Pattern</strong> - Specifies a regular expression that the input field's value is checked against. It allows for complex validation rules.</p>
</li>
<li><p><strong>Autocomplete</strong> - Specifies whether a browser should automatically complete the input based on values the user has entered previously. Can be set to "on" or "off".</p>
</li>
<li><p><strong>Rows (only applicable to textarea)</strong> - The number of rows of the multi-line text input element.</p>
</li>
</ul>
<p>For <strong>date</strong>, <strong>time</strong>, <strong>datetime-local</strong>, <strong>month</strong> and <strong>week</strong>:</p>
<ul>
<li><p><strong>Min</strong> - Specifies the earliest date/time/month/week that can be selected.</p>
</li>
<li><p><strong>Max</strong> - Specifies the latest date/time/month/week that can be selected.</p>
</li>
</ul>
<p>For <strong>number</strong>:</p>
<ul>
<li><p><strong>Min</strong> - Specifies the minimum allowed value for the input.</p>
</li>
<li><p><strong>Max</strong> - Specifies the maximum allowed value for the input.</p>
</li>
<li><p><strong>Step</strong> - Specifies the legal number intervals (e.g., <code>step="2"</code> allows 0, 2, 4...). <code>any</code> allows any value.</p>
</li>
</ul>
<p>For <strong>radio</strong> and <strong>checkbox</strong>:</p>
<ul>
<li><p><strong>IsChecked</strong> - A boolean attribute that specifies if a <code>checkbox</code> or <code>radio</code> button should be selected by default when the page loads.</p>
</li>
<li><p><strong>Options (Radio only)</strong> - A list of Label/Value pairs for the radio options to select from.</p>
</li>
</ul>
<p>Most of the settings above are related to validation, and the library uses the browser's built-in form validation. You can read more about this client-side <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Form_validation">form validation on MDN</a>.</p>
<h2 id="heading-applying-and-submitting-data">Applying and Submitting Data</h2>
<p>The Data input parameter lets you add a JSON document as a string. The library attempts to apply the attribute values to the corresponding input elements based on the <strong>name</strong>.</p>
<p>Consider the following form:</p>
<ul>
<li><p>A text field named <strong>fullName</strong></p>
</li>
<li><p>A checkbox field named <strong>isMember</strong></p>
</li>
<li><p>A radio field <strong>shirtSize</strong> with the Label/Value pairs <code>[Small - S]</code>, <code>[Large - L]</code>, and <code>[X-Large - XL]</code></p>
</li>
</ul>
<p>The JSON document to apply all values would look like this:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"fullName"</span>: <span class="hljs-string">"Stefan Weber"</span>,
  <span class="hljs-attr">"isMember"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"shirtSize"</span>: <span class="hljs-string">"XL"</span>
}
</code></pre>
<p>Similarly, when you submit the form, you will receive an equivalent JSON document as the <strong>OnSubmit</strong> payload.</p>
<h2 id="heading-library-limits">Library Limits</h2>
<p>By the time of writing the Runtime Forms library does not support</p>
<ul>
<li><p>Validations for Radio buttons.</p>
</li>
<li><p>Conditional visibility of form elements.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">If you plan to use this library and need more features, please write a post on the OutSystems Forum.</div>
</div>

<h1 id="heading-demo-walkthrough">Demo Walkthrough</h1>
<p>Let's take a closer look at how this works. In <strong>ODC Studio</strong>, open the <strong>Runtime Forms Demo</strong> application.</p>
<p>In the <strong>Logic</strong> tab, open the <strong>BuildSampleForm</strong> server action. This action is designed to create individual form elements using the <strong>FieldDefinition</strong> structure. It returns a list of <strong>FieldDefinition</strong> that serve as input parameters for the library's <strong>RuntimeForm</strong> widget.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749726674780/4346e143-423d-4925-8970-8a4b8157933b.png" alt class="image--center mx-auto" /></p>
<p>Check the various configurations of the field elements.</p>
<p>In the <strong>Interface</strong> tab open the <strong>Run</strong> screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749726794208/fde86953-3837-4e56-b7aa-60ec503faac9.png" alt class="image--center mx-auto" /></p>
<p>The <strong>GetFormDefinition</strong> data action executes the <strong>BuildSampleForm</strong> server action. The result of the data action is bound to the <strong>Fields</strong> input parameter of the <strong>RuntimeForm</strong> widget.</p>
<p>The local <strong>DataEntry</strong> variable is linked to the <strong>JSON Data</strong> textarea on the right side of the screen. When you click the <strong>Apply Data</strong> button (triggering the <strong>ApplyDataOnClick</strong> client action), the content of this variable is assigned to the <strong>DataSubmitted</strong> variable, which is bound to the Data input parameter of the widget.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The above is necessary to prevent rerenders of the form elements while typing JSON data. Please also note that the <strong>RuntimeForm </strong>widget is wrapped in an <strong>If </strong>and only rendered after the <strong>GetFormDefinition </strong>action ran.</div>
</div>

<p>When running the demo you will see the following screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749727350532/bdf8845f-4361-4954-83c3-2f32b872b29b.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Hit the <strong>Apply Data</strong> button for applying the provided JSON data to the form.</p>
</li>
<li><p>Leave the <strong>Company Mail Address</strong> empty and click the <strong>Submit</strong> button. Note the validation error message below the mail address.</p>
</li>
<li><p>Enter a invalid value into the mail address field e.g. <code>me@</code> and note the validation message.</p>
</li>
<li><p>Fill in all required fields and click the <strong>Submit</strong> button. Note the JSON data in the <strong>Last Submitted JSON data</strong> area.</p>
</li>
<li><p>Add and modify Field definitions in the <strong>BuildSampleForm</strong> server action.</p>
</li>
</ul>
<h1 id="heading-library-modifications">Library Modifications</h1>
<p>As mentioned above, you may need to adjust the library to fit your needs, such as adding custom client-side validations or additional form elements. Take a look at the <strong>RuntimeForms</strong> library, especially the following:</p>
<p><strong>RuntimeForm</strong> widget</p>
<ul>
<li><p><strong>ValidateForm</strong> client action - Validates all non-disabled field elements. If you want to customize or extend browser-based validation, this is where you can do it.</p>
</li>
<li><p><strong>PopulateFormData</strong> client action - Fills the individual field elements with the provided data.</p>
</li>
<li><p><strong>SubmitOnClick</strong> - Triggers the OnSubmit event with the form data as the payload.</p>
</li>
</ul>
<p><strong>RuntimeFormField</strong></p>
<ul>
<li><strong>ApplyFieldConfiguration</strong> client action - This action applies type-specific field attributes to the elements (e.g., required, minLength, etc.).</li>
</ul>
<h1 id="heading-summary">Summary</h1>
<p>This article introduces the <strong>Runtime Forms</strong> library, available on ODC Forge, which enables dynamic form creation in OutSystems Developer Cloud applications. Unlike static forms, <strong>Runtime Forms</strong> allow developers or even end-users to define and render forms on-the-fly using a declarative approach. The component supports various input types and validations.</p>
<p>I hope you enjoyed it and would appreciate your feedback.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[Access Control Lists for Graph Data Items]]></title><description><![CDATA[In Part 1, we synchronized OutSystems application data with Microsoft Search & Intelligence using the Microsoft Graph External Data Connections API. This, along with a search vertical and result display template, made our data available to users in t...]]></description><link>https://without.systems/odc-with-graph-ingested-data-access-control</link><guid isPermaLink="true">https://without.systems/odc-with-graph-ingested-data-access-control</guid><category><![CDATA[outsystems]]></category><category><![CDATA[Microsoft Graph]]></category><category><![CDATA[Microsoft 365]]></category><category><![CDATA[Microsoft Search]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Tue, 03 Jun 2025 13:15:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747645397100/6abc4715-c4e2-433f-9c33-3c398e1c0965.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In Part 1, we synchronized OutSystems application data with Microsoft Search &amp; Intelligence using the Microsoft Graph External Data Connections API. This, along with a search vertical and result display template, made our data available to users in the search console.</p>
<p>During synchronization, we gave permission to the ingested data records to the "everyone" group, allowing all organization users to search and view the data. In a real application, you will likely need to restrict who can search and view the ingested data. In this tutorial, we will explore how to manage access control lists for ingested data records to ensure that only authorized users have access.</p>
<h1 id="heading-demo-application"><strong>Demo Application</strong></h1>
<p>This article series includes a demo application called "<strong>ODC with Graph Demo</strong>," available on <strong>ODC Forge</strong>. Be sure to download the version of the application that matches this article.</p>
<p>For this article, you need to install Version 0.2 from ODC Forge.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>ODC with Graph Demo</strong>".</p>
</li>
<li><p>Click on the Asset <strong>(Do not click on Install on the tile!)</strong>.</p>
</li>
<li><p>Switch to the <strong>Version</strong> tab and click on Install next to <strong>Version 0.2</strong>.</p>
</li>
</ul>
<p>Version 0.2 depends on other Forge components:</p>
<ul>
<li><p><strong>OAuthTokenExchange</strong> - An external logic library that helps retrieve access tokens easily. In this tutorial however we use an action to retrieve the discovery document from Microsoft Entra only.</p>
</li>
<li><p><strong>Graph External Data Connections API</strong> - A connector library that integrates with Microsoft Graph API for external data connections.</p>
</li>
<li><p><strong>Graph Users API</strong> - A connector library that integrates with Microsoft Graph API users endpoint to retrieve user details.</p>
</li>
</ul>
<h1 id="heading-what-we-build">What we build</h1>
<p>In this part of the tutorial series, we will add specific access control list entries to ingested testimonials, allowing access only to designated users. This involves the following steps:</p>
<ul>
<li><p><strong>Mapping User Accounts</strong> - Map OutSystems application users to Microsoft Entra users.</p>
</li>
<li><p><strong>Access Control List</strong> - Add access control list entries during data ingestion to Microsoft Graph.</p>
</li>
</ul>
<h1 id="heading-prerequisites">Prerequisites</h1>
<p>Before we begin, please reset the existing external data connection configurations in your Microsoft 365 tenant by following the <a target="_blank" href="https://without.systems/ingesting-outsystems-data-into-microsoft-graph#heading-cleanup">cleanup steps</a> and recreating the external data connection schema as described in part one of the series.</p>
<h2 id="heading-entra-application-permissions">Entra Application Permissions</h2>
<p>Our application registration we are using to ingest data requires one additional permission to retrieve Entra user details. In the <a target="_blank" href="https://entra.microsoft.com/"><strong>Entra admin center</strong></a>, <a target="_blank" href="https://entra.microsoft.com/">go to <strong>Application</strong></a><strong>s - App Registrations</strong> and select the “Graph External Data Connection ODC” registration.</p>
<p>In the <strong>API permissions</strong> menu, click <strong>Add permission</strong>.</p>
<ul>
<li><p>Select <strong>Microsoft Graph</strong> and <strong>Application permission</strong>.</p>
</li>
<li><p>Search for and select <strong>User.ReadBasic.All</strong> permission.</p>
</li>
<li><p>Click <strong>Add permission</strong>.</p>
</li>
</ul>
<p>This permission requires an administrator to grant admin consent.</p>
<ul>
<li>Click <strong>Grant admin consent &lt;your domain&gt;</strong> and confirm the dialog.</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Instead of <strong>User.ReadBasic.All</strong>, you can also select<strong> User.Read.All</strong> permission, which grants access to all user details, whereas <strong>ReadBasic </strong>only allows retrieval of limited user attributes. However, for the demo, this is sufficient.</div>
</div>

<p>Before we try the demo application, let's explore Access Control List entries for external data in Microsoft Graph a bit more in depth.</p>
<h1 id="heading-access-control-list-entries">Access Control List Entries</h1>
<p>Access Control List entries in Microsoft Graph for external data specify who can access certain ingested data records and the level of access they have.</p>
<p>Entries are defined by a <strong>type</strong> attribute that identifies the identity you want to grant permissions to. The type can have one of the following values:</p>
<ul>
<li><p><strong>user</strong> - Indicates that you want to grant permission to an individual Entra user account.</p>
</li>
<li><p><strong>group</strong> - Indicates that you want to grant permission to an individual Entra group.</p>
</li>
<li><p><strong>externalGroup</strong> - A non-Entra group that your application manages. We will discuss external groups shortly.</p>
</li>
<li><p><strong>everyone</strong> - A special type that indicates public access.</p>
</li>
<li><p><strong>application</strong> - Grants permission to a specific Entra application registration.</p>
</li>
</ul>
<p>Next, a <strong>value</strong> must be provided that corresponds to the type you specified:</p>
<ul>
<li><p><strong>user</strong> - The Entra user's object identifier.</p>
</li>
<li><p><strong>group</strong> - The Entra group's object identifier.</p>
</li>
<li><p><strong>externalGroup</strong> - The external group identifier.</p>
</li>
<li><p><strong>everyone</strong> - Must be set to “everyone”.</p>
</li>
<li><p><strong>application</strong> - The Application (client) ID of the Entra application registration.</p>
</li>
</ul>
<p>Finally, the <strong>accessType</strong> attribute defines the level of permission:</p>
<ul>
<li><p><strong>grant</strong> - Grants access to the record.</p>
</li>
<li><p><strong>deny</strong> - Denies access to the record. Deny Access Control List entries take precedence over grant entries.</p>
</li>
</ul>
<p>A sample JSON representation of a single Access Control List entry that grants a specific Entra user access to a record looks like this.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"accessType"</span>: <span class="hljs-string">"grant"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"user"</span>,
  <span class="hljs-attr">"value"</span>: <span class="hljs-string">"381a61cf-7d4e-40a3-a225-83c5bc36fbf0"</span>
}
</code></pre>
<p>A Graph ingested data record can have multiple Access Control List entries with different types.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It's important to note that Access Control List entries must always be included in the <strong>Graph_CreateOrUpdateItem</strong> operation, even when you're just updating an item. There is no separate way of managing ACLs for items.</div>
</div>

<h1 id="heading-mapping-user-accounts">Mapping User Accounts</h1>
<p>Microsoft 365 users are authenticated through Microsoft Entra, and Entra does not automatically recognize OutSystems user accounts. Therefore, to manage authorizations, we need to identify which Entra user account matches an OutSystems user.</p>
<p>Even if OutSystems users log in through Entra, we, as of today, cannot directly access the login identification data within our application. The only information we have is the user data from the <strong>User</strong> entity. This means we have to use the <strong>user's email address</strong> to identify the corresponding Entra user.</p>
<p>There are two ways to do this. If the email address of an OutSystems user matches the <strong>Universal Principal Name (UPN)</strong> of the Entra user, we can easily get the Entra user details using the <strong>Graph_GetUser</strong> action from the <strong>Graph Users API</strong> connector library.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In Microsoft Entra, the <strong>User Principal Name (UPN)</strong> is the unique sign-in name for a user and is formatted like an email address.</div>
</div>

<p>If the OutSystems user's email address does not match Entra's UPN, you can use the <strong>Graph_ListUsers</strong> action from the <strong>Graph Users API</strong> connector library. This allows you to filter for the user whose mail attribute matches the OutSystems user's email address. The filter would look like this</p>
<pre><code class="lang-plaintext">mail eq 'mail@domain.com'
</code></pre>
<p>Read more on <a target="_blank" href="https://learn.microsoft.com/en-us/graph/filter-query-parameter">OData filters</a> in the Microsoft Documentation.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The demo application includes a test screen where you can try out <strong>filter </strong>and <strong>search </strong>criteria.</div>
</div>

<h1 id="heading-external-groups">External Groups</h1>
<p>External Groups represent group entities from systems outside of Microsoft Entra. These groups are used to reflect the access control structures of external data sources.</p>
<p>When developing an OutSystems application with record-level permissions (Entity records), it's better to manage permissions at the group or role level instead of individual user accounts. This makes permission management easier over time.</p>
<p>External Groups for Microsoft Graph External Data let you sync your application's group or role structures and use them in Access Control List entries for item permissions.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Syncing here means <strong>you need to manage the group</strong>. It is your responsibility to add and remove external group members whenever there is a change in your application.</div>
</div>

<p>An external group is identified by a unique identifier that you need to specify when creating an external group in Microsoft Graph. This identifier can be any alphanumeric string without special characters. The external identifier is then used in all future member management operations.</p>
<p>The <strong>Graph External Data Connections API</strong> connector library offers the actions you need to create, update, and delete external groups, as well as add and remove group members.</p>
<h2 id="heading-creating-an-external-group">Creating an External Group</h2>
<p>With the <strong>Graph_CreateExternalGroup</strong> action you can create a new external group in Microsoft Graph by providing the following information.</p>
<ul>
<li><p><strong>ExternalGroupId</strong> - The unique identifier of the external group. Can be any alphanumeric string without special characters.</p>
</li>
<li><p><strong>DisplayName</strong> - Optional display name</p>
</li>
<li><p><strong>Description</strong> - Optional description</p>
</li>
</ul>
<h2 id="heading-adding-and-removing-external-group-members">Adding and Removing External Group Members</h2>
<p>The actions <strong>Graph_CreateIdentity</strong> and <strong>Graph_DeleteIdentity</strong> let you add and likewise remove members from an external group. Members can be one of the following types</p>
<ul>
<li><p><strong>user</strong> - A Microsoft Entra user account identified by its object identifier.</p>
</li>
<li><p><strong>group</strong> - A Microsoft Entra group identified by its object identifier.</p>
</li>
<li><p><strong>externalGroup</strong> - Another, already existing, external group that you want to nest.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Please note that it may take up to 30 seconds after creating a new external group before you can add members to it.</div>
</div>

<h2 id="heading-adding-an-external-group-item-access-control-list-entry">Adding an External Group Item Access Control List Entry</h2>
<p>Using an external group in an ACL is straight forward. The JSON representation of an external group ACL would look like this.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"accessType"</span>: <span class="hljs-string">"grant"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"externalGroup"</span>,
  <span class="hljs-attr">"value"</span>: <span class="hljs-string">"externalGroupIdentifier"</span>
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">When adding an external data item to Microsoft Graph with ACLs based on new external groups, it may take some time for Microsoft 365 to evaluate group memberships. From my own observations up to 60 minutes.</div>
</div>

<p>Now that we have covered the basics, let's explore the implementation of the demo application.</p>
<h1 id="heading-demo-application-walkthrough"><strong>Demo Application Walkthrough</strong></h1>
<p>In this walkthrough, we will explore the key implementation details. Be sure to check the comments in the various server actions as well.</p>
<p>The demo application assigns Access Control List entries based only on user accounts, without using external groups. As a small challenge, try implementing a group permission structure in the demo application on your own.</p>
<p>In <strong>ODC Studio</strong>, open the <strong>ODC with Graph</strong> Demo application.</p>
<h2 id="heading-saving-a-testimonial">Saving a Testimonial</h2>
<p>In the <strong>Logic</strong> tab, open the <strong>SaveTestimonial</strong> action under <strong>Server Actions - Actions</strong>. This action runs when a user creates or updates a testimonial on the <strong>Testimonial</strong> screen.</p>
<p>This action first performs a check if it is a new testimonial or an existing one and depending on the outcome executed either the <strong>Testimonial_Create</strong> or <strong>Testimonial_Update</strong> CRUD wrappers.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748863405157/73c97498-b9a9-4a85-8587-7e8a71acea25.png" alt class="image--center mx-auto" /></p>
<p>The <strong>AddPermission</strong> server action is executed when creating a new and is responsible for creating the initial record permission for the creator of the testimonial.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748862343882/b1a8d7b5-10a4-4195-8711-8ce486726556.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>GetUserDetails</strong> - Reads the current user's email from the User entity.</p>
</li>
<li><p><strong>GetEntraAccessToken</strong> - Retrieves an access token using the application credentials to access Microsoft Graph.</p>
</li>
<li><p><strong>Graph_GetUser</strong> - Queries Microsoft Graph using the user's email address to obtain Entra user details with the Universal Principal Name.</p>
</li>
<li><p><strong>TestimonialMember_Create</strong> - A CRUD wrapper to create a record in the TestimonialMember entity with the OutSystems User Identifier and the Entra user's object identifier.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">If your OutSystems user email address does not match your Entra Universal Principal Name, you should replace <strong>Graph_GetUser</strong> with <strong>Graph_ListUsers</strong> and use filter criteria to find the correct Entra user.</div>
</div>

<p>Back in the <strong>SaveTestimonial</strong> action, whether creating or updating a testimonial, the <strong>IngestContent</strong> finally syncs the testimonial with Microsoft Graph.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748862727928/71fd2190-c865-4846-9bbc-5830baee5807.png" alt class="image--center mx-auto" /></p>
<p>Most of the action flow was already explained in part 1. The new addition is the <strong>GetTestimonialAccessList</strong> action, which retrieves the current permissions from the <strong>TestimonialMember</strong> entity and returns a list of <strong>ItemAccessControlList</strong> and assigned to the <strong>Graph_CreateOrUpdateItem</strong> request structure.</p>
<h2 id="heading-modifying-permissions">Modifying Permissions</h2>
<p>You can modify testimonial permissions in the demo application for existing testimonials. In the <strong>Permissions</strong> tab of a testimonal you can add or remove OutSystems users.</p>
<p>Adding a new member with permissions to a testimonial triggers the <strong>AddTestimonialMember</strong> action under <strong>Server Actions - Actions</strong>. Removing a member triggers the <strong>RemoveTestimonialMember</strong> action.</p>
<p>Both actions first execute the <strong>AddPermission</strong> or <strong>DeletePermission</strong> actions, followed by the <strong>IngestContent</strong> action to update the record with the new ACL entries in Microsoft Graph.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748864120344/de2e5405-e6fa-4836-8297-d471b10cfee4.png" alt class="image--center mx-auto" /></p>
<p>This wraps up the demo application walkthrough. Try using the demo application with different user accounts to see the search results in Microsoft 365. As an extra challenge, try adding external group management to the application.</p>
<h1 id="heading-notes-and-recommendations">Notes and Recommendations</h1>
<p>The demo application performs all actions synchronously, triggered from the frontend. In a real use case, you should offload data ingestion to Microsoft Graph to a workflow.</p>
<p>In addition, I recommend implementing a queue that stores an entry for each data item (testimonial) to create, update, and delete, and removes the entry from the queue once the data ingestion operation succeeds. This way, you minimize the risk of application data and graph data becoming inconsistent.</p>
<h1 id="heading-summary">Summary</h1>
<p>In this tutorial, we explored how to define Access Control List entries for ingested data items to limit access for specific user accounts. We also discussed how the Graph External Data Connections API and Graph Users API connector libraries can be used to manage external groups and their memberships.</p>
<p>I hope you enjoyed it and would appreciate your feedback.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[Ingesting OutSystems Data into Microsoft Graph]]></title><description><![CDATA[In most cases, when we create an integration with Microsoft 365, we use data and actions from the Microsoft 365 ecosystem in OutSystems applications. This can include reading user profile information, retrieving files from a OneDrive folder, sending ...]]></description><link>https://without.systems/ingesting-outsystems-data-into-microsoft-graph</link><guid isPermaLink="true">https://without.systems/ingesting-outsystems-data-into-microsoft-graph</guid><category><![CDATA[Microsoft Search]]></category><category><![CDATA[outsystems]]></category><category><![CDATA[Microsoft Graph]]></category><category><![CDATA[Microsoft 365]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Wed, 07 May 2025 03:38:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746073188802/285b52e6-946f-4f7a-90ec-bcbe9825ea77.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In most cases, when we create an integration with Microsoft 365, we use data and actions from the Microsoft 365 ecosystem in OutSystems applications. This can include reading user profile information, retrieving files from a OneDrive folder, sending an email on behalf of a user or resource mailbox, and much more.</p>
<p>However, the Graph API also lets us add our application data to the Graph, making it directly accessible to Microsoft 365 applications and services.</p>
<ul>
<li><strong>Context IQ in Outlook Web</strong> - Allows users to find in-content suggestions from ingested application data while composing an email.</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">At the time of writing, Context IQ in Outlook Web is in preview.</div>
</div>

<ul>
<li><p><strong>Microsoft 365 apps</strong> - Allows application data to be discoverable from microsoft365.com under <strong>Quick Access</strong> and <strong>My Content.</strong></p>
</li>
<li><p><strong>Microsoft 365 Copilot</strong> - Allows users to easily find, summarize, and learn important details about all content relevant to a user's natural language prompts, including ingested application data.</p>
</li>
<li><p><strong>Microsoft Search</strong> - Allows data to be searchable for users in Microsoft Search, including Office.com, Bing at Work, and SharePoint.</p>
</li>
</ul>
<p>Each of the above integrations has specific requirements on how external data must be injected into the Graph to become available. A full description of the individual requirements is <a target="_blank" href="https://learn.microsoft.com/en-us/graph/connecting-external-content-experiences">available here</a>.</p>
<p>In this tutorial, we will learn how to add application data to Microsoft Graph for use with <strong>Microsoft Search</strong>. In future tutorials, we will cover the other integrations as well.</p>
<h1 id="heading-demo-application"><strong>Demo Application</strong></h1>
<p>This article series includes a demo application called "<strong>ODC with Graph Demo</strong>," available on <strong>ODC Forge</strong>. Be sure to download the version of the application that matches this article.</p>
<p>For this article, you need to install Version 0.1 from ODC Forge.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>ODC with Graph Demo</strong>".</p>
</li>
<li><p>Click on the Asset <strong>(Do not click on Install on the tile!)</strong>.</p>
</li>
<li><p>Switch to the <strong>Version</strong> tab and click on Install next to <strong>Version 0.1</strong>.</p>
</li>
</ul>
<p>Version 0.1 depends on other Forge components:</p>
<ul>
<li><p><strong>OAuthTokenExchange</strong> - An external logic library that helps retrieve access tokens easily. In this tutorial however we use an action to retrieve the discovery document from Microsoft Entra only.</p>
</li>
<li><p><strong>Graph External Data Connections API</strong> - A connector library that integrates with Microsoft Graph API for external data connections.</p>
</li>
</ul>
<h1 id="heading-what-we-build">What we build</h1>
<p>The demo application lets users create and manage simple customer testimonials. These testimonials will be available in Microsoft Search, enabling users to search and filter them.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746533552867/3c6dc09f-d619-4e65-84d3-c8ccd6398aa8.png" alt class="image--center mx-auto" /></p>
<p>The screenshot above shows a sample search result for a testimonial. Notice the selected <strong>Testimonials</strong> tab, known as a <strong>vertical</strong>, which makes it easy to filter search results to show only testimonials. The URL redirects the user to the demo application to view the full details of the testimonial.</p>
<p>To achieve this, we need to follow several steps:</p>
<ul>
<li><p><strong>Register a connection and data schema</strong> - The connection serves as a container within the Graph API, storing content items (data). The schema defines the property names and data types of these content items.</p>
</li>
<li><p><strong>Customize M365 Search &amp; Intelligence</strong> - We need to specify how search results for our content items are displayed in M365 search and set up a vertical for easy filtering of search results.</p>
</li>
<li><p><strong>Sync application data</strong> - Whenever a user adds, modifies, or deletes a testimonial in the demo application, we need to create, update, or delete the content item in Microsoft Graph.</p>
</li>
</ul>
<p>Let's start with the prerequisites.</p>
<h1 id="heading-prerequisites">Prerequisites</h1>
<p>Interacting with Microsoft Graph requires credentials from your Microsoft Entra tenant, with permissions to manage a data connection and content items.</p>
<h2 id="heading-register-entra-application"><strong>Register Entra Application</strong></h2>
<p>In the <a target="_blank" href="https://entra.microsoft.com">Entra admin center</a>, go to <strong>Applications - App Registrations</strong> and click on <strong>New registration</strong>.</p>
<ul>
<li><p><strong>Name</strong> - Graph External Data Connection ODC</p>
</li>
<li><p><strong>Supported account types</strong> - Accounts in this organizational directory only</p>
</li>
<li><p>Click <strong>Register</strong></p>
</li>
</ul>
<p>From the <strong>Overview</strong> page, copy the values for:</p>
<ul>
<li><p><strong>Application (client) ID</strong></p>
</li>
<li><p><strong>Directory (tenant) ID</strong></p>
</li>
</ul>
<p>Next, select the <strong>Certificates &amp; secrets</strong> menu. In the <strong>Client secrets</strong> tab, click on <strong>New client secret</strong>.</p>
<ul>
<li><p><strong>Description</strong> - Demo Application</p>
</li>
<li><p><strong>Expires</strong> - 90 days (3 months)</p>
</li>
<li><p>Click <strong>Add</strong></p>
</li>
</ul>
<p>Copy the <strong>Value</strong>, which is the <strong>Client Secret</strong>. It will only be shown once.</p>
<p>Finally, we need to assign the permission to create and manage a Graph data connection.</p>
<p>In the <strong>API permissions</strong> menu, click <strong>Add permission</strong>.</p>
<ul>
<li><p>Select <strong>Microsoft Graph</strong> and <strong>Application permission</strong>.</p>
</li>
<li><p>Search for and select <strong>ExternalConnection.ReadWrite.OwnedBy</strong> permission.</p>
</li>
<li><p>Search for and select <strong>ExternalItem.ReadWrite.OwnedBy</strong> permission.</p>
</li>
<li><p>Click <strong>Add permission</strong>.</p>
</li>
<li><p>Remove the default <strong>User.Read</strong> permission.</p>
</li>
</ul>
<p>This permission requires an administrator to grant admin consent.</p>
<ul>
<li>Click <strong>Grant admin consent &lt;your domain&gt;</strong> and confirm the dialog.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746509864794/465fb6ce-e22a-4651-89b6-d0aad7f5026a.png" alt class="image--center mx-auto" /></p>
<p>With your <strong>TenantId</strong>, <strong>ClientId</strong>, and <strong>ClientSecret</strong>, you can now set up the demo application's settings.</p>
<h2 id="heading-demo-application-settings">Demo Application Settings</h2>
<p>In <strong>ODC Portal - Apps</strong>, choose the <strong>ODC with Graph Demo</strong> application.</p>
<p>In the configuration tab, enter the following values:</p>
<ul>
<li><p><strong>EntraTenantId</strong> - Directory (tenant) ID.</p>
</li>
<li><p><strong>EntraClientID</strong> - Application (client) ID.</p>
</li>
<li><p><strong>EntraClientSecret</strong> - Secret value copied after creating a new secret.</p>
</li>
</ul>
<p>Leave the fourth setting, <code>ConnectionId</code>, with its default value. We'll explore this one shortly.</p>
<h1 id="heading-run-the-demo-application">Run the Demo Application</h1>
<p>With the settings defined, you can now run the demo application (make sure to grant your user the <strong>ODCwithGraphDemo</strong> role first).</p>
<p>On the start screen, you will see an alert box notifying you that your Microsoft Graph tenant is not yet configured.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746539093297/779bc631-001b-4f1e-968b-0b72ffcc18d6.png" alt class="image--center mx-auto" /></p>
<p>Click the <strong>Configure Data Connection</strong> button to register a data connection and a data schema. This process can take up to 15 minutes to complete. We will explore the details of connection and schema registration shortly. For now, we just want to be able to add some testimonials to the demo application.</p>
<p>Once the connection and data schema is ready you can add your first testimonal.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746531462873/60b09468-0d2b-4c91-9ae4-dd065b92da39.png" alt class="image--center mx-auto" /></p>
<p>When you <strong>Save</strong>, the testimonial is stored in the <strong>Testimonial</strong> entity and then added to Microsoft Graph. Similarly, if you update or delete an existing testimonial, it is updated or deleted in Microsoft Graph.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">If you encounter error messages, check your Entra application API permissions and the application settings for EntraClientId, EntraClientSecret, and EntraTenantId.</div>
</div>

<p>After you create some testimonials, try searching for one of the company names or testimonial texts in Microsoft 365. You will notice that none of your testimonials appear yet. This is because, although we can already add data to Microsoft Graph, we still need to complete some final configuration steps in <strong>Microsoft 365 Search &amp; Intelligence</strong> and enable our connection.</p>
<h1 id="heading-microsoft-365-search-amp-intelligence-configuration">Microsoft 365 Search &amp; intelligence Configuration</h1>
<p>Microsoft 365 Search &amp; Intelligence can be accessed through the <a target="_blank" href="https://admin.microsoft.com/">Microsoft 365 admin center</a> under <strong>Settings</strong>.</p>
<p>Here, we configure the vertical—the tab in the search bar that lets you filter testimonials only—how testimonials are displayed in the search results, and finally, we activate our connection.</p>
<h2 id="heading-add-a-vertical">Add a Vertical</h2>
<p>A vertical is a tab on the search results page that shows results from a specific content source, like our injected testimonial data. It helps users to filter their search on a particular category of information, making the results more relevant. Some of the default verticals are</p>
<ul>
<li><p><strong>All</strong> - shows results from all sources</p>
</li>
<li><p><strong>Files</strong> - shows documents and files</p>
</li>
<li><p><strong>People</strong> - shows user profiles</p>
</li>
<li><p><strong>Sites</strong> - shows Sharepoint sites</p>
</li>
</ul>
<p>In <strong>Search &amp; intelligence</strong> settings switch to the <strong>Customizations</strong> tab and select <strong>Verticals</strong> from the menu options.</p>
<ul>
<li><p>Click <strong>Add</strong></p>
</li>
<li><p><strong>Name</strong> - Testimonials</p>
</li>
<li><p>Click Next</p>
</li>
<li><p>In the <strong>Select a content source</strong> screen select <strong>Connectors</strong> and then the configured <strong>Testimonials</strong> data source.</p>
</li>
<li><p>Click <strong>Next</strong></p>
</li>
<li><p>Skip the <strong>Add a query</strong> screen by clicking <strong>Next</strong></p>
</li>
<li><p>Skip the <strong>Filters</strong> screen by clicking <strong>Next</strong></p>
</li>
<li><p>Review the vertical and click <strong>Add Vertical</strong></p>
</li>
</ul>
<p>Back in the Verticals list select the <strong>checkbox</strong> next to <strong>Testimonials</strong> and click the <strong>Enable</strong> button in the top menu.</p>
<h2 id="heading-add-a-result-type">Add a Result type</h2>
<p>Result types are a customization feature that controls how search results are displayed based on specific conditions, such as the content source (like our injected testimonial data), but not limited to that. With a result type, you configure an Adaptive Card, a user interface widget that is bound to result data, which defines how a search result is shown to the user in Microsoft Search. It is a powerful way to enhance the user experience for your custom data.</p>
<p>In <strong>Search &amp; intelligence</strong> settings switch to the <strong>Customizations</strong> tab and select <strong>Result types</strong> from the menu options.</p>
<ul>
<li><p>Click <strong>Add</strong></p>
</li>
<li><p><strong>Name</strong> - Testimonial</p>
</li>
<li><p>Click <strong>Next</strong></p>
</li>
<li><p>Content Source - Testimonials (the name of our External Data Connections container)</p>
</li>
<li><p>Click <strong>Next</strong></p>
</li>
<li><p>Skip the <strong>Set rules for the result type</strong> screen by clicking <strong>Next</strong>.</p>
</li>
<li><p>In the <strong>Design your layout screen</strong> paste the following Adaptive Card template to the text area.</p>
</li>
</ul>
<pre><code class="lang-json">{
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"AdaptiveCard"</span>,
    <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.3"</span>,
    <span class="hljs-attr">"body"</span>: [
        {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"ColumnSet"</span>,
            <span class="hljs-attr">"columns"</span>: [
                {
                    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Column"</span>,
                    <span class="hljs-attr">"width"</span>: <span class="hljs-string">"auto"</span>,
                    <span class="hljs-attr">"items"</span>: [
                        {
                            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Image"</span>,
                            <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://res.cdn.office.net/midgard/versionless/defaultmrticon.png"</span>,
                            <span class="hljs-attr">"horizontalAlignment"</span>: <span class="hljs-string">"center"</span>,
                            <span class="hljs-attr">"size"</span>: <span class="hljs-string">"small"</span>
                        }
                    ],
                    <span class="hljs-attr">"horizontalAlignment"</span>: <span class="hljs-string">"center"</span>
                },
                {
                    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Column"</span>,
                    <span class="hljs-attr">"width"</span>: <span class="hljs-string">"stretch"</span>,
                    <span class="hljs-attr">"items"</span>: [
                        {
                            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"ColumnSet"</span>,
                            <span class="hljs-attr">"columns"</span>: [
                                {
                                    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Column"</span>,
                                    <span class="hljs-attr">"width"</span>: <span class="hljs-string">"auto"</span>,
                                    <span class="hljs-attr">"items"</span>: [
                                        {
                                            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                                            <span class="hljs-attr">"text"</span>: <span class="hljs-string">"[${company}](${url})"</span>,
                                            <span class="hljs-attr">"weight"</span>: <span class="hljs-string">"bolder"</span>,
                                            <span class="hljs-attr">"size"</span>: <span class="hljs-string">"medium"</span>,
                                            <span class="hljs-attr">"maxLines"</span>: <span class="hljs-number">3</span>,
                                            <span class="hljs-attr">"color"</span>: <span class="hljs-string">"accent"</span>
                                        }
                                    ],
                                    <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"none"</span>
                                }
                            ],
                            <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"small"</span>
                        },
                        {
                            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                            <span class="hljs-attr">"text"</span>: <span class="hljs-string">"[${url}](${url})"</span>,
                            <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"small"</span>,
                            <span class="hljs-attr">"weight"</span>: <span class="hljs-string">"bolder"</span>,
                            <span class="hljs-attr">"color"</span>: <span class="hljs-string">"dark"</span>
                        },
                        {
                            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Container"</span>,
                            <span class="hljs-attr">"items"</span>: [
                                {
                                    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                                    <span class="hljs-attr">"text"</span>: <span class="hljs-string">"**${lastUpdatedByName}** modified {{DATE(${lastUpdatedOn})}}"</span>,
                                    <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"small"</span>,
                                    <span class="hljs-attr">"$when"</span>: <span class="hljs-string">"${lastUpdatedByName!='' &amp;&amp; lastUpdatedOn!=''}"</span>
                                },
                                {
                                    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                                    <span class="hljs-attr">"text"</span>: <span class="hljs-string">"Modified on {{DATE(${lastUpdatedOn})}}"</span>,
                                    <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"small"</span>,
                                    <span class="hljs-attr">"$when"</span>: <span class="hljs-string">"${lastUpdatedByName=='' &amp;&amp; lastUpdatedOn!=''}"</span>
                                },
                                {
                                    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                                    <span class="hljs-attr">"text"</span>: <span class="hljs-string">"Modified by __${lastUpdatedByName}__"</span>,
                                    <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"small"</span>,
                                    <span class="hljs-attr">"$when"</span>: <span class="hljs-string">"${lastUpdatedByName!='' &amp;&amp; lastUpdatedOn==''}"</span>
                                }
                            ],
                            <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"small"</span>
                        },
                        {
                            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                            <span class="hljs-attr">"text"</span>: <span class="hljs-string">"${ResultSnippet}"</span>,
                            <span class="hljs-attr">"maxLines"</span>: <span class="hljs-number">2</span>,
                            <span class="hljs-attr">"wrap"</span>: <span class="hljs-literal">true</span>,
                            <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"small"</span>
                        }
                    ],
                    <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"medium"</span>
                }
            ]
        }
    ],
    <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"http://adaptivecards.io/schemas/adaptive-card.json"</span>,
    <span class="hljs-attr">"$data"</span>: {
        <span class="hljs-attr">"lastUpdatedOn"</span>: <span class="hljs-string">"2019-09-25T06:08:39Z,SHORT"</span>,
        <span class="hljs-attr">"ResultSnippet"</span>: <span class="hljs-string">"Wonderful work. Very helpful."</span>,
        <span class="hljs-attr">"lastUpdatedByName"</span>: <span class="hljs-string">"Stefan Weber"</span>,
        <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://without.systems"</span>,
        <span class="hljs-attr">"company"</span>: <span class="hljs-string">"without.systems"</span>
    }
}
</code></pre>
<p>Notice the <code>${property}</code> elements in the Adaptive Card that connect individual properties of the defined data schema of a testimonial to the card. The <code>${ResultSnippet}</code> is a special system property that links the inserted content value and also provides search highlighting.</p>
<ul>
<li><p>Click <strong>Next</strong></p>
</li>
<li><p>Review the Result type and click <strong>Add result type</strong></p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It may take several hours for a new result type to become available.</div>
</div>

<h2 id="heading-activate-connection">Activate Connection</h2>
<p>The final step is to activate our connection.</p>
<p>In the <strong>Search &amp; Intelligence</strong> settings, switch to the <strong>Data Sources</strong> tab.</p>
<ul>
<li>Click on <strong>Include Connector Results</strong> in the <strong>Required Actions</strong> column of the <strong>Testimonials</strong> connection and confirm the dialog.</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">After activating the connection, it may take a few minutes before you can search for testimonials and filter using the Testimonial vertical.</div>
</div>

<p>By the end of this step, you should be able to search for testimonials in Microsoft Search.</p>
<p>Lets walk through the demo implementation details.</p>
<h1 id="heading-demo-application-walkthrough">Demo Application Walkthrough</h1>
<p>In this walkthrough, we will explore the key implementation details. Be sure to check the comments in the various server actions as well.</p>
<p>In <strong>ODC Studio</strong>, open the <strong>ODC with Graph</strong> Demo application.</p>
<h2 id="heading-data-connection-and-schema-registration">Data Connection and Schema Registration</h2>
<p>In the Logic tab, open the <strong>RegisterGraphDataConnection</strong> action under <strong>Server Actions - Graph</strong>. This action was executed when you clicked the <strong>Configure Data Connection</strong> button on the demo application's start screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746534432145/2ef409c4-f337-43d3-978e-685df19a143b.png" alt class="image--center mx-auto" /></p>
<ul>
<li><strong>GetEntraAccessToken</strong> - Retrieves an access token from Entra via OAuth client-credentials flow.</li>
</ul>
<p>The demo application requests a new access token every time it interacts with Microsoft Graph. In a production environment, you would set up a token cache to request a new token only when the previous one has expired.</p>
<ul>
<li><p><strong>Graph_CreateExternalConnection</strong> - This action is provided by the <strong>Graph External Data Connections API</strong> connector library and <a target="_blank" href="https://learn.microsoft.com/en-us/graph/api/externalconnectors-external-post-connections?view=graph-rest-1.0&amp;tabs=http">creates a new data connection</a> with</p>
<ul>
<li><p>id - the short name of the data connection</p>
</li>
<li><p>name - the display name of the data connection</p>
</li>
<li><p>description - additional description of the data connection.</p>
</li>
</ul>
</li>
<li><p><strong>Graph_CreateOrUpdateConnectionSchema</strong> - This action registers a data schema for the data connection.</p>
</li>
</ul>
<p>A data schema is a set of properties with its data type that represent the content item’s (testimonal) metadata. A single property definition in JSON looks like this.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"company"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
  <span class="hljs-attr">"isSearchable"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"isRetrievable"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"isQueryable"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"labels"</span>: [
     <span class="hljs-string">"title"</span>
  ],
  <span class="hljs-attr">"aliases"</span>: []
}
</code></pre>
<p>Besides the property name, the definition specifies if you can search for the value (isSearchable), if the property can be used in KQL queries (isQueryable), or if the property can be used in <strong>result types</strong> (isRetrievable). Additionally, you can map properties to well-known labels of the Microsoft Search Index, like <strong>title</strong> in the example above. More information can be found in the <a target="_blank" href="https://learn.microsoft.com/en-us/graph/api/externalconnectors-externalconnection-patch-schema?view=graph-rest-1.0&amp;tabs=http">documentation</a>.</p>
<h2 id="heading-ingest-content-item">Ingest Content Item</h2>
<p>In the Logic tab, open the <strong>IngestContent</strong> action under <strong>Server Actions - Graph</strong>. This action is triggered by the <strong>SaveTestimonial</strong> action after a testimonial is created or updated. It creates or updates the content item in Graph.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746536112290/4e8c07c8-ebeb-4a85-b132-66191740fe43.png" alt class="image--center mx-auto" /></p>
<p>The <strong>Graph_CreateOrUpdateItem</strong> action at the bottom of the action flow expects a serialized JSON structure as Request parameter. The reason is that the object you have to pass to the Graph endpoint is dynamic based on the schema you defined.</p>
<p>Inspect the Item structure in <strong>Data - Structures - Content</strong>. The structure consists of the following parts</p>
<ul>
<li><p><strong>Id</strong> - the Id is the unique identifier of the content item, the primary key value of our testimonal.</p>
</li>
<li><p><strong>Properties</strong> - Attributes that correspond to the schema we registered for the connection</p>
</li>
<li><p><strong>AccessControlList</strong> - Permissions that specify who can view/read the content item. ACLs are not covered in this tutorial though.</p>
</li>
<li><p><strong>Content</strong> - In our case the content object will hold the value of the testimonal text.</p>
</li>
</ul>
<p>Lets break the server action flow down</p>
<ul>
<li><p><strong>Testimonal_Get</strong> - This server action retrieves the full details of the requested testimonial.</p>
</li>
<li><p><strong>Id</strong> - We assign the testimonal identifier to the Id property of the Item structure.</p>
</li>
<li><p><strong>Properties</strong> - Assign the metadata values of the testimonials to the Item Properties objects.</p>
</li>
<li><p><strong>Content</strong> - Assign the text of the testimonial as content object of type text to the Item Content object.</p>
</li>
<li><p><strong>AccessControlListEntry</strong> - Access Control List that define who has access to the content item are not covered in this tutorial, but you must add at least one access control list entry. We take the easy route here and grant permission to everyone.</p>
</li>
<li><p><strong>SerializeItem</strong> - Serialize the Item structure and request parameter for the <strong>Graph_CreateOrUpdateItem</strong> action.</p>
</li>
</ul>
<p>This concludes the demo application walkthrough. Explore the rest of the demo application and the configuration options in Microsoft 365 Search &amp; intelligence. The final step is to cleanup everything.</p>
<h1 id="heading-cleanup">Cleanup</h1>
<p>Once you have finished exploring data ingestion to Microsoft Graph, you should perform the following cleanup steps in Microsoft 365 Search &amp; Intelligence:</p>
<ul>
<li><p>Remove the Result type</p>
</li>
<li><p>Remove the Vertical</p>
</li>
<li><p>Delete the Data Connection</p>
</li>
</ul>
<h1 id="heading-notes-and-recommendations">Notes and Recommendations</h1>
<p>The demo application performs all actions synchronously, triggered from the frontend. In a real use case, you should offload data ingestion to Microsoft Graph to a workflow.</p>
<p>In addition, I recommend implementing a queue that stores an entry for each data item (testimonial) to create, update, and delete, and removes the entry from the queue once the data ingestion operation succeeds. This way, you minimize the risk of application data and graph data becoming inconsistent.</p>
<p>The Graph External Data Connections API library integrates with the endpoints to create a connection and its schema. However, since this is a one-time operation, you can also use <a target="_blank" href="https://www.postman.com">Postman</a> or a curl command instead of doing it in your application.</p>
<p>Be patient when changing Search &amp; Intelligence settings like verticals or result types in the M365 admin center. In some cases, it may take several hours to see and use a new configuration due to caching.</p>
<h1 id="heading-summary">Summary</h1>
<p>In this tutorial, we built a simple integration with Microsoft Graph and ingested OutSystems application data into the Search index. Having a central way to search for both Microsoft 365 data and OutSystems application data is a great benefit for users who would otherwise have to switch between applications. Adding application data to Microsoft Search also provides additional benefits that I will explore in later tutorials.</p>
<p>I hope you enjoyed it and would appreciate your feedback.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[Adaptive Card based Loop Components in Microsoft Outlook]]></title><description><![CDATA[In the ODC with Loop Components series so far, we have been using Microsoft Teams for our Adaptive Card-based Loop Components. By now, you should have been able to unfurl an auction link from the demo application into an Adaptive Card in a Teams conv...]]></description><link>https://without.systems/adaptive-card-based-loop-components-in-microsoft-outlook</link><guid isPermaLink="true">https://without.systems/adaptive-card-based-loop-components-in-microsoft-outlook</guid><category><![CDATA[outsystems]]></category><category><![CDATA[Microsoft365]]></category><category><![CDATA[#adaptive-cards]]></category><category><![CDATA[Bot Framework]]></category><category><![CDATA[azure bot service]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Wed, 23 Apr 2025 15:47:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745313425327/a12bb8ec-262f-412e-ab87-ae9361b878ac.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the ODC with Loop Components series so far, we have been using Microsoft Teams for our Adaptive Card-based Loop Components. By now, you should have been able to unfurl an auction link from the demo application into an Adaptive Card in a Teams conversation and interact with it by placing a bid.</p>
<p>If you try to copy and paste an auction link—either by copying the link from the browser or using the <strong>Copy component</strong> icon on a card—into a new Outlook message, you'll notice it doesn't work yet, meaning the URL won't unfurl. To extend our sample to support Microsoft Outlook messages as well, we need to do two additional steps:</p>
<ul>
<li><p>Add the <strong>Microsoft 365 channel</strong> configuration in our Azure AI Bot resource in the Azure Portal.</p>
</li>
<li><p>Add a <strong>Search Command</strong> to our App Manifest.</p>
</li>
<li><p>(Optionally) Implement the configured <strong>Search Command</strong> in our OutSystems Developer Cloud application.</p>
</li>
</ul>
<h1 id="heading-demo-application"><strong>Demo Application</strong></h1>
<p>This article series includes a demo application called "<strong>ODC with Loop Components Demo</strong>," available on <strong>ODC Forge</strong>. Be sure to download the version of the application that matches each article in this series.</p>
<p>For this article, you need to install Version 0.4 from ODC Forge.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>ODC with Loop Components Demo</strong>".</p>
</li>
<li><p>Click on the Asset <strong>(Do not click on Install on the tile!)</strong>.</p>
</li>
<li><p>Switch to the <strong>Version</strong> tab and click on Install next to <strong>Version 0.4</strong>.</p>
</li>
</ul>
<p>Version 0.4 depends on other Forge components:</p>
<ul>
<li><p><strong>OAuthTokenExchange</strong> - An external logic library that helps retrieve access tokens easily. In this tutorial however we use an action to retrieve the discovery document from Microsoft Entra only.</p>
</li>
<li><p><strong>LiquidFluid</strong> - An external logic library that merges data with templates based on the Shopify Liquid Templating language.</p>
</li>
<li><p><strong>UriParser</strong> - An external logic library that parses a given URI/URL.</p>
</li>
</ul>
<h1 id="heading-what-we-build">What we build</h1>
<p>In this part, we extend our implementation to allow pasting auction links into Outlook messages, which are then expanded into Adaptive Card-based Loop components.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745315251521/1d5b7d2d-2150-4606-931f-53c7f3cec29b.png" alt class="image--center mx-auto" /></p>
<p>The good news is that we don't need to change our implementation. We only need to configure the <strong>Azure AI Bot</strong> resource in the <strong>Azure Portal</strong> and make one addition to our App Manifest by adding a <a target="_blank" href="https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/define-search-command">Search Command</a>. After that, our card will work just like it does in Teams, including card refreshes and, of course, the ability to place a bid.</p>
<h1 id="heading-activate-microsoft-365-channel">Activate Microsoft 365 channel</h1>
<p>This task is straightforward. Open the <a target="_blank" href="https://portal.azure.com">Azure Portal</a> and select your <strong>Azure AI Bot</strong> Resource (if you followed the tutorial, its name should be BotId-&lt;Application (client) ID&gt;).</p>
<p>At the resource level, select <strong>Settings - Channels</strong> and under <strong>Available Channels</strong>, click the <strong>Microsoft 365</strong> channel entry.</p>
<p>Click the <strong>Apply</strong> button to activate this channel.</p>
<h1 id="heading-modify-app-manifest">Modify App Manifest</h1>
<p>Activating the Microsoft 365 channel extends the support of our Azure AI Bot resource to Microsoft 365 applications.</p>
<p>However, when you try to paste an auction link into a new message, it still doesn't expand into an Adaptive Card. It took me a while to figure this out because the documentation isn't very clear, but for Microsoft Outlook to support link unfurling, your App Manifest must have at least one command configured.</p>
<p>In an App Manifest, you can configure two types of commands:</p>
<ul>
<li><p><strong>Query</strong> - Allows a user to perform searches. Query implementations return search results from which the user can select.</p>
</li>
<li><p><strong>Action</strong> - Allows a user to perform any action, like creating a task in Microsoft To-Do directly from the conversation or message.</p>
</li>
</ul>
<p>For link unfurling into an Adaptive Card, we only need to define one command in the App Manifest. However, we don't need to implement the command (though we will look at a sample implementation later in this tutorial).</p>
<p>Open your <strong>manifest.json</strong> and replace the <strong>empty commands array</strong> with the following:</p>
<pre><code class="lang-json">{
  ...,
  <span class="hljs-attr">"commands"</span>: [
    {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"searchAuction"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"query"</span>,
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Search Auction"</span>,
      <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Searches available auctions"</span>,
      <span class="hljs-attr">"initialRun"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"fetchTask"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"context"</span>: [<span class="hljs-string">"compose"</span>, <span class="hljs-string">"commandBox"</span>],
      <span class="hljs-attr">"parameters"</span>: [
        {
          <span class="hljs-attr">"name"</span>: <span class="hljs-string">"searchTerm"</span>,
          <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Search Term"</span>,
          <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Part of the auction title to search for"</span>,
          <span class="hljs-attr">"inputType"</span>: <span class="hljs-string">"text"</span>
        }
      ]
    }
  ],
  ...
}
</code></pre>
<p>This defines a <strong>query</strong> command called <strong>searchAuction</strong> that takes one input parameter, <strong>searchTerm</strong>, and is available in the Microsoft Outlook new message window and the Microsoft Teams command bar (<strong>context</strong>).</p>
<p>Additionally, increase the app manifest <code>version</code> property.</p>
<p>After making the changes, compress the manifest.json file along with the icons into a zip file. Then, re-upload the custom app to Microsoft Teams as described <a target="_blank" href="https://without.systems/odc-with-loop-components-basic-setup-and-link-unfurling?source=more_series_bottom_blogs#heading-create-app-manifest">here</a>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">After uploading, make sure to <strong>restart Microsoft Teams and Microsoft Outlook</strong>.</div>
</div>

<h1 id="heading-try-it">Try it</h1>
<p>Even without implementing the query command, the link unfurling of an auction should now work without any extra setup.</p>
<p>Open Microsoft Outlook and click on <strong>New - Mail</strong> in the ribbon menu. In the message compose window, click on Apps in the ribbon menu. Check that the app list includes the <strong>ODCwithLoop</strong> app.</p>
<p>Copy an auction link from the demo application and paste it into the message body. After pressing return, the link should successfully unfurl to our auction card, just like in Microsoft Teams.</p>
<p>With link unfurling working, we can now move on to implementing the query command in our application. This step is optional if link unfurling is the only feature you need. However, since we defined a query command, it's available in both the Outlook and Teams interfaces. It would create a poor user experience if someone tries it and finds that it doesn't return any results.</p>
<h1 id="heading-search-auction-command">Search Auction Command</h1>
<p>Our defined <strong>Search Auction</strong> command is available in both Microsoft Teams and Microsoft Outlook.</p>
<p>In Teams, you can find it in the conversation command bar by clicking on the + icon and searching for ODCwithLoop. After clicking on the app, you will see the following screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745321563428/1d33186d-0c90-48a3-ad64-0465e78794f9.png" alt class="image--center mx-auto" /></p>
<p>In Outlook, it is available in the compose message window. Click on Apps and select the ODCwithLoop app from the list.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745338411817/6934a98e-e7a8-4ff3-819c-45e2ef525245.png" alt class="image--center mx-auto" /></p>
<p>Whenever you start typing our messaging endpoint receives a request of type <code>invoke</code> and a name of <code>composeExtension/query</code>. This request contains a value property that looks like this:</p>
<pre><code class="lang-json">{
  ...,
  <span class="hljs-attr">"value"</span>: {
    <span class="hljs-attr">"commandId"</span>: <span class="hljs-string">"searchAuction"</span>,
    <span class="hljs-attr">"parameters"</span>: [
      {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"searchTerm"</span>,
        <span class="hljs-attr">"value"</span>: <span class="hljs-string">"pot"</span>
      }
    ],
    <span class="hljs-attr">"queryOptions"</span>: {
      <span class="hljs-attr">"count"</span>: <span class="hljs-number">25</span>,
      <span class="hljs-attr">"skip"</span>: <span class="hljs-number">0</span>
    }
  }
  ...
}
</code></pre>
<ul>
<li><p><strong>commandId</strong> - the value defined for the command id in the app manifest.</p>
</li>
<li><p><strong>parameters</strong> - an array of parameter key/value pairs as defined in the app manifest.</p>
</li>
<li><p><strong>queryOptions</strong> - the default query options count (max items to return) and skip (skip number of records.</p>
</li>
</ul>
<p>The response to a <code>composeExtension/query</code> must be a list of cards supported by the search widget. At the time of writing, the search widgets in both Teams and Outlook support:</p>
<ul>
<li><p><strong>Hero Card</strong> - A card that usually has a large image, one or more buttons, and text.</p>
</li>
<li><p><strong>Thumbnail Card</strong> - A card that usually has a thumbnail image, one or more buttons, and text.</p>
</li>
</ul>
<p>Both card types are only for displaying the search result list. The user can click on an entry to add the card to a Teams conversation or an Outlook message. To add the full card, we need to include some extra configuration and logic to retrieve the full card from the messaging endpoint once a user clicks on an entry.</p>
<h2 id="heading-handling-search">Handling Search</h2>
<p>When a user starts typing in the search widget, an activity request is sent to the messaging endpoint. This request is of type <code>invoke</code> and has the name <code>composeExtension/query</code>. It includes the <strong>searchTerm</strong> in the <strong>value</strong> object, along with other configurations (see the sample above).</p>
<p>Open <strong>ODC Studio</strong> and go to <strong>Logic - Integrations - REST - MessagingEndpoint</strong>, then open the <strong>Messages</strong> flow.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745376019933/2342bef3-5e94-4c0c-a0e1-e8408d42a23c.png" alt class="image--center mx-auto" /></p>
<p>Inspect the third condition (query) of the <strong>Invoke</strong> switch.</p>
<ul>
<li><p><strong>DeserializeQueryCommandActivity</strong> - Deserializes the request containing the <strong>value</strong> object that contains the <strong>searchTerm</strong> parameter.</p>
</li>
<li><p><strong>FilterSearchTerm</strong> - The <strong>searchTerm</strong> is part of the <strong>parameters</strong> array and we need to filter the list to get it.</p>
</li>
</ul>
<p><strong>MessagingSearchAuctionAction</strong></p>
<p>This action performs a query on the Auction entity, constructs a payload structure and renders the search result template.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745376387963/5752d62a-67e8-46bb-8d4e-7be686347be3.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>SearchAuctions</strong> - Queries the Auction entity.</p>
</li>
<li><p><strong>GetDefaultDomain</strong> - Retrieves the domain name of the current ODC stage. This is needed to build the full URL to the auction.</p>
</li>
<li><p><strong>SearchAuctions.List</strong> and <strong>AppendAuction</strong> - Adds the search results to a local payload structure and constructs the auction URL.</p>
</li>
<li><p><strong>RenderAuctionCardListResponse</strong> - Renders the response using the payload data.</p>
</li>
</ul>
<p><strong>RenderAuctionCardListResponse</strong> uses the following Liquid template:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"composeExtension"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"result"</span>,
        <span class="hljs-attr">"attachmentLayout"</span>: <span class="hljs-string">"list"</span>,
        <span class="hljs-attr">"attachments"</span>: [
            {% for auction in auctions %}
            {
                <span class="hljs-attr">"contentType"</span>: <span class="hljs-string">"application/vnd.microsoft.card.thumbnail"</span>,
                <span class="hljs-attr">"content"</span>: {
                    <span class="hljs-attr">"title"</span>: <span class="hljs-string">"{{auction.title}}"</span>,
                    <span class="hljs-attr">"text"</span>: <span class="hljs-string">"{{auction.description}}"</span>
                },
                <span class="hljs-attr">"preview"</span>: {
                    <span class="hljs-attr">"contentType"</span>: <span class="hljs-string">"application/vnd.microsoft.card.thumbnail"</span>,
                    <span class="hljs-attr">"content"</span>: {
                        <span class="hljs-attr">"title"</span>: <span class="hljs-string">"{{auction.title}}"</span>,
                        <span class="hljs-attr">"text"</span>: <span class="hljs-string">"{{auction.description}}"</span>,
                        <span class="hljs-attr">"tap"</span>: {
                            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"invoke"</span>,
                            <span class="hljs-attr">"value"</span>: {
                                <span class="hljs-attr">"url"</span>: <span class="hljs-string">"{{auction.webUrl}}"</span>
                            }
                        }
                    }
                }
            }
            {% endfor %}
        ]
    }
}
</code></pre>
<p>For each auction in the payload structure, an attachment is created. The attachment includes the same thumbnail card content and preview, both needed for message extension responses, with one difference. The <strong>preview</strong> card has an extra object called <strong>tap</strong>. This object specifies what should happen when a user clicks on the card, such as when selecting an entry in the search widget. In our setup, it should <strong>invoke</strong> the messaging endpoint and send a parameter <strong>url</strong>, which contains the full URL to the auction.</p>
<p>This will send a request activity to our messaging endpoint with the <strong>type</strong> <code>invoke</code> and the <strong>name</strong> <code>composeExtension/selectItem</code>. We need to handle this next to return the full auction card.</p>
<h2 id="heading-handle-item-selection">Handle Item Selection</h2>
<p>Back in the MessagingEndpoint - Messages flow, check the fourth Invoke switch condition. This condition handles the <code>composeExtension/selectItem</code> request.</p>
<ul>
<li><p><strong>DeserializeSelectItemActivity</strong> - Deserializes the request into a structure that includes the <strong>url</strong> parameter of the selected entry.</p>
</li>
<li><p><strong>CreateFullCardForSelectedItem</strong> - Executes the MessagingQueryLinkAction server action, which we also use to respond to a link unfurling request.</p>
</li>
</ul>
<p>With everything set up, you can now try it out. Use the search widget in either the Teams or Outlook app to look up an auction. Click on an entry to add the full card to the conversation or message.</p>
<p>I recommend debugging through the flows to check the different results.</p>
<h1 id="heading-summary">Summary</h1>
<p>In this tutorial, we set up our Azure AI Bot resource to work with Microsoft Outlook, in addition to Microsoft Teams, for our Adaptive Card-based Loop Component demo. We then updated our app manifest and added a search command, which is necessary for it to function in Microsoft Outlook. Finally, we implemented the search command, although it is optional if only link unfurling is needed. We learned that we need one logic to handle search requests and another to return a full card when a user clicks on a search result.</p>
<p>For now, this is the final tutorial in the ODC with Loop Components series. There are certainly more topics to explore, but this series should provide a good starting point. I hope you enjoyed working through it. Feedback is always appreciated.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[Adaptive Card Actions]]></title><description><![CDATA[In the last part of this series, we implemented a refresh pattern to keep our Adaptive Card-based Loop Components updated. These steps set the stage for the next phase, where we add user interaction to our auctions. Specifically, we want to enable us...]]></description><link>https://without.systems/odc-with-loop-components-card-interaction</link><guid isPermaLink="true">https://without.systems/odc-with-loop-components-card-interaction</guid><category><![CDATA[outsystems]]></category><category><![CDATA[Microsoft365]]></category><category><![CDATA[azure bot service]]></category><category><![CDATA[Bot Framework]]></category><category><![CDATA[#adaptive-cards]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Mon, 07 Apr 2025 09:22:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743413058425/01220d59-cce0-4437-a36b-b761b82e2fc9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the last part of this series, we implemented a refresh pattern to keep our Adaptive Card-based Loop Components updated. These steps set the stage for the next phase, where we add user interaction to our auctions. Specifically, we want to enable users to bid on items.</p>
<p>Our implementation should allow the following:</p>
<ul>
<li><p>Users can bid on an item until the auction is closed from the web application.</p>
</li>
<li><p>The card should show the highest bid.</p>
</li>
<li><p>Closed auctions display a “Closed” badge next to the title and do not allow further bids.</p>
</li>
</ul>
<h1 id="heading-demo-application"><strong>Demo Application</strong></h1>
<p>This article series includes a demo application called "<strong>ODC with Loop Components Demo</strong>," available on <strong>ODC Forge</strong>. Be sure to download the version of the application that matches each article in this series.</p>
<p>For this article, you need to install Version 0.3 from ODC Forge.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>ODC with Loop Components Demo</strong>".</p>
</li>
<li><p>Click on the Asset <strong>(Do not click on Install on the tile!)</strong>.</p>
</li>
<li><p>Switch to the <strong>Version</strong> tab and click on Install next to <strong>Version 0.3</strong>.</p>
</li>
</ul>
<p>Version 0.3 depends on other Forge components:</p>
<ul>
<li><p><strong>OAuthTokenExchange</strong> - An external logic library that helps retrieve access tokens easily. In this tutorial however we use an action to retrieve the discovery document from Microsoft Entra only.</p>
</li>
<li><p><strong>LiquidFluid</strong> - An external logic library that merges data with templates based on the Shopify Liquid Templating language.</p>
</li>
<li><p><strong>UriParser</strong> - An external logic library that parses a given URI/URL.</p>
</li>
</ul>
<p>After updating the demo application in your environment, you can try it out right away because the App Manifest—the configuration file that added the app to Microsoft Teams—has not changed. Note that only new Loop components will refresh, while all previously added cards in a channel will stay the same.</p>
<h1 id="heading-recap">Recap</h1>
<p>Let's quickly recap what we covered in the last part of the series. We implemented a refresh pattern that keeps an Adaptive Card-based Loop Component up to date. This means a card triggers a Universal Action, which then returns an updated card.</p>
<p>We achived this by adding a <strong>refresh</strong> object to the Adaptive Card template</p>
<pre><code class="lang-json">{
    ...,        
    <span class="hljs-attr">"refresh"</span>: {
        <span class="hljs-attr">"action"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Action.Execute"</span>,
        <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Refresh"</span>,
        <span class="hljs-attr">"verb"</span>: <span class="hljs-string">"refreshAuctionCard"</span>,
        <span class="hljs-attr">"data"</span>: {
            <span class="hljs-attr">"url"</span>: <span class="hljs-string">"{{webUrl}}"</span>
        }
        }
    },
    ....
}
</code></pre>
<p>The definition above sends an <strong>Activity</strong> of type <strong>Invoke</strong> to the messaging endpoint when executed. We observed that the action can be identified by the <strong>verb</strong> in our messaging endpoint implementation, and we can pass extra data using a <strong>data</strong> object.</p>
<p>The messaging endpoint processes the request and returns a complete Adaptive Card, wrapped in a status response template that looks like this:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"statusCode"</span>: <span class="hljs-number">200</span>,
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"application/vnd.microsoft.card.adaptive"</span>,
    <span class="hljs-attr">"value"</span>: &lt;Adaptive Card Content&gt;
}
</code></pre>
<p>The changes we made in the previous step also laid the foundation for handling other actions besides the <strong>refresh</strong> action.</p>
<h1 id="heading-what-we-build">What we build</h1>
<p>We are going to modify our implementation so that when a user pastes a running auction into a Teams channel, it will look like this.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743916819752/86d6a2ee-6fb1-4a67-a31f-34e6a368273d.png" alt class="image--center mx-auto" /></p>
<p>A running auction should have a green "<strong>Active</strong>" badge next to the title. Below the description, the user should be able to enter an amount and place a bid.</p>
<p>After placing the bid, the card should immediately update to show the highest bid for the auction next to the description.</p>
<p>If an auction is closed from the web application, the card will render or update to look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743917061168/41cf8ee3-70f6-43c5-b734-39d12611ded1.png" alt class="image--center mx-auto" /></p>
<p>Closed auctions display a red badge “<strong>Closed</strong>” next to the title and the highest bid next to the description. Closed auction do not have an input field and “Place Bid” button.</p>
<p>In addition closed auction cards will not update anymore.</p>
<p>Let's now look at the changes needed to make this happen.</p>
<h1 id="heading-entities">Entities</h1>
<p>In ODC Studio, switch to <strong>Data - Entities - Database</strong>.</p>
<p>The <strong>Auction</strong> entity now has an additional attribute called <strong>IsActive</strong>, which shows whether an auction is running (true) or completed (false).</p>
<p>The <strong>Bid</strong> entity keeps track of all user bids for an auction. For this demo, it's quite simple, as we are only storing the amount.</p>
<h1 id="heading-templates">Templates</h1>
<p>Go to <strong>Data - Resources - Templates</strong>.</p>
<p>Download the <strong>AuctionCard</strong> template locally and inspect it with a text editor.</p>
<h2 id="heading-declaring-variables">Declaring Variables</h2>
<p>At the top of the template are some Liquid instructions to define additional variables based on data merged from the application.</p>
<pre><code class="lang-json">{% if isActive %}
  {% assign status = <span class="hljs-attr">"Active"</span> %}
  {% assign statusStyle = <span class="hljs-attr">"Good"</span> %}
{% else %}
  {% assign status = <span class="hljs-attr">"Closed"</span> %}
  {% assign statusStyle = <span class="hljs-attr">"Attention"</span> %}
{% endif %}

{% if bid %}
  {% assign price = bid %}
  {% assign priceColor = <span class="hljs-attr">"Good"</span> %}
{% else %}
  {% assign priceColor = <span class="hljs-attr">"Default"</span> %}
{% endif %}
</code></pre>
<p>The first If statement sets the <strong>status</strong> and <strong>statusStyle</strong> variables based on the <strong>isActive</strong> boolean value from the application. These variables are used to show the badge next to the title.</p>
<p>The second If statement checks if there is a <strong>bid</strong> value, which exists only if there has been at least one bid. If there is a <strong>bid</strong>, it updates the <strong>price</strong> variable (part of the merged data) with the <strong>bid</strong> amount.</p>
<h2 id="heading-conditional-rendering">Conditional Rendering</h2>
<p>Throughout the template, you will notice several <strong>If</strong> conditions that wrap parts of the template. For example, the <strong>refresh</strong> object should only be displayed if the auction is still running.</p>
<pre><code class="lang-json">{% if isActive %}
  <span class="hljs-string">"refresh"</span>: {
    <span class="hljs-attr">"action"</span>: {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Action.Execute"</span>,
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Refresh"</span>,
      <span class="hljs-attr">"verb"</span>: <span class="hljs-string">"refreshAuctionCard"</span>,
      <span class="hljs-attr">"data"</span>: {
        <span class="hljs-attr">"url"</span>: <span class="hljs-string">"{{webUrl}}"</span>
      }
    }
  },
{% endif %}
</code></pre>
<p>Inspect the entire template to identify which parts are displayed only when an auction is still active.</p>
<h2 id="heading-form-and-action">Form and Action</h2>
<p>The most interesting part of the template is where we display the input field and the button for placing a bid.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Container"</span>,
  <span class="hljs-attr">"items"</span>: [
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Input.Number"</span>,
      <span class="hljs-attr">"placeholder"</span>: <span class="hljs-string">"Enter your bidding.."</span>,
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"bid"</span>,
      <span class="hljs-attr">"isRequired"</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">"errorMessage"</span>: <span class="hljs-string">"Please enter an amount"</span>
    }
  ]
}
</code></pre>
<p>The above part renders a <strong>Container</strong> with a single number input field (type of <strong>Input.Number</strong>).</p>
<p>It's important to note that the <strong>id</strong> of an input field becomes the attribute name when we submit the form using the <strong>Place Bid</strong> button.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">More details about Adaptive Cards elements can be found in the official documentation at <a target="_self" href="https://adaptivecards.microsoft.com/">Welcome - Adaptive Cards</a>.</div>
</div>

<pre><code class="lang-json"><span class="hljs-string">"actions"</span>: [
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Action.Execute"</span>,
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"bidAction"</span>,
      <span class="hljs-attr">"verb"</span>: <span class="hljs-string">"placeBidding"</span>,
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Place Bid"</span>,
      <span class="hljs-attr">"data"</span>: {
        <span class="hljs-attr">"url"</span>: <span class="hljs-string">"{{webUrl}}"</span>
      }
    }
]
</code></pre>
<p>All card actions must be included in the <strong>actions</strong> array. Our template has one action of type <strong>Action.Execute</strong>. You'll notice that this is quite similar to the refresh action, and it is. Like the <strong>refresh</strong> action, it has a <strong>verb</strong>—which we use to identify the action taken in our messaging endpoint—and the same <strong>data</strong> object that passes the full URL to the auction item.</p>
<p>When a user enters an amount into the <strong>Input.Number</strong> field and clicks the “<strong>Place Bid</strong>” button, all input field data, along with the data defined at the action level, will be added to the data object sent to the messaging endpoint. In our case, the data object sent to the messaging endpoint will look like this.</p>
<pre><code class="lang-json"><span class="hljs-string">"data"</span>: {
  <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://&lt;YOUR ODC STAGE&gt;/ODCwithLoopComponentsDemo/AuctionDetail?AuctionId=12f521f9-5137-4489-be8c-9172673b710b"</span>,
  <span class="hljs-attr">"bid"</span>: <span class="hljs-number">650.00</span>
}
</code></pre>
<p>Take your time to inspect the template and the <strong>RenderAuctionCard</strong> server action in <strong>Logic - Server Actions - Loop</strong>.</p>
<h1 id="heading-deserialize-activity">Deserialize Activity</h1>
<p>In <strong>Logic - Server Actions - Util</strong> inspect <strong>DeserializeAdaptiveCardActionActivity</strong> and the <strong>InvokeAdaptiveCardActionActivity</strong> structure in <strong>Data - Structures - Messaging</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743919729126/e5c47aec-1343-467b-99ee-6ae242e38f07.png" alt class="image--center mx-auto" /></p>
<p>In addition to the <strong>Url</strong> data item, this structure includes an extra attribute called <strong>Bid</strong>, which deserializes the <strong>bid</strong> value sent to the messaging endpoint.</p>
<h1 id="heading-messaging-endpoint"><strong>Messaging Endpoint</strong></h1>
<p>In <strong>Logic - Integrations - REST - MessagingEndpoint</strong>, open the <strong>Messages</strong> endpoint. In the previous part of the series, the second switch statement "Verb" had only one condition for the refresh action. We now add a second condition for the <strong>placeBidding</strong> verb defined in the <strong>AuctionCard</strong> template.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743919472957/3915ba82-0742-4753-8244-e467d2195845.png" alt class="image--center mx-auto" /></p>
<p>The MessagingAuctionBidAction server action processes the deserialized Activity and returns an updated card.</p>
<h1 id="heading-bid-action">Bid Action</h1>
<p>Switch to <strong>Logic - Server Actions - Messaging</strong> and review the <strong>MessagingAuctionBidAction</strong> server action.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743920020297/5e1f5c64-ff26-4d98-8346-a0b52310f3b7.png" alt class="image--center mx-auto" /></p>
<p>You will note that this action is almost identical to the refresh action with the following exceptions</p>
<ul>
<li><p><strong>Auction_AddBidding</strong> - Saves the received bid in the <strong>Bid</strong> entity.</p>
</li>
<li><p><strong>RenderAuctionCard</strong> - Displays the updated Auction card with the user's bid. (see note below)</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Important note</strong>: The demo does not verify if the bid is higher than the current highest bid. I wanted to keep the demo as simple as possible. Feel free to add extra checks as you see fit.</div>
</div>

<h1 id="heading-try"><strong>Try</strong></h1>
<p>Paste an auction link from the demo application into a Microsoft Teams conversation. This will first display the auction card.</p>
<p>Next, change the auction price in the demo application.</p>
<p>In Microsoft Teams, click on a different conversation, then return to the conversation where you pasted the link. The card will update to show the new price you entered.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note that cards do not update while you remain in the conversation. They automatically refresh only when you re-enter the conversation.</div>
</div>

<p>Place a bid and see if the card updates with the new value.</p>
<p>Close a bid in the web application and check if the card updates in the channel.</p>
<h1 id="heading-summary">Summary</h1>
<p>In this tutorial, we explored how to add form elements to an Adaptive Card-based Loop Component and submit form data to the messaging endpoint using a card action.</p>
<p>I hope you enjoyed it. Please leave a comment with your feedback or any questions you have.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[Keep Adaptive Cards Up to Date]]></title><description><![CDATA[At the end of the previous part of this series, we were able to copy a link from our OutSystems Developer Cloud application, paste it into a Microsoft Teams conversation, and have our messaging endpoint "unfurl" this link into an Adaptive Card Loop c...]]></description><link>https://without.systems/odc-with-loop-components-adaptive-card-refresh</link><guid isPermaLink="true">https://without.systems/odc-with-loop-components-adaptive-card-refresh</guid><category><![CDATA[outsystems]]></category><category><![CDATA[Microsoft365]]></category><category><![CDATA[azure bot service]]></category><category><![CDATA[Bot Framework]]></category><category><![CDATA[#adaptive-cards]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Wed, 19 Mar 2025 08:49:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742277236962/c9126d94-55f5-48bf-934c-fe9868fd636e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At the end of the previous part of this series, we were able to copy a link from our OutSystems Developer Cloud application, paste it into a Microsoft Teams conversation, and have our messaging endpoint "unfurl" this link into an Adaptive Card Loop component. However, the cards displayed in a Microsoft Teams conversation are still static, meaning they do not update when the data in our application changes, such as the price of an auction. One of the <a target="_blank" href="https://without.systems/odc-with-loop-components-introduction#heading-loop-components-overview">key characteristics of a Loop component</a> is that it is "<strong>Live</strong>," meaning it automatically updates its content when the bound data changes.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">There are some situations where a Loop component doesn't need to be live, and a static card is enough. For example, if you have versioned data and you share just one version of that data in a conversation.</div>
</div>

<p>In this tutorial, we explore how to refresh an Adaptive Card-based Loop component using a <a target="_blank" href="https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/overview">Universal Action for Adaptive Cards</a>. Universal Actions are Adaptive Card actions that send an <strong>Activity</strong> of type “<strong>invoke</strong>” to the messaging endpoint with additional data and Refresh is a special type of a Universal Action.</p>
<h1 id="heading-demo-application"><strong>Demo Application</strong></h1>
<p>This article series includes a demo application called "<strong>ODC with Loop Components Demo</strong>," available on <strong>ODC Forge</strong>. Be sure to download the version of the application that matches each article in this series.</p>
<p>For this article, you need to install Version 0.2 from ODC Forge.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>ODC with Loop Components Demo</strong>".</p>
</li>
<li><p>Click on the Asset <strong>(Do not click on Install on the tile!)</strong>.</p>
</li>
<li><p>Switch to the <strong>Version</strong> tab and click on Install next to <strong>Version 0.2</strong>.</p>
</li>
</ul>
<p>Version 0.2 depends on other Forge components:</p>
<ul>
<li><p><strong>OAuthTokenExchange</strong> - An external logic library that helps retrieve access tokens easily. In this tutorial however we use an action to retrieve the discovery document from Microsoft Entra only.</p>
</li>
<li><p><strong>LiquidFluid</strong> - An external logic library that merges data with templates based on the Shopify Liquid Templating language.</p>
</li>
<li><p><strong>UriParser</strong> - An external logic library that parses a given URI/URL.</p>
</li>
</ul>
<p>After updating the demo application in your environment, you can try it out right away because the App Manifest—the configuration file that added the app to Microsoft Teams—has not changed. Note that only new Loop components will refresh, while all previously added cards in a channel will stay the same.</p>
<h1 id="heading-recap">Recap</h1>
<p>In part one of the series, our messaging endpoint only responded to Activities of type "<strong>invoke</strong>" with the name "<strong>composeExtension/queryLink</strong>." To create this response, we used a single liquid template (based on the Shopify Liquid templating language) and merged it with auction data.</p>
<p>The response looked like this</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"composeExtension"</span>: {
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"result"</span>,
    <span class="hljs-attr">"attachmentLayout"</span>: <span class="hljs-string">"list"</span>,
    <span class="hljs-attr">"attachments"</span>: [
      {
        <span class="hljs-attr">"preview"</span>: &lt;&lt;Preview Adaptive Card&gt;&gt;
        <span class="hljs-string">"content"</span>: &lt;&lt;Full Adaptive Card&gt;&gt;
      }
    ]
  }
}
</code></pre>
<p>“<strong>preview</strong>” contains a simplified Adaptive Card (a thumbnail card), while “<strong>content</strong>” holds the full details of the Adaptive Card. Consider this response as a one-time message from your Messaging Endpoint to initially display an Adaptive Card in a channel.</p>
<p>"One-time" means we cannot reuse the same liquid template for future responses, such as refreshing a card (<em>covered in this tutorial</em>) or responding to user interactions (<em>covered in the next tutorial</em>). Future responses must include only the full Adaptive Card, which means just the “<strong>content</strong>” object from the response shown above.</p>
<p>In summary</p>
<ul>
<li><p><strong>composeExtension/queryLink</strong> - Respond with a “composeExtension” object containing an attachment with a preview and full card.</p>
</li>
<li><p><strong>other</strong> - Respond with an Adaptive Card only.</p>
</li>
</ul>
<p>To accomodate this we have to split our single liquid template into multiple templates. Let’s get started.</p>
<h1 id="heading-templates">Templates</h1>
<p>Open the <strong>ODC with Loop Components Demo</strong> application in ODC Studio and go to <strong>Data - Resources - Templates</strong>.</p>
<p>Note the various liquid templates in the folder, download them locally and inspect them with a text editor.</p>
<ul>
<li><p><strong>AuctionCard</strong> - This template represents a full Adaptive Card for an auction.</p>
</li>
<li><p><strong>AuctionCardPreview</strong> - This is the preview version of the auction card.</p>
</li>
<li><p><strong>AuctionQueryLinkResponse</strong> - This template is used to create a composeExtension response for link unfurling requests. It combines the results of the AuctionCard and AuctionCardPreview templates.</p>
</li>
<li><p><strong>StatusValueResponse</strong> - This template is combined with the result of an Auction card and is used for follow-up responses of an auction card, such as when refreshing a card. It includes a "status code," a "type," and a "value”. In our case the value will always be an Adaptive Card.</p>
</li>
</ul>
<h2 id="heading-render-auctioncard-and-auctioncardpreview">Render AuctionCard and AuctionCardPreview</h2>
<p>Go to <strong>Logic - Server Actions - Loop</strong> and check both <strong>RenderAuctionCard</strong> and <strong>RenderAuctionCardPreview</strong>. They are similar, but they use different templates and have a different input parameter structure.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742274265500/f3360e56-75bb-4d1a-a87d-803df7e78b5c.png" alt class="image--center mx-auto" /></p>
<p>First, the card payload data is serialized and then merged with the liquid template in Resources. Finally, the rendered template is returned.</p>
<h2 id="heading-render-querylinkresponse">Render QueryLinkResponse</h2>
<p>Inspect the <strong>RenderQueryLinkResponse</strong> server action. This action creates the queryLink response by combining the results of <strong>RenderAuctionCard</strong> and <strong>RenderAuctionCardPreview</strong> into a liquid template.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742275094912/a0436528-57c3-4c10-a6f2-850caf4d442c.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>RenderAuctionPreviewCard</strong> - Returns the rendered preview card.</p>
</li>
<li><p><strong>RenderAuctionCard</strong> - Returns the full auction card.</p>
</li>
<li><p><strong>UnfurlPayload</strong> - Assigns both the preview and full card to a local structure instance of AuctionQueryLink.</p>
</li>
<li><p><strong>SerializeUnfurlPayload</strong> - Serializes the payload to a string (LiquidFluid only accepts JSON data as a string to merge with a template).</p>
</li>
<li><p><strong>RenderTemplate</strong> - Renders the AuctionQueryLinkResponse template.</p>
</li>
</ul>
<h2 id="heading-render-statusvalueresponse">Render StatusValueResponse</h2>
<p>As mentioned above, subsequent responses for an existing card must only include the Adaptive Card. However, that card must be wrapped in a response structure that also returns a status code and type. The RenderStatusValueResponse server action wraps a rendered auction card in the required response structure.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742276082352/9ad51925-3d3d-4572-8751-3c5c7a162687.png" alt class="image--center mx-auto" /></p>
<p>The process here is straightforward, and you should already be familiar with it. Here is an example of what this action produces.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"statusCode"</span>: <span class="hljs-number">200</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"application/vnd.microsoft.card.adaptive"</span>,
  <span class="hljs-attr">"value"</span>: {
    <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"http://adaptivecards.io/schemas/adaptive-card.json"</span>,
    <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.6"</span>,
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"AdaptiveCard"</span>,
    <span class="hljs-attr">"metadata"</span>: {
      <span class="hljs-attr">"webUrl"</span>: <span class="hljs-string">"https://&lt;&lt;ODC Stage&gt;&gt;/ODCwithLoopComponentsDemo/AuctionDetail?AuctionId=9f890964-b33d-4817-8fa7-41171c8a0c3c"</span>
    },
    <span class="hljs-attr">"refresh"</span>: {
      <span class="hljs-attr">"action"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Action.Execute"</span>,
        <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Refresh"</span>,
        <span class="hljs-attr">"verb"</span>: <span class="hljs-string">"refreshAuctionCard"</span>,
        <span class="hljs-attr">"data"</span>: {
          <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://&lt;&lt;ODC Stage&gt;&gt;/ODCwithLoopComponentsDemo/AuctionDetail?AuctionId=9f890964-b33d-4817-8fa7-41171c8a0c3c"</span>
        }
      }
    },
    <span class="hljs-attr">"body"</span>: [
      {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
        <span class="hljs-attr">"size"</span>: <span class="hljs-string">"medium"</span>,
        <span class="hljs-attr">"weight"</span>: <span class="hljs-string">"bolder"</span>,
        <span class="hljs-attr">"text"</span>: <span class="hljs-string">"Mein bestes Produkt"</span>
      },
      {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"ColumnSet"</span>,
        <span class="hljs-attr">"columns"</span>: [
          {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Column"</span>,
            <span class="hljs-attr">"width"</span>: <span class="hljs-string">"stretch"</span>,
            <span class="hljs-attr">"items"</span>: [
              {
                <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                <span class="hljs-attr">"text"</span>: <span class="hljs-string">"was ich jemands erste"</span>,
                <span class="hljs-attr">"wrap"</span>: <span class="hljs-literal">true</span>
              }
            ],
            <span class="hljs-attr">"verticalContentAlignment"</span>: <span class="hljs-string">"Center"</span>,
            <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"Medium"</span>
          },
          {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Column"</span>,
            <span class="hljs-attr">"width"</span>: <span class="hljs-string">"auto"</span>,
            <span class="hljs-attr">"items"</span>: [
              {
                <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                <span class="hljs-attr">"text"</span>: <span class="hljs-string">"500.0"</span>,
                <span class="hljs-attr">"wrap"</span>: <span class="hljs-literal">true</span>,
                <span class="hljs-attr">"size"</span>: <span class="hljs-string">"ExtraLarge"</span>,
                <span class="hljs-attr">"weight"</span>: <span class="hljs-string">"Bolder"</span>
              }
            ]
          }
        ],
        <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"None"</span>
      }
    ]
  }
}
</code></pre>
<h1 id="heading-refresh-action">Refresh Action</h1>
<p>Before we dive into how everything comes together at the messaging endpoint, let's quickly explore how a channel (like Microsoft Teams) knows that an Adaptive Card needs to be refreshed and how our messaging endpoint identifies which auction card should be updated.</p>
<p>Open the <strong>AuctionCard</strong> template in a text editor.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"http://adaptivecards.io/schemas/adaptive-card.json"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.6"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"AdaptiveCard"</span>,
  <span class="hljs-attr">"metadata"</span>: {
    <span class="hljs-attr">"webUrl"</span>: <span class="hljs-string">"{{webUrl}}"</span>
  },
  <span class="hljs-attr">"refresh"</span>: {
    <span class="hljs-attr">"action"</span>: {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Action.Execute"</span>,
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Refresh"</span>,
      <span class="hljs-attr">"verb"</span>: <span class="hljs-string">"refreshAuctionCard"</span>,
      <span class="hljs-attr">"data"</span>: {
        <span class="hljs-attr">"url"</span>: <span class="hljs-string">"{{webUrl}}"</span>
      }
    }
  },
  <span class="hljs-attr">"body"</span>: ....,
}
</code></pre>
<p>What's new is a “<strong>refresh</strong>” object that defines an <strong>action</strong>.</p>
<ul>
<li><p><strong>type</strong> - This is always Action.Execute.</p>
</li>
<li><p><strong>title</strong> - Besides automatic card refreshes, Microsoft Teams and other channel applications show a three-dot menu next to the card to manually execute the action. The title you enter here becomes the menu option name.</p>
</li>
<li><p><strong>verb</strong> - This is any string that helps us identify which action is requested. At the messaging endpoint, we use this verb to recognize the refresh request.</p>
</li>
<li><p><strong>data</strong> - This is additional data sent to the messaging endpoint when the refresh action is requested. Here, we define a single item “url.” The handlebars <code>{{ webUrl }}</code> are replaced with the full URL of the Loop component (auction) when merging the template with data in the <strong>RenderAuctionCard</strong> server action.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Automatic card updates have some channel-specific limitations you might need to consider. You can read more about it here: <a target="_self" href="https://learn.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model#refresh-mechanism">Universal Action Model - Adaptive Cards | Microsoft Learn</a>.</div>
</div>

<h1 id="heading-messaging-endpoint">Messaging Endpoint</h1>
<p>In <strong>Logic - Integrations - REST - MassgingEndpoint</strong> open the <strong>Messages</strong> endpoint. In the last part the first switch statement had only one condition for the link unfurling (<strong>type</strong> of “<strong>invoke</strong>” and <strong>name</strong> of “<strong>composeExtension/queryLink</strong>”).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742281483699/785c0359-e1ab-4a81-9641-c7cd606436c7.png" alt class="image--center mx-auto" /></p>
<p>Adaptive Card-based actions, whether it's a refresh or any other defined action in the card, send an activity request of <strong>type</strong> "<strong>invoke</strong>" with a <strong>name</strong> of "<strong>adaptiveCard/action</strong>". This invocation is now handled in the second condition added to the switch statement.</p>
<p>Right after <strong>DeserializeAdaptiveCardActionActivity</strong> is called, which deserializes the request into a structure that provides access to two important pieces of information:</p>
<ul>
<li><p><strong>verb</strong> - The action verb as defined in the Auction Adaptive Card. We use the verb to identify which action is requested.</p>
</li>
<li><p><strong>data</strong> - Additional data defined in the Auction Adaptive Card and passed to the messaging endpoint. Currently, only the Loop component URL is passed.</p>
</li>
</ul>
<p>The next <strong>switch</strong> statement currently has a single condition that checks the <strong>verb</strong> and executes <strong>MessagingRefreshAuctionAction</strong>.</p>
<h2 id="heading-messagingrefreshauctionaction">MessagingRefreshAuctionAction</h2>
<p>This action generates the response that our messaging endpoint needs to send back to the channel. It includes an Adaptive Card for the auction, wrapped in a Status response structure.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742281870743/6c720d9a-e000-4297-93f2-5c032d62d651.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>Auction_GetByUrl</strong> - This auction parses the given URL (sent via the invoke activity) for the auction identifier. Then it queries the Auction entity and returns the auction details.</p>
</li>
<li><p><strong>RenderAuctionCard</strong> - see above. Renders the Adaptive Card for the auction.</p>
</li>
<li><p><strong>RenderStatusValueResponse</strong> - see above. Wraps the auction card in a status response structure.</p>
</li>
</ul>
<h1 id="heading-try">Try</h1>
<p>Paste an auction link from the demo application into a Microsoft Teams conversation. This will first display the auction card.</p>
<p>Next, change the auction price in the demo application.</p>
<p>In Microsoft Teams, click on a different conversation, then return to the conversation where you pasted the link. The card will update to show the new price you entered.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note that cards do not update while you remain in the conversation. They automatically refresh only when you re-enter the conversation.</div>
</div>

<h1 id="heading-summary">Summary</h1>
<p>In this tutorial, we explored how to keep Adaptive Card-based Loop components up to date in Microsoft Teams by adding a refresh action to a card and implementing a messaging endpoint action to return the most recent Adaptive Card for an auction.</p>
<p>The steps we implemented also lay the foundation for the next part of the tutorial, where we will add user interactions to the card.</p>
<p>I hope you enjoyed it. Please leave a comment with your feedback or any questions you have.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[AI Agent Orchestration with OutSystems Developer Cloud]]></title><description><![CDATA[AI is everywhere. In every discussion I've had in recent months, AI capabilities and development with AI models have come up. I often notice that when people talk about implementation, they quickly mention custom code AI frameworks, especially for AI...]]></description><link>https://without.systems/agent-orchestration-with-outsystems-developer-cloud</link><guid isPermaLink="true">https://without.systems/agent-orchestration-with-outsystems-developer-cloud</guid><category><![CDATA[outsystems]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[Amazon Bedrock]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Mon, 10 Mar 2025 11:25:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741289494984/70a68e7f-22cc-4b12-b230-05112c704033.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AI is everywhere. In every discussion I've had in recent months, AI capabilities and development with AI models have come up. I often notice that when people talk about implementation, they quickly mention custom code AI frameworks, especially for AI agent orchestration. Frameworks like crewAI, for example. For some reason, OutSystems Developer Cloud doesn't even cross their minds in this area, even though they use ODC for various other developments. From my perspective, that's unfortunate. It's true that custom code frameworks like crewAI offer a lot for agentic AI, including many built-in utilities, tools, and safeguards, but in the end, they are just code.</p>
<p>In this lab, I want to show that OutSystems Developer Cloud is more than capable of building agentic AI solutions. The initial effort might be higher than with a custom code AI framework, but on the other hand, you save on setting up a deployment pipeline. (….. and python 😎)</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This doesn't mean that ODC is suitable for every agentic use case scenario. Personally I love crewAI. It is a very powerful framework for even the most complex use-cases.</div>
</div>

<h1 id="heading-demo-application">Demo Application</h1>
<p>This lab includes a component on Forge called “<strong>Agentic AI Lab</strong>”.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">My Lab components are not complete demo applications but are quickly drafted implementations that show a specific pattern. Please note that these Lab components are not fully tested and will likely need more tuning and refactoring.</div>
</div>

<p>Dependencies:</p>
<ul>
<li><p><strong>AWSBedrockRuntime</strong> - External Logic Connector library for Amazon Bedrock.</p>
</li>
<li><p><strong>LiquidFluid</strong> - External Logic Utility library. This library is used to merge text templates with placeholders with data using the Shopify Liquid Templating language.</p>
</li>
</ul>
<p>The Lab uses Amazon Bedrock models. You will need AWS credentials with access to the following models in the <strong>us-east-1</strong> region.</p>
<ul>
<li><p><strong>amazon.nova-micro-v1:0</strong></p>
</li>
<li><p><strong>us.anthropic.claude-3-5-sonnet-20241022-v2:0</strong> (cross region inference)</p>
</li>
</ul>
<p>After installing the Lab into your ODC development environment, set the application settings for the <strong>AWS Access Key</strong> and <strong>AWS Secret Access Key</strong>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Although this lab uses Amazon Bedrock, it can easily be adapted to other AI runtimes and models, but that is not covered in this article.</div>
</div>

<h1 id="heading-introduction">Introduction</h1>
<p>Let's begin by introducing some important terms and definitions related to agents and agent orchestration.</p>
<h2 id="heading-model-and-prompt">Model and Prompt</h2>
<p>Let’ start with AI models and prompts. Sometimes, I notice misunderstandings about how AI models work, so let's clear a few things up.</p>
<ul>
<li><p>AI models are stateless, meaning they do not store anything. Everything you think the model needs as input must be included in the prompt, which is then sent to the model to produce a result.</p>
</li>
<li><p>The prompt sent to the model is plain text. The API, such as the Chat Completion API from OpenAI, is an abstraction on top of the model that makes it easier to create a prompt with the necessary model-specific instruction tokens.</p>
</li>
</ul>
<p>The <a target="_blank" href="https://platform.openai.com/docs/api-reference/chat">OpenAI Chat Completion API</a> has become a de facto standard in the market. Most major vendors offer an equivalent or similar API to interact with their models.</p>
<p>At a glance, the API allows you to easily build a prompt for the model with:</p>
<ul>
<li><p><strong>System Prompt</strong> - A system prompt is a set of predefined instructions and guidelines given to an AI model to shape its responses and behavior. It defines the context, tone, and boundaries within which the AI operates.</p>
</li>
<li><p><strong>Message Conversation</strong> - An array of messages between a user and the model (assistant) that form a conversation history.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The <strong>System Prompt</strong> and <strong>Message Conversation</strong> together provide the context available to a model.</div>
</div>

<ul>
<li><strong>Tools</strong> - (not applicable to all models) A tool definition includes a tool name, a description, and a JSON schema. The model examines the entire context to determine if a tool should be used based on the description. If necessary, tool parameters defined in the JSON schema are extracted from the context. Once all required tool parameters are extracted, the model returns a "Tool Use" result with the tool name and its extracted parameters. It is up to your application to execute the actual tool and add the results back to the Message Conversation.</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">A model cannot execute an external tool on its own.</div>
</div>

<ul>
<li><strong>Configurations</strong> - Additional model settings. Here, you can usually specify the maximum number of tokens—words or parts of words—that the model should generate, and also how creative the output should be.</li>
</ul>
<h2 id="heading-agents">Agents</h2>
<p>An AI agent is like a role in a company, with specific tasks and decision-making abilities, and access to tools for completing tasks or gathering information to make informed decisions. Employees in a role also bring their own knowledge and experience, which helps them handle tasks and interact with other employees and roles in the company.</p>
<p>When defining an AI agent, you consider the same questions as when defining a company role and its responsibilities (tasks). However, there is one difference: while company roles often cover a wide range of topics, AI agents are designed to focus on a specific topic and related tasks.</p>
<ul>
<li><p><strong>Purpose</strong> - The purpose of the agent and a description of what it can do.</p>
</li>
<li><p><strong>Goals</strong> - A description of the goals and objectives the agent aims to achieve, along with a definition of the concrete results the agent produces.</p>
</li>
<li><p><strong>Instructions</strong> - Guidelines on how the agent should operate and interact with its environment (e.g., a user) to achieve its goals.</p>
</li>
<li><p><strong>Tools</strong> - A definition of the tools it must or can use to produce the results.</p>
</li>
<li><p><strong>Constraints</strong> - Additional safeguards and constraints that must be considered when the agent performs its tasks. (These exist for human roles too, though they might not always be written down).</p>
</li>
</ul>
<p>From a technical perspective an agent would consist of</p>
<ul>
<li><p><strong>Model</strong> - An AI model that powers the agent. The <strong>Purpose</strong>, <strong>Goals</strong>, <strong>Instructions</strong>, and <strong>Constraints</strong> serve as general guidelines for its behavior. The AI model you choose to power an agent can depend on several criteria, such as its ability to infer on a specific topic, the maximum context window, or its proficiency in a particular language.</p>
</li>
<li><p><strong>Tools</strong> - One or more tool definitions. Remember, a model cannot run a tool on its own. It can only decide if a tool should be used and identify the necessary input parameters from the context, leaving the actual tool execution to your application. A provided tool is simply a combination of a description and the required input parameter definitions. Tools supply on-demand data to the context.</p>
</li>
<li><p><strong>Knowledge</strong> - Additional information added to give the agent more context, helping it perform tasks better. This is like the extra knowledge and experience a human agent might have.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">At the time of writing, <a target="_self" href="https://success.outsystems.com/documentation/outsystems_developer_cloud/building_apps/about_ai_agent_builder/">OutSystems AI Agent Builde</a>r allows you to configure an agent that meets all the criteria above.</div>
</div>

<h2 id="heading-agent-orchestration">Agent Orchestration</h2>
<p>Agent Orchestration involves <strong>managing and coordinating multiple AI agents</strong> to complete complex tasks and goals. An orchestrator is an agent designed to choose the best agents from a group to perform tasks. Different development frameworks for agentic AI solutions use various names for this agent, such as orchestrator, supervisor, or dispatcher. Besides selecting and activating agents, the orchestrator handles the overall context information and ensures that an agent receives the necessary context information when needed.</p>
<p>The main idea of agent orchestration with highly capable agents is that with minimal input data, the entire process can run independently. Agents collaborate to gather information, make decisions, and produce a result, such as placing an order with a vendor. However, fully automated orchestrations are not quite common right now. Instead, <strong>orchestrations linked to a chat conversation</strong> with a human in the loop are more typical.</p>
<ol>
<li><p>A user sends a message to a chat, which is added to the orchestration context.</p>
</li>
<li><p>The orchestrator reviews the context and, if needed, selects an agent to handle the user's message. The orchestrator also provides the full context or parts of it to the agent.</p>
</li>
<li><p>The agent processes the message and produces a result, which could be the final outcome or a request for clarification or more information.</p>
</li>
<li><p>The orchestrator adds the agent's output to the overall context and displays it to the user in the chat.</p>
</li>
<li><p>Repeat from Step 1.</p>
</li>
</ol>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Between this approach and the fully automated one mentioned earlier, there are many other orchestration strategies. Some organizations have already implemented partially automated and even fully automated orchestrations of agents using various implementation patterns.</div>
</div>

<h1 id="heading-why-you-should-care">Why you should care</h1>
<p>An orchestrated collection of AI agents with data and tools is excellent for automation and offers many advantages over using a single model and single agent.</p>
<ul>
<li><p><strong>Automation</strong> - You can begin with an agentic human-in-the-loop orchestration and gradually move towards more automation by sending one agent's output directly to another agent. As your agents become more advanced, you'll need a human less often to check or manually adjust results.</p>
</li>
<li><p><strong>Model</strong> - You can choose the best model for your agent to perform its tasks from thousands of available foundational and fine-tuned models, including special-purpose models optimized for tasks like data extraction.</p>
</li>
<li><p><strong>Cost</strong> - Whether in your data center or in the cloud, using models incurs costs. By choosing smaller and therefore less expensive models for simpler tasks instead of relying on a single, large, and costly general-purpose model, you can significantly reduce your overall costs.</p>
</li>
<li><p><strong>Performance</strong> - Smaller models are typically faster at inferring than large models, resulting in better performance.</p>
</li>
<li><p><strong>Hallucination</strong> - Using focused agents with an orchestrator lets you carefully manage the context for a model's use, generally keeping the model's context smaller and reducing the risk of model hallucinations.</p>
</li>
<li><p><strong>Tool Overlap</strong> - Instead of defining many tools in a single model invocation, you attach tools to individual agents. This approach reduces the chance of the model choosing the wrong tool due to ambiguous tool descriptions.</p>
</li>
</ul>
<h1 id="heading-lab-outline">Lab Outline</h1>
<p>In this lab we a are building a human-in-the-loop agentic solution. We are going to implement two agents:</p>
<ul>
<li><p><strong>Product Agent</strong> - This agent acts as a product manager in our company. In our implementation this agent is only responsible for creating new products in our company product catalog. Meaning that the agent will create a new product record in an entity of our application.</p>
</li>
<li><p><strong>LinkedIn Agent</strong> - This agent is a social media expert and drafts LinkedIn posts. In our implementation it is just an AI model with some instructions.</p>
</li>
</ul>
<p>And, of course, an <strong>Orchestrator</strong> that evaluates which agent is best suited to handle a request from a conversation.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Both agents are quite limited because the main focus of this lab is on orchestration, not on the individual agent implementations.</div>
</div>

<h1 id="heading-agent-and-orchestrator-implementation">Agent and Orchestrator Implementation</h1>
<p>Let's walk through the important steps to implement an agent and an orchestrator. Open the <strong>Agent AI Lab</strong> app in ODC Studio.</p>
<h2 id="heading-agent">Agent</h2>
<p>Both agents are defined in a static entity <strong>AgentSettings</strong> under <strong>Data - Entities - Database</strong>. Each agent record includes the following properties:</p>
<ul>
<li><p><strong>Name</strong> - A short name that also serves as the record ID to identify the agent.</p>
</li>
<li><p><strong>Model</strong> - The Amazon Bedrock model the agent will use. Note that the Product Agent uses Amazon Nova Micro, a very small yet fast model, while the LinkedIn Agent uses the Anthropic Sonnets Claude model.</p>
</li>
<li><p><strong>Role</strong> - The agent's role title.</p>
</li>
<li><p><strong>Description</strong> - A description of the agent's capabilities.</p>
</li>
<li><p><strong>Goal</strong> - The outcome the agent aims to achieve.</p>
</li>
<li><p><strong>Instruction</strong> - Additional instructions for the agent.</p>
</li>
</ul>
<p>Open the server action <strong>Agent_ProductManager</strong> in <strong>Logic - Server Actions - Agents</strong>. Unlike the LinkedIn agent, the Product Manager agent has an attached tool. Let's go through this one to explore the implementation details.</p>
<p>In general, the action flow of an agent consists of the following steps:</p>
<ul>
<li><p>Retrieve agent settings from the AgentSettings static entity</p>
</li>
<li><p>Build the system prompt for the agent</p>
</li>
<li><p>Add tool specification(s)</p>
</li>
<li><p>Invoke the model</p>
</li>
<li><p>Handle tool execution (if requested by the model)</p>
</li>
<li><p>Return the response</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741494826727/505d5536-71bd-4108-b7a7-f2a57c7e9a77.png" alt class="image--center mx-auto" /></p>
<p>Lets take a close look at <strong>RenderAgentSystemPrompt</strong></p>
<h3 id="heading-renderagentsystemprompt-action">RenderAgentSystemPrompt Action</h3>
<p>This action uses the RenderTemplate action from the LiquidFluid external logic Forge component. The template, AgentPrompt, is located in Data - Resources - Agents. LiquidFluid allows you to merge JSON data with a text template that contains handlebars placeholders based on the Shopify Liquid Templating language.</p>
<p>The template for an agent looks like this:</p>
<pre><code class="lang-markdown">You are an {{ role }}.  You will engange in an open-ended conversation, providing helpful and accurate information.

<span class="hljs-strong">**Scope**</span>

Your scope of providing assistance is limited to the following:

{{ description }}.

<span class="hljs-strong">**Instructions**</span>

{{ instructions }}

<span class="hljs-strong">**Goal and expected result**</span>
{{ goal }}

<span class="hljs-strong">**Additional Instructions**</span>

<span class="hljs-bullet">-</span> Provide well-reasoned responses that directly address the users intent
<span class="hljs-bullet">-</span> Provide additional explanations where appropiate
<span class="hljs-bullet">-</span> Ask for clarification if parts of the question are ambigous.
<span class="hljs-bullet">-</span> Ask for additional information if the context does not provide sufficient information
<span class="hljs-bullet">-</span> Do assist in user questions or intents that are outside of the given scope. Decline such requests and respond respectfully.
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741495255177/e714882a-b611-4c69-aafe-2d6a8d3c7396.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>TemplateData</strong> - Here we assign parameters to a local structure variable that matches the handlebar placenholders in the template.</p>
</li>
<li><p><strong>SerializeTemplateData</strong> - Generates the stringified JSON document that is required for the LiquidFluid external logic.</p>
</li>
<li><p><strong>RenderTemplate</strong> - Merges the data with the agent template from Data - Resources - Agents</p>
</li>
<li><p><strong>Prompt</strong> - Assigned the rendered prompt to the output.</p>
</li>
</ul>
<h3 id="heading-getproductcreatetoolspec-action">GetProductCreateToolSpec Action</h3>
<p>Switch to the <strong>GetProductCreateToolSpec</strong> server action in <strong>Logic - Server Actions - Tools</strong>.</p>
<p>A tool specification consists of the following:</p>
<ul>
<li><p><strong>Name</strong> - A tool descriptor that identifies a tool.</p>
</li>
<li><p><strong>Description</strong> - A detailed explanation of the tool and when it should be used.</p>
</li>
<li><p><strong>Schema</strong> - A JSON schema with required and optional input parameters.</p>
</li>
</ul>
<p>Our Product Create Tool includes the following:</p>
<ul>
<li><p>Name - <strong>create_product</strong></p>
</li>
<li><p>Description - <strong>Creates a new product in the product catalog. This tool requires a user to specify a name, description, and price.</strong></p>
</li>
</ul>
<p>The parameter schema is loaded from <strong>Data - Resources - Tools</strong> and looks like this:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
    <span class="hljs-attr">"properties"</span>: {
        <span class="hljs-attr">"name"</span>: {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
            <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Name of the product"</span>
        },
        <span class="hljs-attr">"description"</span>: {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
            <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Short description of the product"</span>
        },
        <span class="hljs-attr">"price"</span>: {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"number"</span>,
            <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Price of the product."</span>
        }
    },
    <span class="hljs-attr">"required"</span>: [<span class="hljs-string">"name"</span>, <span class="hljs-string">"description"</span>, <span class="hljs-string">"price"</span>]
}
</code></pre>
<p>The schema defines three required properties name, description and price.</p>
<h3 id="heading-handle-model-response">Handle Model response</h3>
<p>The most interesting part is handling the model response.</p>
<p>After running the Converse action, the model can respond with either a text message or a tool use request. The IF condition after the model call checks for this.</p>
<p>If it's a regular response, we simply return the text response generated by the model. This might happen if the user didn't provide enough information for the Product Create tool.</p>
<p>If it's a tool request (tool_use), we know it can only be the Product Create tool since we have only one tool attached. If you have multiple tools assigned, you would need to adjust the flow to identify which tool is requested and take the appropriate action.</p>
<ul>
<li><p><strong>FilterCreateProduct</strong> - Here, we filter the list of messages from the Converse action for <code>ToolUse.Name = "create_product"</code>.</p>
</li>
<li><p><strong>DeserializeToolInput</strong> - The <code>FilterCreateProduct.FilteredList.Current.ToolUse.Input</code> property contains the extracted and serialized tool parameters that we deserialize into a <strong>Product_Create</strong> structure.</p>
</li>
<li><p><strong>RunProductCreateTool</strong> - This action creates a new record in our <strong>Product</strong> entity and returns a response text.</p>
</li>
</ul>
<p>Finally, we return the response text from the <strong>RunProductCreateTool</strong> to the orchestrator.</p>
<p>Take a moment to look at the LinkedIn agent too. You'll notice that this agent is much simpler because it doesn't have an attached tool.</p>
<h2 id="heading-orchestrator">Orchestrator</h2>
<p>The Orchestrator is the "heart" of our implementation. It manages and stores the <strong>conversation history</strong>, decides which agent should handle a user's input, and calls the agent implementation.</p>
<p>Open the <strong>Orchestrator</strong> server action in <strong>Logic - Server Actions - Agents</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741498007494/ce9478d6-ba00-4d25-9408-379de58f9430.png" alt class="image--center mx-auto" /></p>
<p>The Orchestrator takes the last user input message as a parameter and then performs the following steps:</p>
<ul>
<li><p><strong>Message_Query</strong> - Retrieves the message history from the Message entity.</p>
</li>
<li><p><strong>ListAgents</strong> - Retrieves the agent settings from the AgentSettings static entity.</p>
</li>
<li><p><strong>AgentClassifier</strong> - Determines the most suitable agent to respond to the user's input message based on the message history. See details below.</p>
</li>
<li><p><strong>Run Agent</strong> - Executes one of the determined agents.</p>
</li>
<li><p><strong>SaveMessageTurn</strong> - Saves both the user input message and the model response to the Message entity.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">We only focus on successful message turns here and simply ignore conversation turns where, for example, an agent could not be determined.</div>
</div>

<h3 id="heading-agent-classifier">Agent Classifier</h3>
<p>The <strong>AgentClassifier</strong> server action in <strong>Logic - Server Actions - Agents</strong> is responsible for selecting one of our agents based on user input and the message context. You'll notice that its implementation resembles a typical agent implementation, and that's exactly what it is.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741499237260/eb9370af-ad1b-4799-a374-5ebe4d36522f.png" alt class="image--center mx-auto" /></p>
<p>There are only two differences between the Agent Classifier and a regular agent: the prompt and the tool specification.</p>
<p>The system prompt for the classifier looks like this.</p>
<pre><code class="lang-markdown">You are an agent dispatcher, an intelligent assistant who analyzes and evaluates requests and then forwards these requests to the most suitable agent. Your job is to understand a request, identify key entities and intentions and determine which of the available agents is best suited to handle a request.

<span class="hljs-strong">**Important**</span>: A request can also be a continuation of a previous request. The conversation history together with the short name of the last agent used is provided. If a request looks as if it is a continuation of a previous request, then use the same agent as before.

Analyze the request and determine exactly one of the following available agents.

<span class="hljs-strong">**Available Agents**</span>
Here is the list of available agents.

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">agents</span>&gt;</span></span>
  {% for agent in agents %}
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">agent</span>&gt;</span></span>
<span class="hljs-code">    &lt;shortName&gt;{{ agent.shortName }}&lt;/shortName&gt;
    &lt;description&gt;{{ agent.description }}&lt;/decription&gt;
  &lt;/agent&gt;
  {% endfor %}
&lt;/agents&gt;
</span>
<span class="hljs-strong">**Instructions for Agent classification**</span>

<span class="hljs-bullet">-</span> Agent Shortname: Choose the most appropiate agent shortname based on the nature of the request. For follow-up requests use the the same agent shortname as the previous interaction.
<span class="hljs-bullet">-</span> Confidence: Indicate how confident you are in the classification on a scale from 0.0 (not confident) to 1.0 (very confident)
<span class="hljs-bullet">-</span> If you are unable to select an agent put "none"

Handle variations in user input, including different phrasing, synonyms, and potential spelling errors. For short requests like "yes", "ok", "I want to know more", or numerical answers, treat them as follow-ups and maintain previous agent selection.

<span class="hljs-strong">**Conversation History**</span>
Here is the conversation history that you need to take into account before answering. The last message of a user has the highest relevance when determining an agent.

A message entry consists of a role and the message. The role can be “user” or “assistant”. “user” messages are requests from a user and ‘assistant’ messages are responses from an agent. Assistant messages have the agent shortname in brackets before the message.

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">history</span>&gt;</span></span>
  {% for message in history %}
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">message</span>&gt;</span></span>
<span class="hljs-code">    &lt;role&gt;{{ message.role }}&lt;/role&gt;
    &lt;message&gt;{{ message.text }}&lt;/message&gt;
  &lt;/message&gt;
  {% endfor %}
&lt;/history&gt;
</span>
<span class="hljs-strong">**Additional Instructions**</span>

Skip any preamble and provide only the response in the specified format.
</code></pre>
<p>In short, this prompt provides the model with information about the available agents and the message history it should consider.</p>
<p>The real highlight is the attached tool specification, which looks like this:</p>
<ul>
<li><p>Name - <strong>analyze_prompt</strong></p>
</li>
<li><p>Description - <strong>Inspect a prompt and provide structured output</strong></p>
</li>
</ul>
<p><strong>Schema</strong></p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
    <span class="hljs-attr">"properties"</span>: {
        <span class="hljs-attr">"userinput"</span>: {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
            <span class="hljs-attr">"description"</span>: <span class="hljs-string">"The original user input"</span>
        },
        <span class="hljs-attr">"selected_agent"</span>: {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
            <span class="hljs-attr">"description"</span>: <span class="hljs-string">"The short name of the selected agent"</span>
        },
        <span class="hljs-attr">"confidence"</span>: {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"number"</span>,
            <span class="hljs-attr">"description"</span>: <span class="hljs-string">"The confidence level between 0.0 and 1.0"</span>
        }
    },
    <span class="hljs-attr">"required"</span>: [<span class="hljs-string">"userinput"</span>, <span class="hljs-string">"selected_agent"</span>, <span class="hljs-string">"confidence"</span>]
}
</code></pre>
<p>Unlike the Product Create tool of the Product agent, this tool is always executed because the model can always derive the parameters from the given prompt. This allows us to deserialize the result every time into a <strong>ClassifierResult</strong> structure and use the <strong>SelectedAgent</strong> property to execute an agent.</p>
<h1 id="heading-try-and-debug">Try and Debug</h1>
<p>Now that we've explored the basics of orchestrator and agent implementation, I suggest you take some time to debug the implementation.</p>
<p>If something isn't working as expected, you may need to modify the prompts with additional instructions or constraints.</p>
<h1 id="heading-ai-agent-builder">AI Agent Builder</h1>
<p>Before we conclude the lab let us quickly discuss where AI Agent Builder fits in.</p>
<p>AI Agent Builder allows you to build a single agent with access to knowledge, data from entities and tools. It fits nicely into an orchestration like the one we just explored.You would just build the orchestration that handles the message context and the execute service agents from your AI Agent Builder agent implementation the same way as we did with our custom agents above.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741500671841/0658d6af-5fac-4bbf-98e2-e630c0dec9a6.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-summary">Summary</h1>
<p>This concludes our lab on <strong>Agentic AI with OutSystems Developer Cloud</strong>. In this lab, we explored the basics of building agent orchestration. It shows that ODC is a good alternative to existing agentic frameworks, although it currently doesn't offer as many features. However, you don't have to worry about custom coding or deploying a custom code agentic solution. OutSystems Developer Cloud, along with some Forge components, provides everything you need to build. The only limitation is that ODC cannot call agents in parallel, but in a human-in-the-loop scenario, this might not be necessary. ODC is rapidly adding new features, and I hope we will see parallel action calling in the platform soon.</p>
<p>Nevertheless, thoroughly evaluate your use-case scenarios before making a decision. The good thing is that you can always mix and match.</p>
<p>I hope you enjoyed it. Please leave a comment with your feedback.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[Basic Setup and Link Unfurling]]></title><description><![CDATA[In this first tutorial of the ODC with Loop Components series, we will set up our environment by creating an Application registration in Entra and setting up an Azure Bot resource. Then, we will explore the implementation details needed to turn an ap...]]></description><link>https://without.systems/odc-with-loop-components-basic-setup-and-link-unfurling</link><guid isPermaLink="true">https://without.systems/odc-with-loop-components-basic-setup-and-link-unfurling</guid><category><![CDATA[outsystems]]></category><category><![CDATA[microsoft loop]]></category><category><![CDATA[azure bot service]]></category><category><![CDATA[#adaptive-cards]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Fri, 28 Feb 2025 08:22:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740568682526/37684641-497a-4c31-b0f0-fc11eab8fd11.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this first tutorial of the <strong>ODC with Loop Components</strong> series, we will set up our environment by creating an <strong>Application registration in Entra</strong> and setting up an <strong>Azure Bot resource</strong>. Then, we will explore the implementation details needed to turn an application URL into an Adaptive Card-based Loop Component, known as <strong>Link unfurling</strong>. This involves:</p>
<ul>
<li><p><strong>App Manifest</strong> - The App Manifest is a configuration file that defines our Microsoft 365 app. It specifically configures settings for link unfurling. The App Manifest is packaged with icon resources and then uploaded to Microsoft 365 through the Microsoft Teams app.</p>
</li>
<li><p><strong>Messaging Endpoint</strong> - An exposed REST API in ODC that receives link unfurling requests - via the Azure Bot resource -, processes them, and returns the Adaptive Card-based Loop Component.</p>
</li>
</ul>
<h1 id="heading-demo-application">Demo Application</h1>
<p>This article series includes a demo application called "<strong>ODC with Loop Components Demo</strong>," available on <strong>ODC Forge</strong>. Be sure to download the version of the application that matches each article in this series.</p>
<p>For this article, you need to install Version 0.1 from ODC Forge.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>ODC with Loop Components Demo</strong>".</p>
</li>
<li><p>Click on the Asset <strong>(Do not click on Install on the tile!)</strong>.</p>
</li>
<li><p>Switch to the <strong>Version</strong> tab and click on Install next to <strong>Version 0.1</strong>.</p>
</li>
</ul>
<p>Version 0.1 depends on other Forge components:</p>
<ul>
<li><p><strong>OAuthTokenExchange</strong> - An external logic library that helps retrieve access tokens easily. In this tutorial however we use an action to retrieve the discovery document from Microsoft Entra only.</p>
</li>
<li><p><strong>LiquidFluid</strong> - An external logic library that merges data with templates based on the Shopify Liquid Templating language.</p>
</li>
<li><p><strong>UriParser</strong> - An external logic library that parses a given URI/URL.</p>
</li>
</ul>
<p>In the demo, you can create and edit auction entries with a title, description, and starting auction price. In Version 0.1, this is the only feature available, but we will expand it throughout the tutorial series.</p>
<p>After this tutorial, we want to be able to paste a link from an auction detail screen into a Teams conversation. This link should expand into an Adaptive Card-based Loop component that looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740581883946/ba42f1b2-93ab-4c6d-8273-93bc95277713.png" alt class="image--center mx-auto" /></p>
<p>In later parts of the series, we will enable interaction with this card, allowing users to bid on the item.</p>
<h1 id="heading-messaging-endpoint">Messaging Endpoint</h1>
<p>The <a target="_blank" href="https://without.systems/odc-with-loop-components-introduction#heading-building-a-loop-component">Building a Loop Component</a> section in the series' introductory article mentiones that we need to create a Messaging Endpoint REST API to handle link unfurling. If you've read my <a target="_blank" href="https://without.systems/series/odc-msteams-bots">ODC with Bots for Teams</a> series, you might wonder why we aren't reusing what we built there and why this is a separate tutorial series.</p>
<p>In the ODC with Bots for Teams series, we developed a pattern for Conversational Bots that you can interact with in Microsoft Teams or other chat platforms like Alexa and Slack.</p>
<p><strong>Conversational Bots</strong> are one feature that can be configured in an <strong>App Manifest</strong> and then uploaded to your Microsoft 365 environment.</p>
<p>Link unfurling, which transforms a link into an Adaptive Card-based Loop component, is another feature known as a <strong>Message Extension</strong>.</p>
<p><strong>Conversational Bots</strong> and <strong>Message Extensions</strong> use the same core technology stack. This includes a <strong>Bot resource</strong> in Azure with an associated <strong>Entra application registration</strong> and an exposed REST API in ODC, which serves as the <strong>Messaging Endpoint</strong> handling <strong>requests</strong> from either a Conversational Bot channel or a Message Extension added to a Microsoft 365 app.</p>
<p>However, there is a fundamental difference in how Bots and Message Extensions send a <strong>response</strong> to a channel.</p>
<ul>
<li><p><strong>Conversational Bots</strong> - Conversational Bots - the Bot handlers to be more precise - send a response to the channel via the Azure Bot Connector API.</p>
</li>
<li><p><strong>Message Extensions</strong> - For Message Extensions, the response is directly returned from the Messaging Endpoint REST API.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Don't be confused by the term Bots. Both types need an Azure Bot resource.</div>
</div>

<h2 id="heading-conversational-bots">Conversational Bots</h2>
<p>In ODC, we can use one Messaging Endpoint to manage requests from multiple Azure Bot resources and configured channels. The ODC with Bots for Teams series shows how to create this type of multi-bot handler using ODC events. The diagram below illustrates this pattern.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740559366867/322463a7-85d4-4759-b37f-88af044e7db5.png" alt class="image--center mx-auto" /></p>
<p>In this diagram, multiple Azure Bot resources use a single Messaging Endpoint REST API within a central ODC application. The Messaging Endpoint authorizes the Bot, stores the incoming activity in an entity, and then triggers an ODC event with details of the incoming activity.</p>
<p>Several other ODC applications can subscribe to this event, process the activity, and then respond to the channel using the Azure Bot Connector API.</p>
<h2 id="heading-message-extensions">Message Extensions</h2>
<p>For Message Extensions, we can't use this decoupled pattern because the Messaging Endpoint must respond directly to the channel. Additionally, a single domain, such as your ODC development stage <code>company-dev.outsystems.app</code>, can only be managed by one - and only one - message extension, and there's no option to include a path. A message extension is defined in the App Manifest. Take a look at the following diagram:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740578740500/7961d878-6fcc-424d-89a3-c2c6be21f436.png" alt class="image--center mx-auto" /></p>
<p>This diagram shows a setup for handling link unfurling and Adaptive Card-based Loop Components.</p>
<p>A single Azure Bot resource with a configured messaging endpoint can manage link unfurling and Adaptive Card actions because the specific domains are listed in the App Manifest. You can create multiple Message Extension App Manifests for one bot resource.</p>
<p>The Messaging Endpoint that receives requests can be set up in a central ODC application with logic to differentiate between the relevant domains based on the domains and even path segments. It then uses service actions from other ODC applications to create or update an Adaptive Card or handle an Adaptive Card action.</p>
<p>While this approach isn't perfect, I used it because I couldn't find a better option.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note that the tutorial demo application doesn't use this pattern but instead has all logic contained in a single web app. The pattern above is meant to give you an idea of how to implement <strong>Message Extensions</strong> in a production environment.</div>
</div>

<h1 id="heading-prerequisites">Prerequisites</h1>
<p>Enough with the theory. Let's start by preparing our environment.</p>
<h2 id="heading-register-entra-application"><strong>Register Entra Application</strong></h2>
<p>A bot resource requires an Entra application registration.</p>
<p>In <a target="_blank" href="https://portal.azure.com/"><strong>Azure Portal</strong></a> go to <strong>App Registrations</strong> and click on <strong>New registration</strong>.</p>
<ul>
<li><p><strong>Name</strong> - Microsoft 365 Message Extension with ODC</p>
</li>
<li><p><strong>Supported account types</strong> - Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)</p>
</li>
<li><p>Click <strong>Register</strong></p>
</li>
</ul>
<p>From the <strong>Overview</strong> page, copy the values of:</p>
<ul>
<li><strong>Application (client) ID</strong></li>
</ul>
<p>We will need that value in the next step.</p>
<h2 id="heading-create-an-azure-bot"><strong>Create an Azure Bot</strong></h2>
<p>With our application registration complete, we can now create an Azure Bot resource. An Azure Bot resource is a middleware that connects Microsoft 365 apps (and others) with the Messaging Endpoint in ODC.</p>
<p>In Azure <strong>Marketplace</strong> search for <strong>Azure Bot</strong> and click <strong>Create - Azure Bot.</strong></p>
<ul>
<li><p><strong>Bot handle</strong> - BotId-<em>&lt;Application ID from Entra App Registration&gt;</em></p>
</li>
<li><p><strong>Subscription</strong> - Select your subscription model</p>
</li>
<li><p><strong>Resource group</strong> - Choose a resource group where you want to place the bot resource</p>
</li>
<li><p><strong>Data residency</strong> - Global</p>
</li>
<li><p><strong>Pricing tier</strong> - Change plan to Free</p>
</li>
<li><p><strong>Type of App</strong> - Multi Tenant (corresponds to our Entra App Registration)</p>
</li>
<li><p><strong>Creation type</strong> - Use existing app registration</p>
</li>
<li><p><strong>App ID</strong> - Paste the <strong>Application ID</strong> from the Entra App Registration Overview page</p>
</li>
<li><p><strong>App tenant ID</strong> - Paste the <strong>Tenant ID</strong> from the Entra App Registration Overview page</p>
</li>
<li><p>Click <strong>Review + Create</strong>, review the information the click <strong>Create</strong></p>
</li>
</ul>
<p>Wait until the deployment is complete, then click on <strong>Go to resource</strong> to continue.</p>
<h2 id="heading-set-bot-profile"><strong>Set Bot Profile</strong></h2>
<p>In the deployed Azure Bot resource, first go to the <strong>Settings - Bot profile</strong> menu and give your bot a meaningful display name and description. You can also upload a custom icon here.</p>
<h2 id="heading-configure-messaging-endpoint"><strong>Configure Messaging Endpoint</strong></h2>
<p>Next switch to the <strong>Settings - Configuration</strong> menu. The only settings we have to configure here is to provide the <strong>FQDN</strong> to our Messaging endpoint.</p>
<p>The messaging endpoint FQDN is the combination of your ODC stage base URL and the path you copied earlier from the Messaged endpoint of the demo application.</p>
<ul>
<li><p><strong>Messaging endpoint</strong> - <code>https://&lt;ODC stage&gt;.</code><a target="_blank" href="http://outsystems.app/ODCwithBotsforTeamsDemo/rest/MessagingEndpoint/Messages"><code>outsystems.app/ODCwithLoopComponentsDemo/rest/MessagingEndpoint/Messages</code></a></p>
</li>
<li><p>Click <strong>Apply</strong> to save the changes.</p>
</li>
</ul>
<h2 id="heading-activate-teams-channel"><strong>Activate Teams Channel</strong></h2>
<p>Next, we need to activate Teams channel support in our Bot resource.</p>
<ul>
<li><p>Choose <strong>Microsoft Teams</strong> from the list of <strong>Available Channels</strong> in the <strong>Settings - Channels</strong> menu.</p>
</li>
<li><p>Read the Terms of Service and <strong>Agree</strong>.</p>
</li>
<li><p>In the <strong>Messaging</strong> tab, select <strong>Microsoft Teams Commercial (most common)</strong>.</p>
</li>
<li><p>Click <strong>Apply</strong>.</p>
</li>
</ul>
<h2 id="heading-summary">Summary</h2>
<p>In the Prerequisites step, we created an Azure Bot resource with an associated Entra application registration. In the Bot configuration, we specified our Messaging Endpoint hosted in the ODC with Loop Components Demo application. Finally, we activated the Microsoft Teams channel.</p>
<p>As mentioned above, an Azure Bot resource in your Azure tenant acts as <strong>middleware</strong>. This middleware exchanges messages, called Activities, between configured channels and the messaging endpoint. It does not matter if you are building Conversational Bots or Message Extensions. These capabilities, or what your implementation actually does, are specified in the <strong>App Manifest</strong>.</p>
<h1 id="heading-microsoft-365-app">Microsoft 365 App</h1>
<p>The App Manifest document describes and configures the capabilities that this specific app—your implementation—can perform. In the <strong>ODC with Bots for Teams</strong> series, we covered the <strong>Conversational Bot</strong> capability, and in this tutorial, we configure a <strong>Message Extension</strong> capability for Link unfurling.</p>
<p>The App Manifest, along with additional resources like icons, needs to be uploaded to your Microsoft 365 tenant through either Microsoft Teams or the Microsoft Office Admin Portal. We will use Microsoft Teams and will "install" the app for a single user, a process known as sideloading.</p>
<h2 id="heading-create-app-manifest">Create App Manifest</h2>
<p>The demo project includes a template for the manifest file, complete with icons. Download the template archive from <strong>Data - Resources</strong> in ODC Studio and extract it to a folder.</p>
<p>An app manifest file is a JSON document that follows the unified app manifest schema. Together with the manifest file you will also need two icons.</p>
<ul>
<li><p><strong>Full color icon</strong> with a size of 192×192</p>
</li>
<li><p><strong>Outline icon</strong> with a size of 32×32</p>
</li>
</ul>
<p>Open the <strong>manifest.json</strong> document from the extracted template in an editor of your choice.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json"</span>,
    <span class="hljs-attr">"version"</span>: <span class="hljs-string">"3.0.0"</span>,
    <span class="hljs-attr">"manifestVersion"</span>: <span class="hljs-string">"1.19"</span>,
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"&lt;Entra Application (client) ID&gt;"</span>,
    <span class="hljs-attr">"name"</span>: {
      <span class="hljs-attr">"short"</span>: <span class="hljs-string">"ODCwithLoop"</span>,
      <span class="hljs-attr">"full"</span>: <span class="hljs-string">"ODCwithLoop Demo"</span>
    },
    <span class="hljs-attr">"developer"</span>: {
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"without.systems"</span>,
      <span class="hljs-attr">"websiteUrl"</span>: <span class="hljs-string">"https://without.systems"</span>,
      <span class="hljs-attr">"privacyUrl"</span>: <span class="hljs-string">"https://without.systems/privacy"</span>,
      <span class="hljs-attr">"termsOfUseUrl"</span>: <span class="hljs-string">"https://without.systems/termsofuse"</span>
    },
    <span class="hljs-attr">"description"</span>: {
      <span class="hljs-attr">"short"</span>: <span class="hljs-string">"Shows how to build a Loop Component Adaptive Card"</span>,
      <span class="hljs-attr">"full"</span>: <span class="hljs-string">"Create interaktive elements with Adaptive Cards and share it across Microsoft 365 apps."</span>
    },
    <span class="hljs-attr">"icons"</span>: {
      <span class="hljs-attr">"outline"</span>: <span class="hljs-string">"outline.png"</span>,
      <span class="hljs-attr">"color"</span>: <span class="hljs-string">"color.png"</span>
    },
    <span class="hljs-attr">"accentColor"</span>: <span class="hljs-string">"#FFFFFF"</span>,
    <span class="hljs-attr">"showLoadingIndicator"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"bots"</span>: [
        {
          <span class="hljs-attr">"botId"</span>: <span class="hljs-string">"&lt;Entra Application (client) ID&gt;"</span>,
          <span class="hljs-attr">"scopes"</span>: [<span class="hljs-string">"personal"</span>, <span class="hljs-string">"team"</span>, <span class="hljs-string">"groupChat"</span>],
          <span class="hljs-attr">"supportsFiles"</span>: <span class="hljs-literal">false</span>,
          <span class="hljs-attr">"isNotificationOnly"</span>: <span class="hljs-literal">false</span>
        }
    ],
    <span class="hljs-attr">"composeExtensions"</span>: [
      {
        <span class="hljs-attr">"botId"</span>: <span class="hljs-string">"&lt;Entra Application (client) ID&gt;"</span>,
        <span class="hljs-attr">"composeExtensionType"</span>: <span class="hljs-string">"botBased"</span>,
        <span class="hljs-attr">"commands"</span>: [],
        <span class="hljs-attr">"messageHandlers"</span>: [
          {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"link"</span>,
            <span class="hljs-attr">"value"</span>: {
              <span class="hljs-attr">"domains"</span>: [
                <span class="hljs-string">"&lt;your ODC development stage domain e.g. company-dev.outsystems.app&gt;"</span>
              ],
              <span class="hljs-attr">"supportsAnonymizedPayloads"</span>: <span class="hljs-literal">false</span>
            }
          }
        ]
      }
    ],
    <span class="hljs-attr">"permissions"</span>: [
      <span class="hljs-string">"identity"</span>,
      <span class="hljs-string">"messageTeamMembers"</span>
    ],
    <span class="hljs-attr">"validDomains"</span>: [
      <span class="hljs-string">"&lt;your ODC development stage domain e.g. company-dev.outsystems.app&gt;"</span>
    ]
  }
</code></pre>
<p>Modify the following values to match your environment</p>
<ul>
<li><p><strong>id</strong> - Application (client) ID from your Entra Application registration</p>
</li>
<li><p><strong>bots.botId</strong> - Application (client) OD from your Entra Application registration</p>
</li>
<li><p><strong>composeExtensions.botId</strong> - Application (client) ID from your Entra Application registration</p>
</li>
<li><p><strong>composeExtensions.messageHandlers.value.domains</strong> - Add your ODC development stage domain name.</p>
</li>
<li><p><strong>validDomains</strong> - Add your ODC development stage domain name.</p>
</li>
</ul>
<p>Save the document, then compress the <strong>manifest.json</strong> file and the <strong>two icon resources</strong> into a <strong>ZIP archive</strong>.</p>
<h2 id="heading-allow-sideloading"><strong>Allow Sideloading</strong></h2>
<p>By default users are not allowed to sideload custom apps into Microsoft Teams. This is defined in a Teams Setup Policy that is associated with your user account in the Microsoft Teams Admin center.</p>
<p>In the <a target="_blank" href="https://admin.teams.microsoft.com/"><strong>Microsoft Teams Admin center</strong></a>.</p>
<ul>
<li><p>Select <strong>Teams apps - Setup policies</strong> in the menu</p>
</li>
<li><p>Click <strong>Add</strong></p>
</li>
<li><p><strong>Name</strong>: OutSystems Bot Developer Setup Policy</p>
</li>
<li><p><strong>Description</strong>: Allows an OutSystems Bot developer to sideload Microsoft Teams apps</p>
</li>
<li><p>Click <strong>Save</strong></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740637189637/651b2539-f2d4-4f54-9edb-fb2bbbb7fd9c.webp" alt class="image--center mx-auto" /></p>
<p>After we have created the policy, we must assign it to one or more users</p>
<ul>
<li><p>Select <strong>Users - Manage users</strong> in the menu</p>
</li>
<li><p>Search your own user account and select it</p>
</li>
<li><p>In the <strong>Policies</strong> tab click <strong>Edit</strong></p>
</li>
<li><p>Select the <strong>OutSystems Bot Developer Setup Policy</strong> in the <strong>Select App setup policy dropdown</strong></p>
</li>
<li><p>Click <strong>Apply</strong></p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740637213836/223533ff-3aba-44aa-be61-caf58b4554da.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It may take a little while for the new settings to take effect. You will need to restart your Microsoft Teams client.</div>
</div>

<h2 id="heading-install-app-to-microsoft-teams"><strong>Install App to Microsoft Teams</strong></h2>
<p>With our package prepared we can now install it to Microsoft Teams. Open your Microsoft Teams client and in the left icon menu click the Apps icon.</p>
<ul>
<li><p>On the bottom left click on <strong>Manage your apps</strong></p>
</li>
<li><p>In the apps list click the <strong>Upload an app</strong> button and select <strong>Upload a custom app</strong></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740637305597/c6390e82-7bac-4b59-85d2-7be7f417700e.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Select the <strong>zip archive</strong> you created</p>
</li>
<li><p>Check the details of the app, then click <strong>Add</strong></p>
</li>
</ul>
<p>With our app uploaded to Microsoft Teams, we can now proceed to try out the demo application.</p>
<h1 id="heading-demo-application-first-try">Demo Application - First Try</h1>
<p>Open the Demo application in your browser and create a new Auction. After saving, click on the entry of the created Auction again, which will take you back to the detail screen. Copy the full URL from the browser.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740637661399/a569138d-ff99-450d-a303-94e8396c5c01.png" alt class="image--center mx-auto" /></p>
<p>Open Microsoft Teams and start a chat conversation with another test account - or a collague you want to bother with your tests 😒.</p>
<p>Paste the URL into the message box, and after a short while, a Loop component should appear next to the pasted URL in the message box.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740637903919/65253924-1b8c-4bb9-ba89-4117a24fdf51.png" alt class="image--center mx-auto" /></p>
<p>If it didn't work, check the following:</p>
<ul>
<li><p>Review your app manifest to ensure you added the correct domain for link unfurling.</p>
</li>
<li><p>Add a breakpoint at the top of the Messages endpoint in the demo application, start the debugger, and check if a request is sent to your REST API.</p>
</li>
<li><p>Verify the messaging endpoint URL in the Bot resource in Azure to ensure it matches the correct full endpoint URL.</p>
</li>
<li><p>Check the botId value to ensure it matches your Entra Application (client) Id.</p>
</li>
</ul>
<p>Before we dive into the implementation details, let's cover one more important part: Adaptive Cards.</p>
<h1 id="heading-adaptive-cards">Adaptive Cards</h1>
<p>When you copy an auction link into a Teams message, it expands into a Loop component based on an Adaptive Card. These are basically JSON-based UI snippets that the host application displays. Host applications are mainly Microsoft 365 apps, but there is also a JavaScript-based SDK to display Adaptive Cards in any web application.</p>
<p>The template for the Adaptive Card JSON document for our Auction looks like this:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"http://adaptivecards.io/schemas/adaptive-card.json"</span>,
    <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.4"</span>,
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"AdaptiveCard"</span>,
    <span class="hljs-attr">"metadata"</span>: {
        <span class="hljs-attr">"webUrl"</span>: <span class="hljs-string">"{{webUrl}}"</span>
    },
    <span class="hljs-attr">"body"</span>: [
        {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
            <span class="hljs-attr">"size"</span>: <span class="hljs-string">"medium"</span>,
            <span class="hljs-attr">"weight"</span>: <span class="hljs-string">"bolder"</span>,
            <span class="hljs-attr">"text"</span>: <span class="hljs-string">"{{title}}"</span>
        },
        {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"ColumnSet"</span>,
            <span class="hljs-attr">"columns"</span>: [
                {
                    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Column"</span>,
                    <span class="hljs-attr">"width"</span>: <span class="hljs-string">"stretch"</span>,
                    <span class="hljs-attr">"items"</span>: [
                        {
                            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                            <span class="hljs-attr">"text"</span>: <span class="hljs-string">"{{description}}"</span>,
                            <span class="hljs-attr">"wrap"</span>: <span class="hljs-literal">true</span>
                        }
                    ],
                    <span class="hljs-attr">"verticalContentAlignment"</span>: <span class="hljs-string">"Center"</span>,
                    <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"Medium"</span>
                },
                {
                    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Column"</span>,
                    <span class="hljs-attr">"width"</span>: <span class="hljs-string">"auto"</span>,
                    <span class="hljs-attr">"items"</span>: [
                        {
                            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                            <span class="hljs-attr">"text"</span>: <span class="hljs-string">"{{price}}"</span>,
                            <span class="hljs-attr">"wrap"</span>: <span class="hljs-literal">true</span>,
                            <span class="hljs-attr">"size"</span>: <span class="hljs-string">"ExtraLarge"</span>,
                            <span class="hljs-attr">"weight"</span>: <span class="hljs-string">"Bolder"</span>
                        }
                    ]
                }
            ],
            <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"None"</span>
        }
    ]
}
</code></pre>
<p>This card defines one Text Block containing the title of the auction followed by a table with two columns containing the description and the price.</p>
<p>Note the handlebar placeholders like <code>{{title}}</code> that will be replaced with values in our implementation as we will see in a bit.</p>
<p>The full reference documentation for Adaptive Cards along with samples can be found at <a target="_blank" href="https://adaptivecards.microsoft.com/">Welcome - Adaptive Cards</a>.</p>
<p>You can create Adaptive Cards either using a text editor or visually using the <a target="_blank" href="https://adaptivecards.io/designer/">Adapative Card Designer</a>.</p>
<h2 id="heading-adaptive-cards-in-a-message-extension">Adaptive Cards in a Message Extension</h2>
<p>To expand a link into an Adaptive Card, the Messaging Endpoint must return it in a specific format that looks like this:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"composeExtension"</span>: {
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"result"</span>,
    <span class="hljs-attr">"attachmentLayout"</span>: <span class="hljs-string">"list"</span>,
    <span class="hljs-attr">"attachments"</span>: [
      {
        <span class="hljs-attr">"preview"</span>: {
          <span class="hljs-attr">"content"</span>: {
            <span class="hljs-attr">"title"</span>: <span class="hljs-string">"{{ title }}"</span>,
            <span class="hljs-attr">"text"</span>: <span class="hljs-string">"{{ description }}"</span>
          },
          <span class="hljs-attr">"contentType"</span>: <span class="hljs-string">"application/vnd.microsoft.card.thumbnail"</span>
        },
        <span class="hljs-attr">"content"</span>: {
          <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"http://adaptivecards.io/schemas/adaptive-card.json"</span>,
          <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.4"</span>,
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"AdaptiveCard"</span>,
          <span class="hljs-attr">"metadata"</span>: {
            <span class="hljs-attr">"webUrl"</span>: <span class="hljs-string">"{{webUrl}}"</span>
          },
          <span class="hljs-attr">"body"</span>: [
            {
              <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
              <span class="hljs-attr">"size"</span>: <span class="hljs-string">"medium"</span>,
              <span class="hljs-attr">"weight"</span>: <span class="hljs-string">"bolder"</span>,
              <span class="hljs-attr">"text"</span>: <span class="hljs-string">"{{title}}"</span>
            },
            {
              <span class="hljs-attr">"type"</span>: <span class="hljs-string">"ColumnSet"</span>,
              <span class="hljs-attr">"columns"</span>: [
                {
                  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Column"</span>,
                  <span class="hljs-attr">"width"</span>: <span class="hljs-string">"stretch"</span>,
                  <span class="hljs-attr">"items"</span>: [
                    {
                      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                      <span class="hljs-attr">"text"</span>: <span class="hljs-string">"{{description}}"</span>,
                      <span class="hljs-attr">"wrap"</span>: <span class="hljs-literal">true</span>
                    }
                  ],
                  <span class="hljs-attr">"verticalContentAlignment"</span>: <span class="hljs-string">"Center"</span>,
                  <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"Medium"</span>
                },
                {
                  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"Column"</span>,
                  <span class="hljs-attr">"width"</span>: <span class="hljs-string">"auto"</span>,
                  <span class="hljs-attr">"items"</span>: [
                    {
                      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"TextBlock"</span>,
                      <span class="hljs-attr">"text"</span>: <span class="hljs-string">"{{price}}"</span>,
                      <span class="hljs-attr">"wrap"</span>: <span class="hljs-literal">true</span>,
                      <span class="hljs-attr">"size"</span>: <span class="hljs-string">"ExtraLarge"</span>,
                      <span class="hljs-attr">"weight"</span>: <span class="hljs-string">"Bolder"</span>
                    }
                  ]
                }
              ],
              <span class="hljs-attr">"spacing"</span>: <span class="hljs-string">"None"</span>
            }
          ]
        }
      }
    ]
  }
}
</code></pre>
<p>The Messaging Endpoint returns a <strong>result</strong> of <strong>composeExtension</strong> for a Link unfurling request, which includes an <strong>Attachment</strong>. This attachment has two important properties: <strong>preview</strong> and <strong>content</strong>. Both are needed for Link unfurling to work. The preview should contain a simplified version of the Adaptive Card, while the content should have the full version. In the example above, the preview only includes the auction title.</p>
<h1 id="heading-implementation-walkthrough">Implementation Walkthrough</h1>
<p>Now, let's look at the different implementation details that will turn an auction link into a Loop component. The steps in this first tutorial are straightforward:</p>
<ul>
<li><p><strong>Handle the queryLink request</strong> - Whenever a user pastes a link that matches the domain in the app manifest into a Teams message, the Teams channel sends a queryLink request to the configured Azure Bot resource, which then forwards the request to the Messaging endpoint.</p>
</li>
<li><p><strong>Unfurl</strong> - In our Messaging endpoint handler, we will process the request, look up the auction in the database, and finally return a response containing the Adaptive Card.</p>
</li>
</ul>
<p>Open the <strong>ODC with Loop Components Demo</strong> application in ODC Studio.</p>
<h2 id="heading-handle-request">Handle Request</h2>
<p>Go to <strong>Logic - Integrations - REST - MessagingEndpoint</strong> and double click on the exposed <strong>Messages</strong> endpoint that handles inbound requests from the Azure Bot resource.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740647059172/584ef0af-88f0-4d64-ba9c-2a3553035a07.png" alt class="image--center mx-auto" /></p>
<p>In a first step we have to determine the specific type of the activity sent to our Messaging Endpoint, because for now we only want to react on Link unfurling requests.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can learn more about the structure of inbound requests in the <a target="_self" href="https://without.systems/odc-with-bots-for-teams-introduction#heading-bot-messages">ODC with Bots for Teams introductory article</a>.</div>
</div>

<p>The <strong>DeserializeInboundMinimalActivity</strong> action deserializes the request's payload and gives us the following information:</p>
<ul>
<li><p><strong>type</strong> - The type of activity, which is always <strong>invoke</strong> for Link unfurling.</p>
</li>
<li><p><strong>name</strong> - The name of the specific invocation, which is <strong>composeExtension/queryLink</strong> for a Link unfurling request.</p>
</li>
</ul>
<p>The following <strong>switch</strong> statement currently has only one conditional path that is executed when <strong>type is invoke</strong> and <strong>name is composeExtension/queryLink</strong>. In all other cases we just end the action flow.</p>
<p><strong>DeserializeInboundQueryLinkActivity</strong> converts the request into a structure that includes extra details such as the user who sent the request, the channel used, and more. The most important detail is the <strong>URL</strong> that was pasted, which is found in the Value attribute of the <strong>InvokeQueryLinkActivity</strong> structure.</p>
<p>The URL is then finally used to prepare a response in the <strong>UnfurlAuctionLink</strong> action.</p>
<h2 id="heading-unfurl">Unfurl</h2>
<p>The <strong>UnfurlAuctionLink</strong> performs several actions to create a response that can be sent back to the channel (Microsoft Teams).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740647865493/f3a7aad3-5e3a-4a14-a186-55bbfbfac72c.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>Parse</strong> - This external logic action extracts the URL from the Link unfurling request.</p>
</li>
<li><p><strong>ParseQueryString</strong> - Splits the query string from the URL into a list of name-value pairs.</p>
</li>
<li><p><strong>HasAuctionDetailPath</strong> - Checks if the URL is from an AuctionDetail screen by examining the segments of the parsed URL. If not, we simply exit.</p>
</li>
<li><p><strong>FilterAuctionId</strong> - Filters the query string name-value pairs for the AuctionId parameter.</p>
</li>
<li><p><strong>RenderAuctionLoop</strong> - Uses the URL and the identified AuctionId value to create the response (see below).</p>
</li>
</ul>
<h2 id="heading-render-auction-response">Render Auction Response</h2>
<p><strong>RenderAuctionLoop</strong> retrieves the Auction entity to obtain the record for the requested auction. It then uses this information to create a complete response for the Messaging Endpoint to send back to the channel.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740648332079/1833e75b-4723-40e1-84af-2aa471300c31.png" alt class="image--center mx-auto" /></p>
<p><strong>Auction_Get</strong> - This action retrieves the record for the given AuctionId from the Auction entity.</p>
<p>The following actions are for merging a response template with data from the auction. The template (available under <strong>Data - Resources - AuctionUnfurl.json</strong>) contains handlebar placeholders that will be replaced by values using RenderTemplate from the <strong>LiquidFluid</strong> external logic library.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">LiquidFluid is a component that can combine data with a template file using the <a target="_self" href="https://shopify.dev/docs/api/liquid">Shopify Liquid Templating language</a>.</div>
</div>

<p>The template contains the following placeholders</p>
<ul>
<li><p><strong>title</strong> - Title of the auction</p>
</li>
<li><p><strong>description</strong> - Full item description</p>
</li>
<li><p><strong>price</strong> - The auction start price</p>
</li>
<li><p><strong>webUrl</strong> - The full url referencing the auction in the demo application</p>
</li>
</ul>
<p>This data structure is represented by a structure <strong>AuctionCard</strong> in <strong>Data - Structures - Auction</strong>.</p>
<p>First, we construct the Card payload by assigning values from <strong>Auction_Get</strong> to the structure properties. Next, we serialize the <strong>AuctionCard</strong> structure, and finally, we use the <strong>RenderTemplate</strong> action to merge the serialized data structure with the template.</p>
<p>The Messaging Endpoint then returns this rendered response to the channel, and the Adaptive Card-based Loop Component is displayed.</p>
<h1 id="heading-missing-pieces">Missing Pieces</h1>
<p>Try changing the auction you pasted earlier in the demo application. You will notice that the card does not update. Being "live" is a core feature of a Loop component, so we need to ensure that cards update when the data changes. We will address this in the next part of the tutorial series.</p>
<p>Another missing piece is that you can copy a Loop component between Microsoft Teams conversations, but not, for example, to a Microsoft Outlook message. We will cover this in a later part.</p>
<p>For now, congratulations! You have successfully turned an OutSystems application link into an Adaptive Card-based Loop component.</p>
<h1 id="heading-summary-1">Summary</h1>
<p>In this part of the <strong>ODC with Loop Components</strong> tutorial series, we set up the prerequisites for a Message Extension in Azure and implemented a Messaging Endpoint that responds to a link unfurling request from a Microsoft Teams channel with an Adaptive Card-based Loop component.</p>
<p>I hope you enjoyed it. Feel free to leave a comment with your questions and feedback.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[Adaptive Card based Loop Components Introduction]]></title><description><![CDATA[This is the beginning of a new tutorial series on developing for Microsoft 365 with OutSystems Developer Cloud. In this series, we will explore how to build Adaptive Card-based Loop components. Throughout this series, I will refer to parts of the ODC...]]></description><link>https://without.systems/odc-with-loop-components-introduction</link><guid isPermaLink="true">https://without.systems/odc-with-loop-components-introduction</guid><category><![CDATA[outsystems]]></category><category><![CDATA[microsoft loop]]></category><category><![CDATA[#adaptive-cards]]></category><category><![CDATA[azure bot service]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Tue, 25 Feb 2025 15:19:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740158019575/d79bec0b-396b-4249-813d-d642d53c0298.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the beginning of a new tutorial series on developing for Microsoft 365 with OutSystems Developer Cloud. In this series, we will explore how to build Adaptive Card-based Loop components. Throughout this series, I will refer to parts of the <a target="_blank" href="https://without.systems/series/odc-msteams-bots">ODC with Bots for Teams</a> tutorial series. Although it's not necessary to have completed the ODC with Bots for Teams tutorials, you should at least read them since they use the same technology stack.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://creators.spotify.com/pod/show/withoutsystems/episodes/Introduction-on-how-to-develop-Adaptive-Card-Loop-components-with-Outsystems-Developer-Cloud-e2vc6m4/a-abq4rov">https://creators.spotify.com/pod/show/withoutsystems/episodes/Introduction-on-how-to-develop-Adaptive-Card-Loop-components-with-Outsystems-Developer-Cloud-e2vc6m4/a-abq4rov</a></div>
<p> </p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Please note that the podcast episode above is automatically generated from this article and may not include a proper level of detail.</div>
</div>

<h1 id="heading-loop-components-overview">Loop Components Overview</h1>
<p>Loop components are a feature in Microsoft 365 that allow users to create live, actionable content that can be <strong>embedded and updated</strong> across various Microsoft 365 apps, such as Teams and Outlook. These components enable users to make real-time updates without switching contexts between different apps.</p>
<p>Their key characteristics are</p>
<ul>
<li><p><strong>Live and Actionable</strong> - They allow users to interact with the content directly within the app, making updates and changes that are immediately reflected across all instances of the component.</p>
</li>
<li><p><strong>Cross-App Functionality</strong> - These components can be used across multiple Microsoft 365 apps, ensuring a seamless experience whether you're in Teams, Outlook, or another supported app.</p>
</li>
<li><p><strong>Contextual Updates</strong> - Users can make updates without leaving their current workflow, reducing the need for context switching and improving productivity.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">At the time of writing, Loop components are supported in Microsoft Teams, Microsoft Outlook, and Microsoft Whiteboard, with more apps expected to support them in the future.</div>
</div>

<p>Technically, Loop components are identified by a URL that, when pasted into a Microsoft 365 app, is transformed into an interactive element.</p>
<p>There are two types of Loop components you can use in Microsoft 365 apps.</p>
<h2 id="heading-loop-components-created-with-microsoft-loop">Loop components created with Microsoft Loop</h2>
<p>In Microsoft Loop, you can create a page in any of your workspaces and then share this page—a Loop component—in other Microsoft 365 apps. Simply copy the component using the Copy component button and paste it into another Microsoft 365 app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740462754849/b58bbce2-791f-4ea6-81de-8751e9f1116b.png" alt="Copy Loop component in Microsoft Loop" class="image--center mx-auto" /></p>
<p>Alternatively, Loop components using Microsoft Loop can also be created directly from various Microsoft 365 apps.</p>
<p>In Microsoft Teams, you can create and send a Loop component from the conversation action menu.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740462937716/70969e92-5558-4657-af68-d6f9c6b8450e.png" alt class="image--center mx-auto" /></p>
<p>In Microsoft Outlook's message compose window, you can create a Loop component from the Ribbon menu.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740463044609/9ddde1fe-3fad-4800-b652-14f993a3987c.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-adaptive-card-based-loop-components">Adaptive Card based Loop Components</h2>
<p>The second type of Loop components is Adaptive Card-based Loop Components. While you can use Microsoft Loop components as provided by Microsoft, <strong>Adaptive Card-based Loop components are the ones that you can develop with OutSystems Developer Cloud</strong>, which is what we cover in this tutorial series.</p>
<p>Throughout this article and all the following tutorials, when I mention Loop components, we are referring to Adaptive Card-based Loop components.</p>
<h1 id="heading-why-you-should-care">Why you should care</h1>
<p>As mentioned earlier, Loop components are live and actionable. "<strong>Live</strong>" means you can share and embed a single instance of a Loop component in multiple Teams conversations or Outlook messages, and it will always show the current state of the data linked to the adaptive card. In upcoming tutorials, we'll explore how this works technically. This means if that data changes, the Loop components will automatically update, so there's no need to send an updated version of it.</p>
<p>"<strong>Actionable</strong>" means a user can interact with a Loop component directly within a Teams conversation or Outlook message. This is beneficial because the user doesn't have to leave the current context, significantly reducing media-breaks between applications.</p>
<p>The combination of these features makes Loop components a great productivity booster. For example, consider the following scenario:</p>
<p>In your application, a user enters new product data that needs approval from someone in the Product Management role. You could send an Outlook message with a Loop component to all product management members. A product manager can then approve the new data directly in Outlook by clicking an Approve button in the Loop component. Once approved, all instances will update, and other product managers will immediately see that the data has already been approved by someone else. Additionally, the product manager who approved the data will see that they were the one who did it. There's no need to redirect users to an approval website anymore, and since the Loop component updates automatically, everyone will know whether an approval has already taken place or is still pending.</p>
<h1 id="heading-building-a-loop-component">Building a Loop Component</h1>
<p>Loop components are identified by a URL, such as a full URL to a screen with input parameters from your OutSystems application. Your implementation recognizes this URL and replaces it with an interactive element, known as an Adaptive Card. This process is called link unfurling and is a feature of <a target="_blank" href="https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions">Message Extensions</a> for Microsoft 365. Developing the Message Extension is the most essential part of this series.</p>
<p>Building a Message Extension involves the following steps:</p>
<ul>
<li><p><strong>Register a Bot and associated Entra application registration</strong> - In our Azure tenant, we need to create an Azure Bot resource along with an Entra application. The Azure Bot resource acts as the gateway between Microsoft 365 apps and our OutSystems Developer Cloud environment.</p>
</li>
<li><p><strong>Messaging Endpoint Implementation</strong> - This is an exposed REST API that processes requests from Microsoft 365 apps through the Azure Bot resource.</p>
</li>
<li><p><strong>Microsoft App Manifest</strong> - This is a configuration file that, along with other resources, needs to be published within your Microsoft 365 tenant. It contains configuration options like which URLs should be unfurled and more.</p>
</li>
</ul>
<h1 id="heading-user-experience-with-loop-components">User Experience with Loop Components</h1>
<p>From a user's perspective, using Loop components might look like this:</p>
<ul>
<li><p>A user copies the full URL from an OutSystems application into a Microsoft Teams conversation or Microsoft Outlook message.</p>
</li>
<li><p>If the URL matches the configuration in your Microsoft App Manifest file, a link unfurling request is sent to the messaging endpoint of the configured Azure Bot resource.</p>
</li>
<li><p>Your messaging endpoint processes the link unfurl request, determines the requested data, and responds with a Loop component based on an Adaptive Card.</p>
</li>
<li><p>The Loop component is then displayed in the Microsoft 365 app.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740502938206/2827c8a1-c65b-49f2-87ed-53c77bbb3c75.png" alt class="image--center mx-auto" /></p>
<p>The Loop component can include interactive elements with input fields and buttons. If the user clicks a button:</p>
<ul>
<li><p>The messaging endpoint is invoked again with a request specifying the card instance and the action.</p>
</li>
<li><p>The messaging endpoint processes the request and, if needed, returns an updated Adaptive Card based Loop component to the app.</p>
</li>
</ul>
<h1 id="heading-summary">Summary</h1>
<p>This concludes the introduction to Adaptive Card-based Loop Components with OutSystems Developer Cloud. Please take a moment to review the provided links, as they contain important additional information.</p>
<p>In the first tutorial, we will set up the prerequisites and unfurl an OutSystems application link for a simple adaptive card in Microsoft Teams - in later articles we will extend this to other Microsoft 365 apps. We will also handle data modifications and Loop component refreshes. This tutorial series includes a full demo application that you can use throughout the tutorials and as a starting point for your own implementation.</p>
<p>I hope you enjoyed the introduction. Feel free to leave a comment with your questions or feedback.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[User Authentication in Conversational Bots]]></title><description><![CDATA[In this final part of the ODC with Bots for Teams series, we explore a way to authenticate a user within a Microsoft Teams conversation. Specifically, we examine how to quietly exchange a user's access token, issued when logging into Microsoft Teams,...]]></description><link>https://without.systems/odc-with-bots-for-teams-authenticate-users</link><guid isPermaLink="true">https://without.systems/odc-with-bots-for-teams-authenticate-users</guid><category><![CDATA[outsystems]]></category><category><![CDATA[azure bot service]]></category><category><![CDATA[Bot Framework]]></category><category><![CDATA[Microsoft Graph]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Wed, 19 Feb 2025 11:59:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738842965847/64fe4429-fcea-412d-9d92-5eb10221238b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this final part of the <strong>ODC with Bots for Teams</strong> series, we explore a way to authenticate a user within a Microsoft Teams conversation. Specifically, we examine how to quietly exchange a user's access token, issued when logging into Microsoft Teams, for an access token of our bot application. This token will have specific Microsoft Graph API permissions that we will use to send an email to the user.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Please note that this token exchange <strong>only works in the Microsoft Teams channel</strong> and other Microsoft Office applications, but not with channels like Slack.</div>
</div>

<p>Authenticating a user within a Teams conversation is a significant topic. Besides the token exchange using Microsoft Entra, which is covered in this article, you can also authenticate a user with any OpenID Connect-compatible Identity Provider.</p>
<p>But when should you authenticate a user from within a conversation?</p>
<p>Explicitly asking a user to authenticate is not always necessary. You only need to require authentication when you need an access token to perform actions on behalf of the user (meaning with permissions associated with the user) on resources. Examples include:</p>
<ul>
<li><p>Interacting with the Microsoft Graph API, such as sending an email on behalf of a user. In this case, you need an access token issued by Microsoft Entra and permissions to send emails.</p>
</li>
<li><p>Interacting with AWS Simple Storage Service. Here, you would authenticate a user with AWS Cognito to get an access token that you can exchange for AWS credentials via AWS STS.</p>
</li>
</ul>
<p>You don't need to require user authentication just to know who the user is. The information sent as part of an <strong>Activity</strong> in the <strong>From</strong> and <strong>Recipient</strong> properties can be sufficient.</p>
<p>For more information on Bot Authentication, visit <a target="_blank" href="https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0">User authentication in the Azure AI Bot Service - Bot Service | Microsoft Learn</a>.</p>
<p>So, without further delay, let's begin by preparing the prerequisites.</p>
<h1 id="heading-demo-application"><strong>Demo Application</strong></h1>
<p>This article series includes a demo application called "<strong>ODC with Bots for Teams Demo</strong>," available on <strong>ODC Forge</strong>. Be sure to download the version of the application that matches each article in this series.</p>
<p>For this article, you need to install Version 0.5 from ODC Forge.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>ODC with Bots for Teams Demo</strong>".</p>
</li>
<li><p>Click on the Asset <strong>(Do not click on Install on the tile!)</strong>.</p>
</li>
<li><p>Switch to the <strong>Version</strong> tab and click on Install next to <strong>Version 0.5</strong>.</p>
</li>
</ul>
<p>Version 0.5 depends on other Forge components:</p>
<ul>
<li><p><strong>OAuthTokenExchange</strong> - An external logic library that helps retrieve access tokens easily.</p>
</li>
<li><p><strong>Bot Framework Service API</strong> - A connector library for using Bot Connector API endpoints.</p>
</li>
<li><p><strong>LiquidFluid</strong> - An external logic library used to render handlebar templates based on the Shopify Liquid Templating syntax.</p>
</li>
</ul>
<h1 id="heading-microsoft-entra-application-registration">Microsoft Entra Application Registration</h1>
<p>In Entra Application Registration, we will configure <strong>permissions</strong>, <strong>scope</strong>, and <strong>authorized clients</strong>, an additional <strong>client secret</strong> and a <strong>redirect URI</strong>.</p>
<p>Our demo bot needs to send an email on behalf of the logged-in user. To send emails, it requires the <strong>Mail.Send</strong> permission.</p>
<p>Additionally, we need to create a <strong>custom scope</strong> and add the <strong>Microsoft Teams client application</strong> as an authorized client. This ensures that only these trusted clients can request tokens from Entra on behalf of the user, which will be used in our bot to send an email.</p>
<p>We create a new <strong>client secret</strong> to be used by the Bot Framework token service to acquire the user token during authentication. You already have one secret configured to retrieve an access token for interacting with the Connector API. You could use the same secret, but creating a separate one makes the purpose of each client secret clearer.</p>
<p>Finally, we add a <strong>redirect URI</strong> to our application registration. This is where the authorization code will be sent after a user successfully signs in from Teams. The entire authentication process is managed by the Azure Bot Framework Service, specifically the <strong>Token Service</strong>, and the redirect URI will direct to that service.</p>
<p>In Azure Portal switch to the Entra application registration that is associated with your configured bot.</p>
<h2 id="heading-grant-send-mail-permission">Grant Send Mail Permission</h2>
<p>Under the Manage menu, select API permissions.</p>
<ul>
<li><p>Click <strong>Add a permission</strong>.</p>
</li>
<li><p>In the <strong>Request API permissions</strong> sidebar, select <strong>Microsoft Graph</strong>.</p>
</li>
<li><p>Choose <strong>Delegated Permissions</strong> (Permissions on behalf of a user).</p>
</li>
<li><p>Type <strong>Mail.Send</strong> in the search box and check the box next to Mail.Send (Send mail as a user).</p>
</li>
<li><p>Click <strong>Add permissions</strong>.</p>
</li>
</ul>
<p>Repeat this step to add the following additional permissions</p>
<ul>
<li><p><strong>openid</strong></p>
</li>
<li><p><strong>email</strong></p>
</li>
<li><p><strong>profile</strong></p>
</li>
</ul>
<p>Finally click on <strong>Grant admin consent for &lt;your domain&gt;</strong>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">By granting admin consent, users won't be asked to approve permissions. This makes the process of performing a silent token exchange simpler.</div>
</div>

<h2 id="heading-configure-scope"><strong>Configure Scope</strong></h2>
<p>Under the <strong>Manage</strong> menu, select <strong>Expose an API</strong>.</p>
<ul>
<li><p>Click the Add link next to <strong>Application ID URI</strong>.</p>
</li>
<li><p>For the value, enter <code>api://&lt;your Microsoft Tenant FQDN&gt;/botid-&lt;copied Application (client) ID&gt;</code>.</p>
</li>
<li><p>Click Save.</p>
</li>
</ul>
<p>In the <strong>Scopes defined by this API</strong> section, click on <strong>Add a scope</strong>.</p>
<ul>
<li><p><strong>Scope name</strong> - access_as_user</p>
</li>
<li><p><strong>Who can consent</strong> - Admins and users</p>
</li>
<li><p><strong>Admin consent display name</strong> - Allow Teams to access the user profile and use the bot on behalf of the user</p>
</li>
<li><p><strong>Admin consent description</strong> - Allow Microsoft Teams to access the user profile and call the application's API on behalf of the user</p>
</li>
<li><p><strong>User consent display name</strong> - Allow Teams to access the user profile and use the bot on behalf of the user</p>
</li>
<li><p><strong>User consent description</strong> - Allow Microsoft Teams to access the user profile and call the application's API on behalf of the user</p>
</li>
</ul>
<h2 id="heading-add-authorized-client-applications">Add Authorized Client Applications</h2>
<p>Still in the same menu.</p>
<p>In the <strong>Authorized client applications</strong> section, click on <strong>Add a client application</strong>.</p>
<p>Each Microsoft M365 application, such as Teams, Teams for Web, and Outlook, has a unique client ID. We are now allowing the Teams Desktop and Web applications to access our application API (ODC Exposed REST API) on behalf of the logged-in user. You can find a complete list of Microsoft First-Party apps with their identifiers here: <a target="_blank" href="https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in"><strong>Verify first-party Microsoft applications in sign-in reports | Microsoft Learn</strong></a>.</p>
<p>For the Microsoft Teams <strong>Desktop and Mobile</strong> applications:</p>
<ul>
<li><p><strong>Client ID</strong> - 1fec8e78-bce4-4aaf-ab1b-5451cc387264</p>
</li>
<li><p>Select the checkbox next to the scope you created earlier</p>
</li>
<li><p>Click <strong>Add application</strong></p>
</li>
</ul>
<p>For the Microsoft Teams for <strong>Web</strong> application:</p>
<ul>
<li><p><strong>Client ID</strong> - 5e3ce6c0-2b1f-4285-8d4b-75ee78787346</p>
</li>
<li><p>Select the checkbox next to the scope you created earlier</p>
</li>
<li><p>Click <strong>Add application</strong></p>
</li>
</ul>
<h2 id="heading-add-client-secret"><strong>Add Client Secret</strong></h2>
<p>Under the <strong>Manage</strong> menu select <strong>Certificates &amp; secrets</strong></p>
<p>Select the <strong>Client secrets</strong> tab and click on <strong>New client secret</strong></p>
<ul>
<li><p><strong>Description</strong> - Bot User Authentication</p>
</li>
<li><p><strong>Expires</strong> - Recommended: 180 days (6 months)</p>
</li>
<li><p>Click <strong>Add</strong></p>
</li>
</ul>
<p>Immediately after adding the new client secret, copy the <strong>Value</strong> and save it for later. It will only be displayed once.</p>
<h2 id="heading-redirect-uri"><strong>Redirect URI</strong></h2>
<p>The final configuration step is to add a Redirect URI to the application registration.</p>
<p>Under the <strong>Manage</strong> menu select <strong>Authentication</strong>.</p>
<p>In the Platform configurations section click Add a platform</p>
<ul>
<li><p>Select <strong>Web</strong></p>
</li>
<li><p><strong>Redirect URIs</strong> - https://token.botframework.com/.auth/web/redirect</p>
</li>
<li><p>Click <strong>Configure</strong></p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The redirect URI above is the default for bots without data residency requirements. Microsoft provides additional redirect URIs for different regions. For more details, see <a target="_self" href="https://learn.microsoft.com/en-us/azure/bot-service/ref-oauth-redirect-urls?view=azure-bot-service-4.0"><strong>Supported OAuth URLs - Bot Service | Microsoft Learn</strong></a>.</div>
</div>

<p>With our Entra application registration set up, we can now add some configuration details to our Azure Bot resource.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Before you leave this section, ensure you have the application registration settings for the <strong>Application (client) ID</strong>, the <strong>Application ID URI</strong> (from Expose an API) and the <strong>client secret</strong>.</div>
</div>

<h1 id="heading-azure-bot-resource">Azure Bot Resource</h1>
<p>Under the Settings menu select Configuration</p>
<ul>
<li><p>Click <strong>Add OAuth Connection Settings</strong></p>
</li>
<li><p><strong>Name</strong>: Default</p>
</li>
<li><p><strong>Service Provider</strong>: Azure Active Directory v2</p>
</li>
<li><p><strong>Client id</strong>: &lt;Application (client) ID&gt; from Entra application registration</p>
</li>
<li><p><strong>Client secret</strong>: &lt;Client secret you created you in the previous step&gt;</p>
</li>
<li><p><strong>Token Exchange URL</strong>: leave empty</p>
</li>
<li><p><strong>Tenant ID</strong>: common</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">We use "common" here instead of our Directory (tenant) ID from the application registration because we have set up a multi-tenant bot. For single-tenant bots, you would enter the specific tenant ID.</div>
</div>

<ul>
<li><p><strong>Scopes</strong>: Mail.Send openid email profile</p>
</li>
<li><p>Click <strong>Save</strong></p>
</li>
</ul>
<p>After saving the connection, click on the newly created connection again. In the top right corner of the sidebar, click on Test connection. Complete the sign-in dialog and in the next dialog, you will receive the issued token. You can copy the value and inspect the token at <a target="_blank" href="https://jwt.io">jwt.io</a>.</p>
<h1 id="heading-app-manifest">App Manifest</h1>
<p>The configuration above lets us require users to authenticate through Azure Entra. With Microsoft Teams, we can use a "silent" token exchange feature that swaps a user's Teams access token for an access token issued for the application registration linked to our bot. To enable this "silent" exchange, we need to modify our Teams app manifest and add some extra properties.</p>
<pre><code class="lang-json">{
  ...,
  <span class="hljs-attr">"permissions"</span>: [<span class="hljs-string">"messageTeamMembers"</span>, <span class="hljs-string">"identity"</span>],
  <span class="hljs-attr">"validDomains"</span>: [<span class="hljs-string">"token.botframework.com"</span>],
  <span class="hljs-attr">"webApplicationInfo"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"&lt;Application (client) ID&gt;"</span>,
    <span class="hljs-attr">"resource"</span>: <span class="hljs-string">"api://&lt;domain name&gt;/BotId-&lt;Application (client) ID&gt;"</span>
}
</code></pre>
<ul>
<li><p><strong>permissions - identity</strong>: This permission allows the bot to access user identity information. It lets the bot get user details, which is important for personalized interactions and implementing SSO.</p>
</li>
<li><p><strong>validDomains</strong>: The domain token.botframework.com is included to let the bot use the Bot Framework OAuth flow for authentication. By specifying this domain, the bot can securely interact with the OAuth token service provided by the Bot Framework, which is necessary for handling user authentication and authorization.</p>
</li>
<li><p><strong>webApplicationInfo - id</strong>: This is the <strong>Application (client) ID</strong> of the bot registered in Microsoft Entra ID. It uniquely identifies the bot application and is used during the authentication process to request tokens.</p>
</li>
<li><p><strong>webApplicationInfo - resource</strong>: This is the Application ID URI that represents the bot's API. It specifies the intended audience for the authentication tokens.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The<strong> ODC with Bots for Teams</strong> demo application includes an updated manifest template in <strong>Data - Resources</strong>. After you modify it, reupload the app to Teams.</div>
</div>

<p>After you modify the app manifest with the new values, package it, and reupload the Teams app to Microsoft Teams as described <a target="_blank" href="https://without.systems/odc-with-bots-for-teams-application-manifest#heading-create-the-app-manifest-file">here</a>.</p>
<p>With our prerequisites complete, we can now explore the implementation details. Open ODC Studio and the demo application.</p>
<h1 id="heading-bot-handler">Bot Handler</h1>
<p>This version of the demo application includes a new handler called <strong>BotHandlerSimpleAuthentication</strong>, which is set up as the in-app event handler for the <strong>OnBotActivity</strong> event. In the <strong>Logic</strong> tab, open <strong>Bots - BotHandlerSimpleAuthentication</strong>.</p>
<p>Compared to our previous event handler we made the following additions.</p>
<h2 id="heading-trygetusertoken">TryGetUserToken</h2>
<p>This action checks the TokenStore to see if there is already a valid, non-expired access token available for the sender of the message and returns it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739857785521/0dcb4373-7b5e-4b44-8a48-4056d3695201.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-sendoauthprompt">SendOAuthPrompt</h2>
<p>Back in the main flow and if there is no non-expired access token available for the user our flow prepares an OAuthCard message and sends it to the user.</p>
<p>An <a target="_blank" href="https://learn.microsoft.com/en-us/javascript/api/botframework-schema/oauthcard?view=botbuilder-ts-latest">OAuthCard</a> is used to facilitate OAuth authentication. You will find the OAuthCard template under <strong>Data - Resources - Cards</strong>.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"message"</span>,
    <span class="hljs-attr">"attachments"</span>: [
        {
            <span class="hljs-attr">"contentType"</span>: <span class="hljs-string">"application/vnd.microsoft.card.oauth"</span>,
            <span class="hljs-attr">"content"</span>: {
                <span class="hljs-attr">"connectionName"</span>: <span class="hljs-string">"{{connectionName}}"</span>,
                <span class="hljs-attr">"tokenExchangeResource"</span>: {
                    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"{{activity}}"</span>
                }
            }
        }
    ],
    <span class="hljs-attr">"recipient"</span>: {
        <span class="hljs-attr">"id"</span>: <span class="hljs-string">"{{recipient}}"</span>
    }
}
</code></pre>
<p><strong>RenderOAuthCard</strong> replaces the placeholder values with actual data. It uses the <strong>LiquidFluid</strong> external logic component to create the final card message.</p>
<ul>
<li><p><strong>connectionName</strong> - The configured OAuth connection name in the Azure Bot resource (Default).</p>
</li>
<li><p><strong>activity</strong> - The Activity ID of the last activity that triggered the authentication flow. We will discuss why this value is important later. For now, remember that we use this ID to track the last message a user sent in the conversation.</p>
</li>
<li><p><strong>recipient</strong> - The ID of the recipient.</p>
</li>
</ul>
<p>The rendered OAuthCard is then sent to the conversation just like any other message.</p>
<h2 id="heading-summary">Summary</h2>
<p>In our bot handler, we first try to get a valid access token for the user from the TokenStore entity. If the user doesn't have an access token, or if the access token has expired, we send an OAuthCard to the user in the Teams conversation to trigger a token exchange.</p>
<h1 id="heading-messaging-endpoint">Messaging Endpoint</h1>
<p>As soon as Microsoft Teams receives the <strong>OAuthCard</strong> message, it sends an <strong>Activity</strong> of type <strong>invoke</strong> to the bot's configured messaging endpoint. This Activity contains the user's Microsoft Teams client access token, which we need to exchange for a Bot application token using the <strong>Azure Bot Token API</strong>.</p>
<p>Open the <strong>Messages</strong> endpoint in <strong>Logic - Integrations - REST - MessagingEndpoint</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739860211252/77eaf458-35ac-4e3b-acbb-46a5b1e64249.png" alt class="image--center mx-auto" /></p>
<p>In the Messages endpoint we handle this special activity of type <strong>invoke</strong> and a name of <strong>signin/tokenExchange</strong>.</p>
<h2 id="heading-deserializeinboundtokenexchange">DeserializeInboundTokenExchange</h2>
<p>This action deserializes the request in to a <strong>InvokeTokenExchange</strong> structure, a format the Azure Bot Token API expects to exchange a token.</p>
<h2 id="heading-tryexchangeusertoken">TryExchangeUserToken</h2>
<p>This action first gets an access token to interact with the <strong>Azure Bot Token API</strong> and then calls the ExchangeToken endpoint using our <strong>InvokeTokenExchange</strong> values. If successful, it saves the access token in the <strong>TokenStore</strong> entity for future use.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739859556906/632eda46-4750-4d88-99ab-b3f83636b0a8.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-original-message-playback">Original Message Playback</h2>
<p>Now comes the tricky part. The user's original message hasn't been processed because our bot handler started an OAuth prompt flow, so the original message is essentially lost. Fortunately, all inbound activities are stored in the <strong>Activity</strong> entity.</p>
<p>When we created the OAuthCard, we added the original message's Activity Id to the card. This Id is sent back to us as part of the invoke - <strong>signin/tokenExchange</strong> activity.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"signin/tokenExchange"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"invoke"</span>,
  ...
  <span class="hljs-attr">"value"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"&lt;Activity ID&gt;"</span>,
    <span class="hljs-attr">"token"</span>: <span class="hljs-string">"&lt;Teams client token&gt;"</span>,
    <span class="hljs-attr">"connectionName"</span>: <span class="hljs-string">"Default"</span>
  },
  ...
}
</code></pre>
<p>We now use this Activity ID to get the original message from the Activity entity (<strong>GetOriginActivity</strong>), deserialize the payload (<strong>DeserializeOriginActivity</strong>), and trigger an <strong>OnBotActivity</strong> with the values from the original activity.</p>
<p>This method replays the original message, and since we now have a valid access token, the bot handler will process the inbound message.</p>
<h2 id="heading-summary-1">Summary</h2>
<p>Our Messaging Endpoint manages the token exchange. It gets the Microsoft Teams client access token and swaps it for an access token for our registered Bot application using the Azure Bot Token Service API. Then, it retrieves the original user message from the Activity Store and triggers an OnBotActivity with the original message payload. This step is essential because the original message isn't processed if the user doesn't have a valid access token.</p>
<h1 id="heading-send-mail-via-graph-api">Send Mail via Graph API</h1>
<p>Back in our bot handler (<strong>BotHandlerSimpleAuthentication</strong>) lets review the part when our action flow could successfully retrieve a users access token from the Token Store.</p>
<h2 id="heading-getprofile">GetProfile</h2>
<p>This action queries the <strong>Me</strong> endpoint of the Microsoft Graph API to retrieve the basic user profile using the user's access token. This step is needed to get the User Principal Name and the email address (in most cases, the UPN will be the same as the primary email address).</p>
<p>For this operation, the <strong>User.Read</strong> permission on the Bot application registration in Entra is required.</p>
<h2 id="heading-sendgraphemail">SendGraphEmail</h2>
<p>This action sends a basic email using the Graph API. In our example, the email is sent from the user to themselves, but it's just for demonstration purposes.</p>
<p>For this operation, the <strong>Mail.Send</strong> permission on the Bot application registration in Entra is necessary.</p>
<h2 id="heading-summary-2">Summary</h2>
<p>To send an email, we use the cached user access token to first get the basic user profile, and then we send an email to that user via the Graph API. This shows how a Microsoft Teams client access token can be successfully exchanged for a bot-specific access token with granted permissions in the Entra application registration.</p>
<h1 id="heading-bonus-excerise">Bonus Excerise</h1>
<p>After testing your bot implementation, you'll quickly notice some delay between sending a message and receiving a response from your bot. As an additional exercise, add a <strong>typing</strong> activity (an activity of type <strong>typing</strong>) to:</p>
<ul>
<li><p>The messaging endpoint before triggering the <strong>OnBotActivity</strong> event.</p>
</li>
<li><p>The bot handler right after the condition that checks if the activity type is a message.</p>
</li>
</ul>
<p>Sending a <strong>typing</strong> activity in a conversation improves the user experience by providing feedback.</p>
<h1 id="heading-the-end-of-the-odc-with-bots-for-teams-series">The End of the ODC with Bots for Teams series</h1>
<p>This is the final tutorial in my series on ODC with Bots for Microsoft Teams. As you can imagine, there is a lot more to the topic than what we covered in these tutorials, but I hope the series gives you a good start on your journey to turning OutSystems Developer Cloud into a full-fledged, multi-bot environment.</p>
<p>I hope you enjoyed this tutorial and the entire series. For questions and feedback, please leave a comment here.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[Protect Azure Bot Messaging Endpoint]]></title><description><![CDATA[So far, we have managed to get a simple bot up and running in OutSystems Developer Cloud (ODC). In fact, we've done much more than that. We built a messaging endpoint that can serve as a gateway to multiple bot applications in ODC, where each bot app...]]></description><link>https://without.systems/odc-with-bots-for-teams-messaging-endpoint-authorization</link><guid isPermaLink="true">https://without.systems/odc-with-bots-for-teams-messaging-endpoint-authorization</guid><category><![CDATA[outsystems]]></category><category><![CDATA[azure bot service]]></category><category><![CDATA[Bot Framework]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Wed, 05 Feb 2025 09:47:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738660296961/1551a7ec-4d68-4711-9bde-9c90243549e9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So far, we have managed to get a simple bot up and running in OutSystems Developer Cloud (ODC). In fact, we've done much more than that. We built a messaging endpoint that can serve as a gateway to multiple bot applications in ODC, where each bot application can respond to inbound messages triggered as events individually. This setup is quite versatile and dynamic.</p>
<p>However, we are still missing two important implementations. First, our messaging endpoint is publicly available without any authorization checks. Essentially, anything could mimic an inbound message, and our bot would react, which is not good.</p>
<p>Second, in some scenarios, we need a user's credentials, not just their ID, to perform actions on behalf of that user in a bot implementation. Currently, through a Teams channel, we get the identifier of a user logged into Teams, but to use Microsoft Graph API operations on behalf of that user, we need the user to explicitly authenticate to retrieve their access token for Graph API operations.</p>
<p>In this tutorial, we will protect our messaging endpoint to ensure that we only handle requests initiated by Azure AI Bot services. In the next tutorial, we will explore how to authenticate a user and retrieve an access token to act on behalf of a user with the Graph API.</p>
<h1 id="heading-demo-application"><strong>Demo Application</strong></h1>
<p>This article series includes a demo application called "<strong>ODC with Bots for Teams Demo</strong>," available on <strong>ODC Forge</strong>. Be sure to download the version of the application that matches each article in this series.</p>
<p>For this article, you need to install Version 0.4 from ODC Forge.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>ODC with Bots for Teams Demo</strong>".</p>
</li>
<li><p>Click on the Asset <strong>(Do not click on Install on the tile!)</strong>.</p>
</li>
<li><p>Switch to the <strong>Version</strong> tab and click on Install next to <strong>Version 0.4</strong>.</p>
</li>
</ul>
<p>Version 0.4 depends on other Forge components:</p>
<ul>
<li><p><strong>OAuthTokenExchange</strong> - An external logic library that helps retrieve access tokens easily.</p>
</li>
<li><p><strong>Bot Framework Service API</strong> - A connector library for using Bot Connector API endpoints.</p>
</li>
</ul>
<h1 id="heading-authenticating-requests-to-the-messaging-endpoint">Authenticating Requests to the Messaging Endpoint</h1>
<p>Any request from a configured channel is routed through the <strong>Azure AI Bot Connector service</strong>, which adds a <strong>signed JSON Web Token</strong> to the Authorization header of each request. The entire authentication and authorization process is well documented here: <a target="_blank" href="https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication?view=azure-bot-service-4.0&amp;tabs=multitenant#connector-to-bot">Authenticate requests with the Bot Connector API - Bot Service | Microsoft Learn</a>. It involves the following steps:</p>
<ul>
<li><p>Retrieve the token from the <strong>Authorization</strong> header.</p>
</li>
<li><p>Parse the <strong>token header</strong> to identify which <strong>key</strong> was used to sign the token.</p>
</li>
<li><p>Retrieve the <strong>public key</strong> for the identified key from the Bot Framework <strong>JSON Web Key Set URI</strong>.</p>
</li>
<li><p>Validate the token.</p>
</li>
</ul>
<p>In ODC, we manage all of this with a custom <strong>OnAuthentication</strong> handler on the exposed Messaging endpoint.</p>
<h2 id="heading-onauthentication-handler">OnAuthentication Handler</h2>
<p>In the demo application under <strong>Logic - Integrations - REST</strong>, note that our <strong>MessagingEndpoint</strong> REST API authentication setting is now configured with <strong>Custom</strong>, which includes the <strong>OnAuthentication</strong> handler. <strong>OnAuthentication</strong> runs before a request reaches the actual endpoint.</p>
<p>Double-click the <strong>OnAuthentication</strong> handler to review the implementation.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738673329255/b0d097b7-5335-4163-ab37-f1d0a643ded9.png" alt class="image--center mx-auto" /></p>
<p>The <strong>OnAuthentication</strong> handler performs several actions, starting with retrieving the value of the Authorization request header using <strong>Request_GetHeader</strong> from the HTTP module. The value is formatted as <code>Bearer &lt;encoded JSON web token&gt;</code>. The JSON web token consists of three parts separated by dots. Each part is a base64 encoded JSON document.</p>
<ul>
<li><p><strong>Header</strong> - Contains information about the token and how the signature was calculated.</p>
</li>
<li><p><strong>Payload</strong> - The token's payload with various key-value pairs, also known as claims.</p>
</li>
<li><p><strong>Signature</strong> - The signature of the payload.</p>
</li>
</ul>
<h2 id="heading-parseauthorizationheader-action">ParseAuthorizationHeader Action</h2>
<p>This action performs some <strong>String_Split</strong> operations. First, it splits by a single space to divide the Authorization header value into the word "Bearer" and the encoded JSON web token.</p>
<p>Next, it splits the encoded JSON web token by a single dot to separate the Header, Payload, and Signature parts.</p>
<p>Note the individual <strong>IF</strong> conditions that raise an exception if the Authorization header is empty or if the split actions do not produce the expected results.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738676047708/f846b039-254b-4c93-8aad-0086a5ace7aa.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-parsejwtheader-action">ParseJwtHeader Action</h2>
<p>This action converts the encoded Header of the token into a <strong>JwtHeader</strong> structure and returns it. The Header structure includes the Key Identifier of the public key that we need to use to verify the token's signature.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738676992280/08abf4e0-18ef-47a2-be45-6eb6ea36efcc.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-getjwkskey-action">GetJwksKey Action</h2>
<p>This action retrieves the public key from the Bot Framework Identity Provider's JSON Web Key Set endpoint. The Bot Framework Identity Provider issues the signed token, and its URL is <code>https://login.botframework.com</code>.</p>
<ul>
<li><p>First we retrieve the Discovery document from the Identity Provider that contains the URI of the JSON Web Key Set endpoint using <strong>GetDiscoveryDocument</strong> from the <strong>OAuthTokenExchange</strong> Forge component.</p>
</li>
<li><p>Then we perform a <strong>GET</strong> request to the endpoint that returns the key sets using <strong>Request_SubmitGetRequest</strong> from the <strong>HTTP</strong> module and deserialize the response into a structure for easy filtering.</p>
</li>
<li><p>We filter the list of retrieved keys to the key identifier retrieved from the token header using a <strong>ListFilter</strong> action.</p>
</li>
<li><p>And finally we <strong>serialize</strong> the single key and return it.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738676733434/ab92d002-b4b7-4775-b648-41bae68e0d61.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Important Note</strong>: This action retrieves the key with every request to the messaging endpoint, which adds extra delay. In a production environment, you should consider periodically syncing the keys from the JWKS endpoint and retrieving the individual key from a cache-enabled entity.</div>
</div>

<h2 id="heading-validatetoken-action">ValidateToken Action</h2>
<p>In <strong>ValidateToken</strong>, we use <strong>JWT_ReadToken</strong> from the <strong>Security</strong> module to check the signature with the retrieved public key.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">If the validation fails, <strong>ValidateToken </strong>will raise an exception.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738676948109/03db5506-f491-4c33-bddf-689b2411a47d.png" alt class="image--center mx-auto" /></p>
<p>If validation succeeds, the request is forwarded to the Messages endpoint for further processing.</p>
<h1 id="heading-summary">Summary</h1>
<p>At the end of this tutorial, we protected our messaging endpoint by validating the access token sent from the Azure AI Bot service. The great thing about this tutorial is that you can use this method whenever you need to validate a signed JSON web token.</p>
<p>Feel free to leave a comment with your questions or feedback.</p>
]]></content:encoded></item><item><title><![CDATA[Microsoft Teams App for Conversational Bots]]></title><description><![CDATA[After we got our bot to respond to a user in the previous part of the ODC with Bots for Teams series, we will now add the bot to our Microsoft Teams client application. Although this is a straightforward task, there are some prerequisites you need to...]]></description><link>https://without.systems/odc-with-bots-for-teams-application-manifest</link><guid isPermaLink="true">https://without.systems/odc-with-bots-for-teams-application-manifest</guid><category><![CDATA[outsystems]]></category><category><![CDATA[azure bot service]]></category><category><![CDATA[Bot Framework]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Mon, 03 Feb 2025 15:22:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738048878824/f6ee88e2-e0ff-40dc-a94b-2eabbb9cff87.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After we got our bot to respond to a user in the previous part of the ODC with Bots for Teams series, we will now add the bot to our Microsoft Teams client application. Although this is a straightforward task, there are some prerequisites you need to meet before you can do this.</p>
<p>An app manifest file connects your bot to the client application. It is a JSON document that describes your bot, its appearance, and behavior within Teams. App manifest files are used not only in Microsoft Teams but also in many other Microsoft 365 applications, providing a unified way to add external app resources to the Microsoft 365 environment.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">At the time of writing, Microsoft is still working on unifying the different manifest definitions across all products.</div>
</div>

<p>Manifest documents and its referenced icon resources are packaged into a ZIP file and then deployed to Microsoft Teams clients either</p>
<ul>
<li><p>By <strong>sideloading</strong> into a single Teams client - This is what we will do in this tutorial. We will upload the Teams app to our local Microsoft Teams client.</p>
</li>
<li><p><strong>Distributing it within the organization</strong> - You can upload a Teams app to your organization, making it available for on-demand or policy-based installation internally.</p>
</li>
<li><p><strong>Distributing it via the Microsoft Teams Store</strong> - You can release your Teams bot/app to the official Microsoft Teams Store to make your solution publicly available. However, your app must meet the store guidelines.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Before continuing, ensure you have successfully completed the other parts of the series. You should be able to receive a response using the <strong>Test with Web Chat</strong> feature in your Bot registration on Azure.</div>
</div>

<p>Now, without further ado, let's begin by setting up the prerequisites.</p>
<h1 id="heading-demo-application"><strong>Demo Application</strong></h1>
<p>This article series includes a demo application called "<strong>ODC with Bots for Teams Demo</strong>," available on <strong>ODC Forge</strong>. Be sure to download the version of the application that matches each article in this series.</p>
<p>For this article, you need to install Version 0.3 from ODC Forge.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>ODC with Bots for Teams Demo</strong>".</p>
</li>
<li><p>Click on the Asset <strong>(Do not click on Install on the tile!)</strong>.</p>
</li>
<li><p>Switch to the <strong>Version</strong> tab and click on Install next to <strong>Version 0.3</strong>.</p>
</li>
</ul>
<p>Version 0.3 depends on other Forge components:</p>
<ul>
<li><p><strong>OAuthTokenExchange</strong> - An external logic library that helps retrieve access tokens easily.</p>
</li>
<li><p><strong>Bot Framework Service API</strong> - A connector library for using Bot Connector API endpoints.</p>
</li>
</ul>
<h1 id="heading-allow-sideloading">Allow Sideloading</h1>
<p>By default users are not allowed to upload custom apps into Microsoft Teams. This is defined in a Teams Setup Policy that is associated with your user account in the Microsoft Teams Admin center.</p>
<p>In the <a target="_blank" href="https://admin.teams.microsoft.com/">Microsoft Teams Admin center</a>.</p>
<ul>
<li><p>Select <strong>Teams apps - Setup policies</strong> in the menu</p>
</li>
<li><p>Click <strong>Add</strong></p>
</li>
<li><p><strong>Name</strong>: OutSystems Bot Developer Setup Policy</p>
</li>
<li><p><strong>Description</strong>: Allows an OutSystems Bot developer to sideload Microsoft Teams apps</p>
</li>
<li><p>Click <strong>Save</strong></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738561390002/0fc45c12-fc8f-4550-956d-deb8786b4338.png" alt class="image--center mx-auto" /></p>
<p>After we have created the policy, we must assign it to one or more users</p>
<ul>
<li><p>Select <strong>Users - Manage users</strong> in the menu</p>
</li>
<li><p>Search your own user account and select it</p>
</li>
<li><p>In the <strong>Policies</strong> tab click <strong>Edit</strong></p>
</li>
<li><p>Select the <strong>OutSystems Bot Developer Setup Policy</strong> in the <strong>Select App setup policy dropdown</strong></p>
</li>
<li><p>Click <strong>Apply</strong></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738561551839/8c43bde8-df66-4d48-8df8-d8a34dad5ffc.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It may take a little while for the new settings to take effect. You will need to restart your Microsoft Teams client.</div>
</div>

<h1 id="heading-activate-teams-channel">Activate Teams Channel</h1>
<p>Next, we need to activate Teams channel support in our Bot resource.</p>
<ul>
<li><p>In the Azure Portal, select your Bot resource.</p>
</li>
<li><p>Choose <strong>Microsoft Teams</strong> from the list of <strong>Available Channels</strong> in the <strong>Settings - Channels</strong> menu.</p>
</li>
<li><p>Read the Terms of Service and <strong>Agree</strong>.</p>
</li>
<li><p>In the <strong>Messaging</strong> tab, select <strong>Microsoft Teams Commercial (most common)</strong>.</p>
</li>
<li><p>Click <strong>Apply</strong>.</p>
</li>
</ul>
<h1 id="heading-create-the-app-manifest-file">Create the App Manifest File</h1>
<p>The demo project includes a template for the manifest file, complete with icons. Download the template archive from <strong>Data - Resources</strong> in ODC Studio and extract it to a folder.</p>
<p>An app manifest file is a JSON document that follows the unified app manifest schema. Together with the manifest file you will also need two icons.</p>
<ul>
<li><p><strong>Full color icon</strong> with a size of 192×192</p>
</li>
<li><p><strong>Outline icon</strong> with a size of 32×32</p>
</li>
</ul>
<p>Create a folder on your development machine</p>
<ul>
<li><p>Copy both icons to the folder</p>
</li>
<li><p>Create a new file <strong>manifest.json</strong></p>
</li>
<li><p>Modify <strong>id</strong> with your bot’s application id from Entra App registration</p>
</li>
<li><p>Modify <strong>botId</strong> with your bot’s application id from Entra App registration</p>
</li>
<li><p>Check that the icon names match your uploaded icons.</p>
</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"manifestVersion"</span>: <span class="hljs-string">"1.19"</span>,
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"&lt;Application ID from app registration&gt;"</span>,
  <span class="hljs-attr">"name"</span>: {
    <span class="hljs-attr">"short"</span>: <span class="hljs-string">"ODCwithTeams Bot"</span>,
    <span class="hljs-attr">"full"</span>: <span class="hljs-string">"ODCwithTeams Demo Bot"</span>
  },
  <span class="hljs-attr">"developer"</span>: {
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"without.systems"</span>,
    <span class="hljs-attr">"websiteUrl"</span>: <span class="hljs-string">"https://without.systems"</span>,
    <span class="hljs-attr">"privacyUrl"</span>: <span class="hljs-string">"https://without.systems/privacy"</span>,
    <span class="hljs-attr">"termsOfUseUrl"</span>: <span class="hljs-string">"https://without.systems/termsofuse"</span>
  },
  <span class="hljs-attr">"description"</span>: {
    <span class="hljs-attr">"short"</span>: <span class="hljs-string">"Shows how to build a bot"</span>,
    <span class="hljs-attr">"full"</span>: <span class="hljs-string">"Full description of your app."</span>
  },
  <span class="hljs-attr">"icons"</span>: {
    <span class="hljs-attr">"outline"</span>: <span class="hljs-string">"outline.png"</span>,
    <span class="hljs-attr">"color"</span>: <span class="hljs-string">"color.png"</span>
  },
  <span class="hljs-attr">"accentColor"</span>: <span class="hljs-string">"#FFFFFF"</span>,
  <span class="hljs-attr">"bots"</span>: [
    {
      <span class="hljs-attr">"botId"</span>: <span class="hljs-string">"&lt;Application ID from app registration&gt;"</span>,
      <span class="hljs-attr">"scopes"</span>: [
        <span class="hljs-string">"personal"</span>
      ],
      <span class="hljs-attr">"isNotificationOnly"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"supportsCalling"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"supportsVideo"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"supportsFiles"</span>: <span class="hljs-literal">false</span>
    }
  ]
}
</code></pre>
<p>This is a basic app manifest file, which we will expand on in the upcoming articles of the series. You can find a complete description of the schema here: <a target="_blank" href="https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema">App Manifest Reference - Teams | Microsoft Learn</a>.</p>
<p>After you have prepared your manifest file, select all the files in the folder (the manifest and icons) and compress them into a zip archive. The name of the zip archive doesn't matter.</p>
<h1 id="heading-install-app-to-microsoft-teams">Install App to Microsoft Teams</h1>
<p>With our package prepared we can now install it to Microsoft Teams. Open your Microsoft Teams client and in the left icon menu click the Apps icon.</p>
<ul>
<li><p>On the bottom left click on <strong>Manage your apps</strong></p>
</li>
<li><p>In the apps list click the <strong>Upload an app</strong> button and select <strong>Upload a custom app</strong></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738567617228/900ff31d-9b0a-4d4b-bc51-a4352b3c3e14.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Select the <strong>zip archive</strong> you created</p>
</li>
<li><p>Check the details of the app, then click <strong>Add</strong></p>
</li>
</ul>
<p>After sucessful upload click the <strong>Open</strong> button to start a conversation with your bot.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738567778127/e4b639ea-d084-42b3-8517-f1239928d162.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-summary">Summary</h1>
<p>In this tutorial, we added our Bot to Microsoft Teams. First, we enabled sideloading of Teams apps by creating a new setup policy in the Microsoft Teams Admin Center. Then, we created a manifest application package and uploaded it to a Microsoft Teams client.</p>
<p>Congratulations! This concludes this part of the series. In the next part, we will protect our messaging endpoint, as it is currently publicly available and does not perform any authorization checks.</p>
<p>Feel free to leave a comment with your questions or feedback. See you in the next part!</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[DeepSeek R1 in OutSystems Developer Cloud with Amazon Bedrock]]></title><description><![CDATA[The release of DeepSeek R1 has gained a lot of attention. It shows reasoning abilities that match, and sometimes surpass, OpenAI's O1 model, achieving these results with only a fraction of the resources needed to train OpenAI's O1 model. At the time ...]]></description><link>https://without.systems/deepseek-odc-bedrock</link><guid isPermaLink="true">https://without.systems/deepseek-odc-bedrock</guid><category><![CDATA[outsystems]]></category><category><![CDATA[genai]]></category><category><![CDATA[Deepseek]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Fri, 31 Jan 2025 09:55:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738232151779/93c6a9f1-0703-44c9-a505-bcfe45b2f0ed.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The release of DeepSeek R1 has gained a lot of attention. It shows reasoning abilities that match, and sometimes surpass, OpenAI's O1 model, achieving these results with only a fraction of the resources needed to train OpenAI's O1 model. At the time of writing, OpenAI charges $15 for 1 million input tokens and $60 for 1 million output tokens through their API, while DeepSeek charges $0.55 and $2.19 for 1 million input and output tokens, respectively. The best part is that DeepSeek models are open source, and you can run them on a runtime of your choice, like <a target="_blank" href="https://ollama.com/">Ollama</a>, <a target="_blank" href="https://github.com/vllm-project/vllm">vLLM</a>, <a target="_blank" href="https://lmstudio.ai/">LM Studio</a>, <a target="_blank" href="https://azure.microsoft.com/en-us/products/ai-foundry">Azure AI</a>, <a target="_blank" href="https://aws.amazon.com/bedrock/">Amazon Bedrock</a> and many more.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">A reasoning model in AI is created to mimic human decision-making and problem-solving. It uses methods like logical reasoning, probabilistic inference, and machine learning to examine data, reach conclusions, and make decisions.</div>
</div>

<p>In this tutorial, we will walk through the steps to set up DeepSeek R1 on Amazon Bedrock as an <strong>Imported Model</strong> and use it in OutSystems Developer Cloud applications.</p>
<p>Alternatively, DeepSeek R1 is also available for <a target="_blank" href="https://docs.aws.amazon.com/bedrock/latest/userguide/amazon-bedrock-marketplace.html"><strong>marketplace deployment</strong></a>, which is more suitable for production use. Marketplace deployments are set up on SageMaker, offering more configuration options for scalability and security. However, deploying DeepSeek through the marketplace <strong>requires a service quota for p5 compute instances</strong>, which must be requested first. (It is actually a feature to prevent accidental high costs). That's why we use the <strong>Imported Models</strong> feature of Bedrock, which doesn't have this restriction but <strong>does come with some limitations</strong>. See the <strong>Important Notes</strong> section of this article. For a development and trial environment, it is good enough.</p>
<h1 id="heading-amazon-bedrock-imported-models">Amazon Bedrock Imported Models</h1>
<p>Bedrock is an easy-to-use, fully managed AI service by AWS. It provides access to a wide range of AI models from Amazon Bedrock's model catalog through a unified API. In addition to these catalog models, Bedrock also allows the import of other models.</p>
<p>Imported models must follow specific predefined architectures to be supported by Bedrock. As of this writing, the supported model architectures include:</p>
<ul>
<li><p>Mistral</p>
</li>
<li><p>Mixtral</p>
</li>
<li><p>Llama 2, Llama 3, Llama 3.1, Llama 3.2, and Llama 3.3</p>
</li>
<li><p>Flan</p>
</li>
</ul>
<p>For a complete list of supported architectures, see <a target="_blank" href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-customization-import-model.html#model-customization-import-model-architecture">Supported Architectures</a> in the Amazon Bedrock documentation.</p>
<p>Fortunately, DeepSeek R1 is available as a distilled Llama model, which lets us import it into Amazon Bedrock. Distillation means that a teacher model (DeepSeek R1) transfers its "knowledge" to a student model (Llama 3.1). DeepSeek R1 and its distilled models are available on Huggingface for download.</p>
<p>For our walkthrough we will use <a target="_blank" href="https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-8B">deepseek-ai/DeepSeek-R1-Distill-Llama-8B</a>.</p>
<h1 id="heading-steps-outline">Steps Outline</h1>
<p>The steps are straightforward, but they may take some time because of the model's size.</p>
<ul>
<li><p>Upload the model to an S3 bucket</p>
</li>
<li><p>Create a model import job in the Bedrock console</p>
</li>
<li><p>Use the model in ODC</p>
</li>
</ul>
<h1 id="heading-prerequisites">Prerequisites</h1>
<p>Before we begin, ensure you meet all the necessary prerequisites.</p>
<h2 id="heading-environment">Environment</h2>
<ul>
<li><p><strong>AWS Account</strong> - You need access to an AWS account with permissions for Amazon Bedrock and Amazon Simple Storage Service.</p>
</li>
<li><p><strong>Git</strong> - On your development workstation, you need the <a target="_blank" href="https://git-scm.com/downloads">git</a> command line tools and <a target="_blank" href="https://git-lfs.com/">git lfs</a> installed.</p>
</li>
<li><p><strong>AWS Credentials</strong> - Access Key and Secret Access Key for an IAM user with permissions to use custom models in Amazon Bedrock.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Not all AWS regions support custom model import. In this tutorial, we are using <strong>us-east-1</strong>.</div>
</div>

<ul>
<li><p><strong>S3 Bucket</strong> - An empty S3 bucket in the us-east-1 region (Bedrock only allows model imports from an S3 bucket).</p>
</li>
<li><p><strong>AWS CLI</strong> - Optional. Can be use as an alternative to upload model files to S3 instead of the console.</p>
</li>
</ul>
<h2 id="heading-outsystems-developer-cloud">OutSystems Developer Cloud</h2>
<p>To follow along, you will need to install two assets from Forge into your OutSystems Developer Cloud development stage.</p>
<ul>
<li><p><strong>AWSBedrockRuntime</strong> - provides an action to execute the Amazon Bedrock InvokeModel API.</p>
</li>
<li><p><strong>AWS Bedrock Model Invocations</strong> - server actions to create a model-specific prompt for model invocation.</p>
</li>
</ul>
<p>With all the prerequisites completed, let's start by uploading the model to an S3 bucket.</p>
<h1 id="heading-upload-model-to-s3">Upload Model to S3</h1>
<p>Unfortunately, there isn't a direct way to copy a model from Huggingface into an S3 bucket, or at least I haven't found one. First, we download the model from Huggingface to our local computer, and then we upload it to S3.</p>
<p>Run the following command in a terminal window:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> --depth 1 https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-8B
</code></pre>
<p>This command will clone the Huggingface repository into a folder named DeepSeek-R1-Distill-Llama-8B. Models are quite large, so the download may take some time.</p>
<p>After the download is complete, upload the entire folder to the S3 bucket you created. You can use either the AWS S3 console or the AWS command line interface if it is installed and set up.</p>
<pre><code class="lang-bash">aws s3 cp DeepSeek-R1-Distill-Llama-8B s3://&lt;bucket&gt;/ --recursive
</code></pre>
<h1 id="heading-import-model-to-amazon-bedrock">Import Model to Amazon Bedrock</h1>
<p>Next, switch to the AWS Bedrock console. Make sure to select the us-east-1 region. In the console's menu, select <strong>Imported models</strong> and click <strong>Import Model</strong>.</p>
<p>In the dialog:</p>
<ul>
<li><p><strong>Model Details - Model name</strong>: DeepSeek-R1-Distill-Llama-8B.</p>
</li>
<li><p><strong>Import job name - Name</strong>: Import-DeepSeek-R1-Distill-Llama-8B.</p>
</li>
<li><p><strong>Model import settings - Model import source</strong>: Amazon S3 bucket.</p>
</li>
<li><p><strong>Model import settings - S3 location</strong>: Choose the DeepSeek-R1-Distill-Llama-8B folder you uploaded to your S3 bucket.</p>
</li>
<li><p><strong>Service access - Choose a method to authorize Bedrock</strong>: Create and use a new service role.</p>
</li>
<li><p><strong>Service access - Service role name</strong>: AWSBedrockModelImportRole. This role will have permission to access your S3 bucket, and you can reuse it later for other model imports to Bedrock.</p>
</li>
</ul>
<p>Click Import Model to start the job. It will take several minutes to complete, and you can check the status in the Jobs tab.</p>
<p>After the job completes successfully, an entry will appear in the model tab. Click on the entry and copy the model <strong>ARN</strong> (Amazon Resource Name).</p>
<p>At this stage, you can also open the model in the Playground and interact with it. If you encounter a "Model not Ready" exception, please refer to the <strong>Important Notes</strong> section of this article.</p>
<p>You will need the model <strong>ARN</strong>, along with <strong>AWS credentials</strong>, to use the model from an OutSystems application.</p>
<h1 id="heading-using-the-model">Using the Model</h1>
<p>Using the model in an ODC app is straightforward. The <strong>AWSBedrockRuntime</strong> Forge component wraps the official Amazon SDK for Bedrock and provides an action called <strong>InvokeModel</strong> for direct model use. This action requires the payload—the request—as binary data. Here's how to create a payload for the DeepSeek R1 model we just imported.</p>
<ul>
<li><p>Open the <strong>AWS Bedrock Model Invocations</strong> library in ODC Studio.</p>
</li>
<li><p>Double-click the server action <strong>Deepseek_Llama_Invoke</strong> in <strong>Logic - Server Actions - Invocation</strong>.</p>
</li>
</ul>
<p>Check the Request input parameter, which has an array of <strong>Messages</strong> with a <strong>Role</strong> and <strong>Content</strong> attribute. The <strong>Role</strong> can be either user or assistant, and the <strong>Content</strong> is any text. This structure somewhat follows the OpenAI API standard for conversational message prompts.</p>
<p>However, a model does not understand this structure directly. Instead, it uses specialized tokens to distinguish between a system prompt, a user message, or an assistant message. Therefore, the first step is to create a prompt string from this structure, which is done in the <strong>GenerateLlamaInvokePayload</strong> action.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Each model architecture has its own specialized tokens. For Llama 3.1, which is the architecture for our imported DeepSeek R1 model, these tokens are documented in the <a target="_self" href="https://www.llama.com/docs/model-cards-and-prompt-formats/llama3_1/">Meta Llama documentation</a>.</div>
</div>

<p><strong>GenerateLlamaInvokePayload</strong> performs the following steps</p>
<ul>
<li><p>Creates a <strong>StringBuilder</strong> objects</p>
</li>
<li><p>If the request has a system prompt it appends</p>
</li>
</ul>
<pre><code class="lang-plaintext">"&lt;|begin_of_text|&gt;&lt;|start_header_id|&gt;system&lt;|end_header_id|&gt;" + NewLine() +
Request.System + "&lt;|eot_id|&gt;" + NewLine()
</code></pre>
<ul>
<li>Iterates over all Message items of the <strong>Messages</strong> array and appends</li>
</ul>
<pre><code class="lang-plaintext">"&lt;|start_header_id|&gt;" + Request.Messages.Current.Role + "&lt;|end_header_id|&gt;" + NewLine() +
Request.Messages.Current.Content + "&lt;|eot_id|&gt;" + NewLine()
</code></pre>
<ul>
<li><p>Execute the <strong>StringBuilder_ToString</strong> action to get the complete prompt string.</p>
</li>
<li><p>Assign global request parameters, such as <strong>MaximumLength</strong>, and the <strong>prompt string</strong> to a local variable.</p>
</li>
<li><p>Serialize the local variable.</p>
</li>
<li><p>Convert the serialized payload to binary data.</p>
</li>
<li><p>Return the binary data payload.</p>
</li>
</ul>
<p>In the <strong>Deepseek_Llama_Invoke</strong> action, this payload is used along with your AWS credentials to invoke the model.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Check out the <strong>Meta_Llama_Invoke</strong> action, and you'll see that it is an exact duplicate because both use the same model architecture.</div>
</div>

<p>Now it's up to you to build something on top of it.</p>
<h1 id="heading-important-notes">Important Notes</h1>
<p>Imported models in Amazon Bedrock are removed when they are not used for a couple of minutes and it takes up to 10 seconds depending on the model to cold start it. This causes additional latency and even my lead to a <strong>ModelNotReady</strong> exception. You can read more in the <a target="_blank" href="https://docs.aws.amazon.com/bedrock/latest/userguide/invoke-imported-model.html#handle-model-not-ready-exception">documentation</a>.</p>
<p>Take some time to review <a target="_blank" href="https://aws.amazon.com/bedrock/pricing">Bedrock pricing</a> for imported models.</p>
<h1 id="heading-summary">Summary</h1>
<p>In this tutorial, we imported a distilled DeepSeek R1 model into Amazon Bedrock. We also explored how to create a prompt string from a request structure in ODC to perform a direct model invocation.</p>
<p>I hope you enjoyed it. Feel free to leave a comment with your questions, or even better, tell us what you have built. We greatly appreciate any feedback.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[Respond to Channel Messages]]></title><description><![CDATA[This is the third part of the ODC with Bots for Teams series. In part two, we set up an Azure Bot resource in your Azure tenant and connected it to a REST API endpoint in OutSystems Developer Cloud, known as the Messaging Endpoint. We tested this set...]]></description><link>https://without.systems/odc-with-bots-for-teams-respond-to-messages</link><guid isPermaLink="true">https://without.systems/odc-with-bots-for-teams-respond-to-messages</guid><category><![CDATA[outsystems]]></category><category><![CDATA[azure bot service]]></category><category><![CDATA[Bot Framework]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Mon, 27 Jan 2025 13:04:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737384965829/980fd15a-5021-425c-bcf8-667ac162480f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the third part of the <a target="_blank" href="https://without.systems/series/odc-msteams-bots">ODC with Bots for Teams</a> series. In part two, we set up an Azure Bot resource in your Azure tenant and connected it to a REST API endpoint in OutSystems Developer Cloud, known as the Messaging Endpoint. We tested this setup using the "<strong>Test with Web Chat</strong>" feature and checked the received messages in the debugger.</p>
<p>In this tutorial, we will parse incoming messages and respond to them using the Bot Connector API.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Make sure you have saved all the values from the previous tutorial: the <strong>Application ID</strong>, T<strong>enant ID</strong>, and <strong>Client Secret</strong>.</div>
</div>

<h1 id="heading-demo-application">Demo Application</h1>
<p>This article series includes a demo application called "<strong>ODC with Bots for Teams Demo</strong>," available on <strong>ODC Forge</strong>. Be sure to download the version of the application that matches each article in this series.</p>
<p>For this article, you need to install Version 0.2 from ODC Forge.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>ODC with Bots for Teams Demo</strong>".</p>
</li>
<li><p>Click on the Asset <strong>(Do not click on Install on the tile!)</strong>.</p>
</li>
<li><p>Switch to the <strong>Version</strong> tab and click on Install next to <strong>Version 0.2</strong>.</p>
</li>
</ul>
<p>Version 0.2 depends on other Forge components:</p>
<ul>
<li><p><strong>OAuthTokenExchange</strong> - An external logic library that helps retrieve access tokens easily.</p>
</li>
<li><p><strong>Bot Framework Service API</strong> - A connector library for using Bot Connector API endpoints.</p>
</li>
</ul>
<h1 id="heading-responding-to-messages">Responding to Messages</h1>
<p>In the introduction to this series, I explained the different types of activities a messaging endpoint can receive from a channel. The most important type is a message. The messaging endpoint receives this activity when someone sends a text message in a conversation where the bot is involved.</p>
<p>In this article, we will explore how to respond to a message activity.</p>
<p>We will use a pattern I came up with after several tries. I believe it provides the most flexible and adaptable way to build bots with OutSystems Developer Cloud. The main idea is that a single messaging endpoint—the one we already created—can be used for multiple connected Azure Bot resources. Additionally, multiple bots can be built as ODC applications to respond to incoming activities. At a glance this pattern involves the following steps</p>
<ul>
<li><p><strong>Handle Inbound Request</strong> - Our messaging endpoint receives the entire activity payload as text. In this first step, we partially deserialize the request and store the whole payload in an entity record, along with some additional data, for later retrieval. Finally, we trigger an event with the activity details.</p>
</li>
<li><p><strong>Subscribe and Handle Event</strong> - Next, we handle the event. For simplicity in the demo, we handle the event directly in the demo application, but you can also create a separate application, subscribe to the event, and handle it there. Handling involves checking the activity type, retrieving the whole activity payload from the entity, deserializing the necessary parts, and then performing actions to respond to the activity.</p>
</li>
<li><p><strong>Authorize with Connector API</strong> - To send a message back to the conversation, we need to authorize with the Connector API. In our handler, we request an access token using our Entra application registration details.</p>
</li>
<li><p><strong>Build and Send Response</strong> - Finally, we create a response message and send it to the conversation through the Connector API.</p>
</li>
</ul>
<h1 id="heading-demo-application-settings">Demo Application Settings</h1>
<p>Before we go through the implementation details, let's configure the demo application's settings.</p>
<p>In the previous part of this series you created and configured Azure Bot resource and an attached Entra application registration. Make sure that you have copied <strong>Application (client) ID</strong> and the generated <strong>Client Secret</strong> from the application registration.</p>
<p>In <strong>ODC Portal - Apps</strong>, select "<strong>ODC with Bots for Teams Demo</strong>."</p>
<p>In the <strong>Configuration</strong> tab, set the following values:</p>
<ul>
<li><p><strong>EntraClientId</strong> - Application (client) ID from the Entra application registration</p>
</li>
<li><p><strong>EntraClientSecret</strong> - Client Secret value from the Entra application registration</p>
</li>
</ul>
<p>With our configuration set, we can now proceed with the implementation details.</p>
<h1 id="heading-handle-inbound-activities">Handle Inbound Activities</h1>
<p>The exposed Messaging REST API endpoint receives activities from a channel. For more information on the different activity types, see the Introduction article. In the endpoint action flow, we perform the following steps:</p>
<ul>
<li><p>Deserialize part of the activity payload</p>
</li>
<li><p>Create an entity record with basic activity details and the full payload</p>
</li>
<li><p>Trigger an <strong>OnBotActivity</strong> event</p>
</li>
</ul>
<p>Switch to <strong>ODC Studio</strong> and double-click the <strong>Messages</strong> endpoint in <strong>Logic - Integrations - REST - MessagingEndpoint.</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737872015122/f726c9db-1116-4f52-91ba-518e037fcedb.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>DeserializeInboundActivity</strong> - This server action converts the received text payload into a BasicActivity structure, which includes some parts of the full activity payload.</p>
</li>
<li><p><strong>Activity_Create</strong> - This server action creates a record in the Activity entity with details of the inbound activity and the full payload received as binary data. (Note the conversion from text to binary data)</p>
</li>
<li><p><strong>OnBotActivity</strong> - This event trigger uses property values from the deserialized activity payload as the event payload. The purpose of the event parameters is to provide a handler with enough information about an activity to determine if it should be handled.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You might wonder why we aren't doing any authorization checks, and you're absolutely right. In a production environment, we need to verify if a request truly comes from one of our configured channels through the Azure Bot resource. I will discuss this in a later part of the series. For now, just remember that your messaging endpoint is public.</div>
</div>

<h1 id="heading-handle-event">Handle Event</h1>
<p>Select the <strong>OnBotActivity</strong> event in <strong>Events - Events</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737872609862/4db87385-54fa-4c54-a5ea-c9e94ca9175d.png" alt class="image--center mx-auto" /></p>
<p>The demo application defines an in-app event handler called <strong>BotHandlerSimpleTextResponse</strong> (<strong>Logic - Bots</strong>). Using an event instead of handling everything directly within the Messaging endpoint allows you to build multiple bots within ODC. These bots can subscribe to this event and handle it based on conditional checks of the event parameters.</p>
<h1 id="heading-bot-handler">Bot Handler</h1>
<p>Open the server action <strong>BotHandlerSimpleTextResponse</strong> in <strong>Logic - Bots</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737873259005/759d0a47-25a2-460c-9fe3-bb0410b0f99a.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>IsMessageType</strong> - This condition checks if the activity type is a message. If it isn't, the action flow ends because we only want to handle messages. At this point, you can add more checks, like verifying the BotId.</p>
</li>
<li><p><strong>Activity_Get</strong> - This action retrieves the full payload as text from the Activity entity.</p>
</li>
<li><p>DeserializePayload - This action deserializes parts of the full payload. Here, we only deserialize the parts relevant for this bot handler to respond to the activity.</p>
</li>
<li><p><strong>OutboundMessagePayload</strong> - Here, we create a new activity of type message with text. Note the mandatory "From" assignment where we assign the Recipient object, which is our Bot.</p>
</li>
<li><p><strong>GetConnectorAPIAcccesToken</strong> - Before sending the activity to the Connector API, we need to acquire an access token, which this server action returns.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note that this action uses an external logic function from the OAuthTokenExchange Forge component, a component I built for easier retrieval of access tokens using client credential or authorization code OAuth 2.0 flows.</div>
</div>

<ul>
<li><strong>Connector_SendToConversation</strong> - This uses an action from the Bot Framework Service API Forge component to send the response activity to the conversation.</li>
</ul>
<h1 id="heading-bot-framework-service-api">Bot Framework Service API</h1>
<p>Calls to the <strong>Connector API</strong> are made to a dynamic <strong>serviceUrl</strong> that depends on the channel that sent the activity. For example, the "Test in Web Chat" feature in the Azure Portal uses a <strong>serviceUrl</strong> of webchat.botframework.com, while activities from a Teams channel have a <strong>serviceUrl</strong> starting with smba.trafficmanager.net. The exact <strong>serviceUrl</strong> depends on the channel and sometimes the region of your bot, which is why the <strong>serviceUrl</strong> must always be deserialized from the inbound activity.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">As you may already know, ODC does not support dynamic base URLs for REST APIs by default. Check the <strong>OnBeforeRequestHandler</strong> in <strong>Logic - Integrations - REST - BotConnectorAPI</strong> for a pattern on how to use a dynamic base URL.</div>
</div>

<h1 id="heading-activity-schema">Activity Schema</h1>
<p>The demo application includes the fixed elements of the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference?view=azure-bot-service-4.0#schema">Bot Activity Schema</a> (<strong>Data - Structures - Schema</strong>) that you can use to build your own payloads.</p>
<h1 id="heading-try-it">Try it</h1>
<p>Switch to the Azure Portal and select your Azure Bot resource. Go to the <strong>Settings - Test in Web Chat</strong> menu. Type anything in the chat window, and you should see the message:</p>
<p><code>Congratulations! You have successfully set up your ODC bot. 🎉</code></p>
<p>I recommend starting the demo application in debugging mode and setting some breakpoints to observe how the application processes the incoming message activity.</p>
<h1 id="heading-summary">Summary</h1>
<p>In part 3 of the ODC with Bots for Teams article series, we explored the steps needed to handle an inbound message activity using the Test in Web Chat feature of the Azure portal. We learned how to deserialize the inbound activity and manage it asynchronously with an in-app event handler.</p>
<p>In the next part of the series, we will activate the Microsoft Teams channel for our Bot resource and create a configuration package to add the bot to the Microsoft Teams client application.</p>
]]></content:encoded></item><item><title><![CDATA[Basic Setup and Receiving Messages]]></title><description><![CDATA[This is the second part of the ODC with Bots for Teams series. In this tutorial, we will create a messaging endpoint in an OutSystems Developer Cloud application and set up a basic Azure Bot resource in your Azure tenant, along with a Microsoft Entra...]]></description><link>https://without.systems/odc-with-bots-for-teams-receiving-messages</link><guid isPermaLink="true">https://without.systems/odc-with-bots-for-teams-receiving-messages</guid><category><![CDATA[outsystems]]></category><category><![CDATA[Bot Framework]]></category><category><![CDATA[azure bot service]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Mon, 20 Jan 2025 11:47:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737346806119/b31aad35-5456-456d-9d3a-1131b3400f4b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the second part of the <a target="_blank" href="https://without.systems/series/odc-msteams-bots">ODC with Bots for Teams</a> series. In this tutorial, we will create a messaging endpoint in an OutSystems Developer Cloud application and set up a basic Azure Bot resource in your Azure tenant, along with a Microsoft Entra application registration.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">By the end of this article, you will be able to<strong> receive messages only</strong>. Later articles cover how to respond to messages, add authentication and more.</div>
</div>

<p>Make sure you have read the <a target="_blank" href="https://without.systems/odc-with-bots-for-teams-introduction">Introduction</a> to understand the different parts of this setup.</p>
<h1 id="heading-demo-application">Demo Application</h1>
<p>This article series includes a demo application called "<strong>ODC with Bots for Teams Demo</strong>," available on <strong>ODC Forge</strong>. Be sure to download the version of the application that matches each article in this series.</p>
<p>For this article, you need to install Version 0.1 from ODC Forge.</p>
<ul>
<li><p>In the ODC Portal, go to <strong>Forge - All Assets</strong>.</p>
</li>
<li><p>Search for "<strong>ODC with Bots for Teams Demo</strong>".</p>
</li>
<li><p>Click on the Asset <strong>(Do not click on Install on the tile!)</strong>.</p>
</li>
<li><p>Switch to the <strong>Version</strong> tab and click on Install next to <strong>Version 0.1</strong>.</p>
</li>
</ul>
<h1 id="heading-messaging-endpoint-application">Messaging Endpoint Application</h1>
<p>A messaging endpoint receives various types of messages from communication channels, including user messages. In an OutSystems Developer Cloud, this is an exposed REST API endpoint that accepts POST requests and is later configured in an Azure Bot resource.</p>
<ul>
<li><p>Open the <strong>ODC with Bots for Teams Demo</strong> application and switch to the <strong>Logic</strong> tab.</p>
</li>
<li><p>In Integrations - REST, select <strong>MessagingEndpoint</strong>.</p>
</li>
</ul>
<p>The exposed REST API is configured with authentication set to None, and documentation is turned off.</p>
<ul>
<li>Click on the <strong>Messages</strong> endpoint</li>
</ul>
<p>The Message endpoint is set up to accept <strong>POST</strong> requests and has one input parameter, <strong>Request</strong>, which is received in the <strong>Body</strong> as type <strong>Text</strong>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Channels send various types of messages, as explained in the <a target="_self" href="https://without.systems/odc-with-bots-for-teams-introduction#heading-bot-messages">Introduction</a> article. The schema for each message can vary and be quite dynamic. That's why we accept the request as plain text and handle deserialization into structures later.</div>
</div>

<ul>
<li>Copy the <strong>URL</strong> of the <strong>Messages</strong> endpoint. If you have not changed the demo app name it should be <code>/ODCwithBotsforTeamsDemo/rest/MessagingEndpoint/Messages</code>. We need that URL path later on, when we configure the Azure Bot resource.</li>
</ul>
<p>For now, we will leave the Demo app and set up the rest in the Azure Portal.</p>
<h1 id="heading-register-entra-application">Register Entra Application</h1>
<p>A bot resource requires an Entra application registration, which we later use to get an access token to communicate with the Bot Connector API and respond to messages.</p>
<p>In <a target="_blank" href="https://portal.azure.com">Azure Portal</a> go to <strong>App Registrations</strong> and click on <strong>New registration</strong>.</p>
<ul>
<li><p><strong>Name</strong> - Microsoft Teams Conversational Bot with ODC</p>
</li>
<li><p><strong>Supported account types</strong> - Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)</p>
</li>
<li><p>Click <strong>Register</strong></p>
</li>
</ul>
<p>From the <strong>Overview</strong> page, copy the values of:</p>
<ul>
<li><strong>Application (client) ID</strong></li>
</ul>
<p>We will need these values later.</p>
<h2 id="heading-add-a-client-secret">Add a Client Secret</h2>
<p>Under the <strong>Manage</strong> menu select <strong>Certificates &amp; secrets</strong></p>
<p>Select the <strong>Client secrets</strong> tab and click on <strong>New client secret</strong></p>
<ul>
<li><p><strong>Description</strong> - ODC Conversational Bot sample</p>
</li>
<li><p><strong>Expires</strong> - Recommended: 180 days (6 months)</p>
</li>
<li><p>Click <strong>Add</strong></p>
</li>
</ul>
<p>Immediately after adding the new client secret, copy the <strong>Value</strong> and save it for later, as it will only be displayed once. We won't use the Client Secret in this article, but it will be needed in future articles.</p>
<h2 id="heading-add-a-redirect-uri">Add a Redirect URI</h2>
<p>The final configuration step is to add a Redirect URI to the application registration.</p>
<p>Under the <strong>Manage</strong> menu select <strong>Authentication</strong>.</p>
<p>In the Platform configurations section click Add a platform</p>
<ul>
<li><p>Select <strong>Web</strong></p>
</li>
<li><p><strong>Redirect URIs</strong> - https://token.botframework.com/.auth/web/redirect</p>
</li>
<li><p>Click <strong>Configure</strong></p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The redirect URI above is the default for bots without data residency requirements. Microsoft provides additional redirect URIs for different regions. For more details, see <a target="_self" href="https://learn.microsoft.com/en-us/azure/bot-service/ref-oauth-redirect-urls?view=azure-bot-service-4.0">Supported OAuth URLs - Bot Service | Microsoft Learn</a>.</div>
</div>

<h1 id="heading-create-an-azure-bot">Create an Azure Bot</h1>
<p>With our basic application registration complete, we can now create an Azure Bot resource that will connect one or more channels (like Microsoft Teams) with our ODC messaging endpoint.</p>
<p>In Azure <strong>Marketplace</strong> search for <strong>Azure Bot</strong> and click <strong>Create - Azure Bot.</strong></p>
<ul>
<li><p><strong>Bot handle</strong> - BotId-&lt;Application ID from Entra App Registration&gt;</p>
</li>
<li><p><strong>Subscription</strong> - Select your subscription model</p>
</li>
<li><p><strong>Resource group</strong> - Choose a resource group where you want to place the bot resource</p>
</li>
<li><p><strong>Data residency</strong> - Global</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">If you choose a specific data residency, ensure it matches the Redirect URI you configured earlier.</div>
</div>

<ul>
<li><p><strong>Pricing tier</strong> - Change plan to Free</p>
</li>
<li><p><strong>Type of App</strong> - Multi Tenant (corresponds to our Entra App Registration)</p>
</li>
<li><p><strong>Creation type</strong> - Use existing app registration</p>
</li>
<li><p><strong>App ID</strong> - Paste the <strong>Application ID</strong> from the Entra App Registration Overview page</p>
</li>
<li><p><strong>App tenant ID</strong> - Paste the <strong>Tenant ID</strong> from the Entra App Registration Overview page</p>
</li>
<li><p>Click <strong>Review + Create</strong>, review the information the click <strong>Create</strong></p>
</li>
</ul>
<p>Wait until the deployment is complete, then click on <strong>Go to resource</strong> to continue.</p>
<h2 id="heading-set-bot-profile">Set Bot Profile</h2>
<p>In the deployed Azure Bot resource, first go to the <strong>Settings - Bot profile</strong> menu and give your bot a meaningful display name and description. You can also upload a custom icon here.</p>
<h2 id="heading-configure-messaging-endpoint">Configure Messaging Endpoint</h2>
<p>Next switch to the <strong>Settings - Configuration</strong> menu. The only settings we have to configure here is to provide the <strong>FQDN</strong> to our Messaging endpoint.</p>
<p>The messaging endpoint FQDN is the combination of your ODC stage base URL and the path you copied earlier from the Messaged endpoint of the demo application.</p>
<ul>
<li><p><strong>Messaging endpoint</strong> - <code>https://&lt;ODC stage&gt;.outsystems.app/ODCwithBotsforTeamsDemo/rest/MessagingEndpoint/Messages</code></p>
</li>
<li><p>Click <strong>Apply</strong> to save the changes.</p>
</li>
</ul>
<h1 id="heading-receive-a-message-from-web-chat">Receive a Message from Web Chat</h1>
<p>Switch to <strong>ODC Studio</strong> and double-click the <strong>Messages</strong> endpoint in <strong>Logic - Integrations - REST - MessagingEndpoint.</strong></p>
<p><strong>Right-click</strong> on the <strong>Start</strong> node and select <strong>Add Breakpoint</strong> from the menu to add a breakpoint.</p>
<p>In the <strong>Debugger</strong> tab, click <strong>Start debugging</strong>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The demo application has no screens, but ODC will still open a Microsoft Edge or Google Chrome browser. Ignore the browser window and any error message you see.</div>
</div>

<p>Leave the debugger and go back to your Azure Bot resource. Select the <strong>Settings - Test in Web Chat</strong> menu. You should immediately see ODC Studio flashing.</p>
<p>Check the <strong>Request</strong> input parameter, which contains a JSON document that should look like this:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"conversationUpdate"</span>,
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"KJvkAp8O6y1"</span>,
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2025-01-20T05:49:46.1194744Z"</span>,
  <span class="hljs-attr">"serviceUrl"</span>: <span class="hljs-string">"https://webchat.botframework.com/"</span>,
  <span class="hljs-attr">"channelId"</span>: <span class="hljs-string">"webchat"</span>,
  <span class="hljs-attr">"from"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"</span>
  },
  <span class="hljs-attr">"conversation"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"xxxxxxxxxxxxxxx-eu"</span>
  },
  <span class="hljs-attr">"recipient"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"BotId-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"My Demo OutSystems Bot"</span>
  },
  <span class="hljs-attr">"membersAdded"</span>: [
    {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"BotId-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"My Demo OutSystems Bot"</span>
    },
    {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"</span>
    }
  ]
}
</code></pre>
<p>This first message indicates that both a bot and a user joined a conversation.</p>
<p><strong>Resume the debugger</strong> to continue.</p>
<p>Next type a message in the Web Chat console and inspect the message again in the debugger.</p>
<p>You should receive a message like this</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"message"</span>,
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"175nbLQPt2A7LhdRnSZeYM-eu|0000000"</span>,
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2025-01-20T05:55:16.1409929Z"</span>,
  <span class="hljs-attr">"localTimestamp"</span>: <span class="hljs-string">"2025-01-20T06:55:18.613+01:00"</span>,
  <span class="hljs-attr">"localTimezone"</span>: <span class="hljs-string">"Europe/Berlin"</span>,
  <span class="hljs-attr">"serviceUrl"</span>: <span class="hljs-string">"https://webchat.botframework.com/"</span>,
  <span class="hljs-attr">"channelId"</span>: <span class="hljs-string">"webchat"</span>,
  <span class="hljs-attr">"from"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">""</span>
  },
  <span class="hljs-attr">"conversation"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"xxxxxxxxxxxxxxx-eu"</span>
  },
  <span class="hljs-attr">"recipient"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"BotId-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"My Demo OutSystems Bot"</span>
  },
  <span class="hljs-attr">"textFormat"</span>: <span class="hljs-string">"plain"</span>,
  <span class="hljs-attr">"locale"</span>: <span class="hljs-string">"de"</span>,
  <span class="hljs-attr">"text"</span>: <span class="hljs-string">"Hello"</span>,
  <span class="hljs-attr">"attachments"</span>: [],
  <span class="hljs-attr">"entities"</span>: [
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"ClientCapabilities"</span>,
      <span class="hljs-attr">"requiresBotState"</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">"supportsListening"</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">"supportsTts"</span>: <span class="hljs-literal">true</span>
    }
  ],
  <span class="hljs-attr">"channelData"</span>: {
    <span class="hljs-attr">"clientActivityID"</span>: <span class="hljs-string">"xxxxxxxxxxxx"</span>
  }
}
</code></pre>
<p>In this case I, identified by the <strong>from</strong> object, sent a text “<strong>Hello</strong>” to the bot identified by the <strong>recipient</strong> object.</p>
<p>The <strong>Test in Web Chat</strong> feature uses the <strong>Web Chat</strong> channel, which is activated by default. Later, you can disable these pre-activated channels once you are satisfied with your bot. However, for development purposes, you should keep it activated as it offers an easy way to test your implementation.</p>
<h1 id="heading-summary">Summary</h1>
<p>Congratulations! You have just completed the initial setup to receive messages from an Azure Bot resource. In this tutorial, we set up a new Azure Bot resource in your Azure Active Directory tenant. This resource sends messages received by a channel to a REST API endpoint in an OutSystems Developer Cloud application. In the next article, we will cover how to respond to incoming messages using the Bot Connector API.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item><item><title><![CDATA[Conversational Teams Bots Introduction]]></title><description><![CDATA[This is the start of a new series of articles about developing bots for Microsoft Teams using OutSystems Developer Cloud.
Bots for Microsoft Teams offer many benefits. They can answer user questions and boost productivity by automating tasks directly...]]></description><link>https://without.systems/odc-with-bots-for-teams-introduction</link><guid isPermaLink="true">https://without.systems/odc-with-bots-for-teams-introduction</guid><category><![CDATA[outsystems]]></category><category><![CDATA[Bot Framework]]></category><category><![CDATA[azure bot service]]></category><dc:creator><![CDATA[Stefan Weber]]></dc:creator><pubDate>Sun, 12 Jan 2025 09:22:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736663862970/c87714b2-b99c-43ce-9dfa-33655058b541.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the start of a new series of articles about developing bots for Microsoft Teams using OutSystems Developer Cloud.</p>
<p>Bots for Microsoft Teams offer many benefits. They can answer user questions and boost productivity by automating tasks directly within a conversation, among other things.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://creators.spotify.com/pod/show/withoutsystems/episodes/OutSystems-Developer-Cloud-with-Azure-Bots-for-Teams-e2tcoue">https://creators.spotify.com/pod/show/withoutsystems/episodes/OutSystems-Developer-Cloud-with-Azure-Bots-for-Teams-e2tcoue</a></div>
<p> </p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Please note that the podcast episode above is automatically generated from this article and may not include a proper level of detail.</div>
</div>

<p>The Microsoft Bot for Teams ecosystem is extensive, and Microsoft provides several tools for developing bot solutions: the Bot Framework SDKs, the Teams Toolkit, Bot Composer, and the newest addition, Copilot Studio. Unfortunately, the documentation mainly assumes you are using one of the provided toolkits, and it takes some time to figure out how to create a bot—or multiple bots—with an alternative technology stack like ODC.</p>
<p>This is what I aim to cover in this article series because, in the end, all the above toolkits just use configurations and APIs you can leverage in ODC directly without needing to create custom code or adapt the Microsoft Bot Framework SDK for .NET.</p>
<p>So I hope you enjoy this journey on how to build Bots for Microsoft Teams with OutSystems Developer Cloud.</p>
<h1 id="heading-why-you-should-care">Why you should care</h1>
<p>Nearly every organization is now working or at least experimenting with GenAI chatbots and AI agents. OutSystems has introduced its own <a target="_blank" href="https://success.outsystems.com/documentation/outsystems_developer_cloud/building_apps/about_ai_agent_builder/">AI Agent Builder</a>, a user-friendly Forge component for creating conversational GenAI solutions. It also includes a UI widget library to design your own chatbot interface. This is ideal if you want to add an AI conversation feature to another ODC application, like a CRM you built. However, if you want to build a conversational AI experience that isn't directly linked to another application in ODC, it makes sense to integrate with an application users are familiar with. Since Microsoft Teams is the most popular messaging platform, it's a great choice.</p>
<p>Microsoft Teams is a great choice for providing users with a single point of contact to access multiple conversational AI solutions, like Bots. This applies not only to the AI Agent Builder but also in general.</p>
<h1 id="heading-components-of-a-custom-bot-environment">Components of a custom Bot Environment</h1>
<p>Although the documentation might seem overwhelming at first, setting up a bot environment for ODC is quite simple. Here are the core components:</p>
<ul>
<li><p>A configured <strong>Azure Bot resource</strong> for each bot you want to build, with an attached <strong>Microsoft Entra application registration</strong>.</p>
</li>
<li><p>A <strong>single REST endpoint in ODC</strong> to receive conversation messages from channels like Microsoft Teams.</p>
</li>
<li><p><strong>Action flows in ODC to handle incoming messages and respond</strong> (with many structures for deserializing and serializing messages 😒).</p>
</li>
<li><p>A <strong>Teams app publishing profile</strong> to add a Bot to a user's Teams client.</p>
</li>
</ul>
<p>Without further ado, let's take a brief look at all of these components.</p>
<h1 id="heading-azure-bot">Azure Bot</h1>
<p>Azure Bot is a resource you can set up from the <a target="_blank" href="https://azuremarketplace.microsoft.com/en/marketplace/apps/Microsoft.AzureBot?tab=Overview">Azure Marketplace</a> in your tenant. Once configured, the Azure Bot resource acts as a gateway or router between one or more channels and a messaging endpoint.</p>
<h2 id="heading-channels">Channels</h2>
<p>Channels are integrations that allow your bot to interact with users on various communication platforms, such as Microsoft Teams, Facebook Messenger, Slack, Telegram, and many others.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In this article series, we are focusing only on Microsoft Teams.</div>
</div>

<h2 id="heading-messaging-endpoint">Messaging Endpoint</h2>
<p>The Messaging Endpoint is a specific URL that handles incoming HTTP requests from messaging channels. This is where the bot receives messages from users and processes them to create appropriate responses. For ODC, this is a single <strong>REST POST endpoint</strong>. A single messaging endpoint doesn't necessarily represent just one bot; it can be used for multiple different Azure Bot resources.</p>
<h1 id="heading-bot-framework-rest-apis">Bot Framework REST APIs</h1>
<p>The Microsoft documentation for developing bots mainly focuses on using one of the official Bot Framework SDKs, such as the .NET Framework. These SDKs work with the Bot Framework REST APIs, which are Microsoft-managed APIs for sending messages (<strong>Connector API</strong>), receiving messages (<strong>Direct Line API</strong>), and managing user authentication (<strong>Token API</strong>) to and from a channel.</p>
<p>In OutSystems Developer Cloud applications, we need to use the Bot Framework REST APIs directly instead of the SDK. The Bot Framework offers the following REST APIs.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">To use the Bot Framework REST APIs, your bot first needs to <a target="_self" href="https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication?view=azure-bot-service-4.0&amp;tabs=singletenant#step-1-request-an-access-token-from-the-microsoft-entra-id-account-login-service">acquire an access token</a>. Then, use this access token in the <strong>Authorization </strong>header of a request.</div>
</div>

<h2 id="heading-connector-api">Connector API</h2>
<p>Communication from a channel to a messaging endpoint is <strong>one-way</strong>. The <strong>messaging endpoint only receives requests</strong> and cannot directly reply to the channel. Instead, for a bot to send a message to a channel, it must use the Bot Framework Service Connector API. This API converts messages from the general Azure Bot message format to the specific format needed by each channel, ensuring messages are delivered correctly across different channels. For more details, see the <a target="_blank" href="https://github.com/microsoft/botbuilder-dotnet/blob/main/libraries/Swagger/ConnectorAPI.json">Connector API OpenAPI specification</a>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It's important to note that the Connector API does not have a standard base URI. Instead, when the <strong>messaging endpoint</strong> receives a request, the incoming request includes a <strong>serviceUrl</strong> property that specifies the endpoint where your bot should send its response. To access the Bot Framework Connector service, you must use the <strong>serviceUrl</strong> value as the base URI for API requests. You can find more details in the <a target="_self" href="https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference?view=azure-bot-service-4.0#base-uri">Connector API documentation</a>.</div>
</div>

<h2 id="heading-token-api">Token API</h2>
<p>In addition to the Connector API, the Bot Framework Service offers the Token API, which issues and manages access tokens to ensure secure and authenticated communication between a bot and a channel. A key feature of the Token API is that it simplifies the authentication process and token retrieval if your bot needs a user to sign in directly within a conversation. See the <a target="_blank" href="https://github.com/microsoft/botbuilder-dotnet/blob/main/libraries/Swagger/TokenAPI.json">Token API OpenAPI specification</a> for details.</p>
<h2 id="heading-direct-line-api">Direct Line API</h2>
<p>The Direct Line API is another API provided by the Bot Framework Service. It allows you to build your own client application to interact with a bot if none of the supported channels meet your needs. See the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-direct-line-3-0-api-reference?view=azure-bot-service-4.0">Direct Line API reference</a> and the <a target="_blank" href="https://github.com/microsoft/botframework-sdk/blob/main/specs/botframework-protocol/directline-3.0.json">OpenAPI specification</a> for details.</p>
<h1 id="heading-bot-messages">Bot Messages</h1>
<p>Channels send requests to the Messaging Endpoint, and bots send requests to a channel using the <strong>Connector API</strong>. Request payloads are defined in the Azure AI Bot Activity Schema, which is a standardized JSON format used across all supported channels. Each request, called an <strong>Activity</strong>, is identified by a <strong>type</strong> attribute in the payload. Below is a list of all possible activities, along with simplified payload examples.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Keep in mind that the actual payload for some activity types is much larger. See full <a target="_self" href="https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference?view=azure-bot-service-4.0#schema">Activity Schema</a>.</div>
</div>

<ul>
<li><strong>message</strong> - Standard communication between bot and user. Includes the main content the bot wants to convey, such as text, attachments, or interactive cards.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"message"</span>,
  <span class="hljs-attr">"text"</span>: <span class="hljs-string">"Hello! How can I assist you today?"</span>
}
</code></pre>
<ul>
<li><strong>conversationUpdate</strong> - Sent when a conversation's state changes. For example, when a user joins or leaves a chat, or when the conversation is created or deleted.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"conversationUpdate"</span>,
  <span class="hljs-attr">"membersAdded"</span>: [
    {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"user1"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"User One"</span>
    }
  ],
  <span class="hljs-attr">"membersRemoved"</span>: [],
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2025-01-10T07:10:00Z"</span>
}
</code></pre>
<ul>
<li><strong>contactRelationUpdate</strong> - Triggers when the bot's contact list changes. Includes adding or removing a user as a contact.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"contactRelationUpdate"</span>,
  <span class="hljs-attr">"action"</span>: <span class="hljs-string">"add"</span>,
  <span class="hljs-attr">"user"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"user1"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"User One"</span>
  }
}
</code></pre>
<ul>
<li><strong>typing</strong> - Indicates that the bot or user is currently typing a response. Helps manage user expectations and improves interaction flow.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"typing"</span>
}
</code></pre>
<ul>
<li><strong>ping</strong> - A lightweight activity used to check connectivity between bot and endpoint. Ensures the bot is active and responsive.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"ping"</span>
}
</code></pre>
<ul>
<li><strong>event</strong> - Represents a named event sent from the bot or channel. Often used for channel-specific events or to trigger specific bot actions.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"event"</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"customEventName"</span>,
  <span class="hljs-attr">"value"</span>: {
    <span class="hljs-attr">"key"</span>: <span class="hljs-string">"value"</span>
  }
}
</code></pre>
<ul>
<li><strong>endOfConversation</strong> - Indicates that the conversation has ended. Useful for closing sessions or indicating that no further interaction is expected.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"endOfConversation"</span>,
  <span class="hljs-attr">"code"</span>: <span class="hljs-string">"completedSuccessfully"</span>,
  <span class="hljs-attr">"text"</span>: <span class="hljs-string">"Thank you for chatting! Goodbye."</span>
}
</code></pre>
<ul>
<li><strong>handoff</strong> - Used to transfer the conversation to a human agent or another bot. It can include context and state information to facilitate the handover.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"handoff"</span>,
  <span class="hljs-attr">"handoffMetadata"</span>: {
    <span class="hljs-attr">"conversationId"</span>: <span class="hljs-string">"12345"</span>,
    <span class="hljs-attr">"context"</span>: <span class="hljs-string">"Customer needs support from a human agent."</span>
  }
}
</code></pre>
<ul>
<li><strong>invoke</strong> - Represents a request to perform a specific operation. Commonly used for actions like triggering a dialog or processing specific input.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"invoke"</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"triggerDialog"</span>,
  <span class="hljs-attr">"value"</span>: {
    <span class="hljs-attr">"dialogId"</span>: <span class="hljs-string">"greetingDialog"</span>
  }
}
</code></pre>
<ul>
<li><strong>installationUpdate</strong> - Sent when the bot is added or removed from a channel or a group. Helps the bot manage its presence and react to changes.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"installationUpdate"</span>,
  <span class="hljs-attr">"action"</span>: <span class="hljs-string">"add"</span>,
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2025-01-10T07:10:00Z"</span>
}
</code></pre>
<h1 id="heading-microsoft-teams-channel-integration">Microsoft Teams Channel Integration</h1>
<p>A channel is set up in the Azure Bot resource, but this alone is not enough to make a bot available in Microsoft Teams. To let users chat with a bot, it must be added as an app to Microsoft Teams.</p>
<p>An app for Microsoft Teams is a JSON document that follows the <a target="_blank" href="https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema">Microsoft App Manifest schema</a> and includes configuration details about your bot. The manifest, along with the referenced app icon resources, is then compressed into a ZIP archive and published either as a custom app to a single Teams client (suitable for testing), to the organization's app catalog, or to the Teams Store.</p>
<p>If you aim to make your bot available to all Teams users by publishing it to the Teams Store, you must submit it to the Teams Store and ensure it meets all the <a target="_blank" href="https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/appsource/prepare/teams-store-validation-guidelines">necessary compliance and security requirements</a>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You need to configure your tenant to <a target="_self" href="https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/prepare-your-o365-tenant#enable-custom-teams-apps-and-turn-on-custom-app-uploading">allow custom apps and custom app uploads</a>.</div>
</div>

<h1 id="heading-other-channel-integrations">Other Channel Integrations</h1>
<p>In this article series, we will focus only on Microsoft Teams. For information on how to integrate a bot with other supported channels, like Slack, please see <a target="_blank" href="https://learn.microsoft.com/en-us/azure/bot-service/bot-service-manage-channels?view=azure-bot-service-4.0">Configure an Azure AI Bot Service bot to run on one or more channels - Bot Service | Microsoft Learn</a>.</p>
<h1 id="heading-conversation-scenario">Conversation Scenario</h1>
<p>Lets look at a simple conversation example scenario.</p>
<p>Mike has already a custom bot app OutSystems Bot published to his Teams client. In the Teams client he selects the OutSystems Bot App and Microsoft Teams display a conversation window with the Bot.</p>
<p>Mike enters “Hello” into the chat field and sends the message.</p>
<h2 id="heading-message-receive">Message Receive</h2>
<p>Our messaging endpoint will now receive two requests with two different type values</p>
<ul>
<li><strong>conversationUpdate</strong> - This request indicates that Mike joined a conversation with the bot. The payload for this request looks like this.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"conversationUpdate"</span>,
  <span class="hljs-attr">"membersAdded"</span>: [
    {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"29:xxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxx_xxxxxxxxxxxxxxxx"</span>,
      <span class="hljs-attr">"aadObjectId"</span>: <span class="hljs-string">"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"</span>
    },
    {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"28:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"</span>
    }
  ],
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"f:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"</span>,
  <span class="hljs-attr">"serviceUrl"</span>: <span class="hljs-string">"https://smba.trafficmanager.net/emea/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/"</span>,
  <span class="hljs-attr">"from"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"29:xxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxx_xxxxxxxxxxxxxxxx"</span>,
    <span class="hljs-attr">"aadObjectId"</span>: <span class="hljs-string">"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"</span>
  }
  <span class="hljs-string">"recipient"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"28:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"OutSystems Bot"</span>
  },
  .... Additional payload data
}
</code></pre>
<p>Note the <strong>membersAdded</strong> array. The first item is the Id of the user Mike, the second one is the Id of the bot. Both have been added to the conversation.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>conversationUpdate </strong>is only sent once for a user joining a conversation.</div>
</div>

<ul>
<li><strong>message</strong> - The second request of type message is the chat message Mike entered in the chat window. The payload looks like this.</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"text"</span>: <span class="hljs-string">"Hello"</span>,
  <span class="hljs-attr">"textFormat"</span>: <span class="hljs-string">"plain"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"message"</span>,
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"xxxxxxxxxxx"</span>,
  <span class="hljs-attr">"serviceUrl"</span>: <span class="hljs-string">"https://smba.trafficmanager.net/emea/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/"</span>,
  <span class="hljs-attr">"from"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"29:xxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxx_xxxxxxxxxxxxxxxx"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Mike"</span>,
    <span class="hljs-attr">"aadObjectId"</span>: <span class="hljs-string">"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"</span>
  },
  <span class="hljs-attr">"conversation"</span>: {
    <span class="hljs-attr">"conversationType"</span>: <span class="hljs-string">"personal"</span>,
    <span class="hljs-attr">"tenantId"</span>: <span class="hljs-string">"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"</span>,
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"a:xxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxx_xxxxxxxxxxxxxxxx"</span>
  },
  <span class="hljs-attr">"recipient"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"28:xxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxx_xxxxxxxxxxxxxxxx"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"OutSystems Bot"</span>
  },
  ... Additional payload data
}
</code></pre>
<h2 id="heading-message-respond">Message Respond</h2>
<p>Responding to an incoming message is straightforward. We can either send a response to the conversation itself or reply to an incoming activity (represented by the <strong>id</strong> attribute of the incoming message payload).</p>
<p>We choose to send a message directly to the conversation.</p>
<ul>
<li><p>First, we acquire an access token for interacting with the <strong>Connector API</strong>.</p>
</li>
<li><p>We extract the <strong>serviceUrl</strong>, <strong>recipient.id</strong> (the Bot Id), <strong>from.id</strong> (Mike's Id), and <strong>conversation.id</strong> values from the "Hello" message sent by Mike.</p>
</li>
<li><p>We make a request to</p>
</li>
</ul>
<pre><code class="lang-plaintext">URL: https://&lt;serviceUrl&gt;/v3/conversations/&lt;conversation.id&gt;/activities
METHOD: POST
AUTHORIZATION: BEARER &lt;Akquired Access Token&gt;
JSON PAYLOAD:
{
  "type": "message",
  "from": {
    "id": "&lt;recipient.id&gt;" // Bot User Identifier
  },
  "recipient": {
    "id": "&lt;from.id&gt;" // Mikes user identifier
  },
  "text": "Welcome Mike, Iam happy to chat with you"
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">We will cover the details of each step throughout the article series, including user authentication.</div>
</div>

<h1 id="heading-summary">Summary</h1>
<p>This concludes the introduction to Bots for Microsoft Teams with OutSystems Developer Cloud. Please take a moment to review the provided links, as they contain important additional information.</p>
<p>Setting up a basic environment for Microsoft Teams Bots with ODC is straightforward. It involves creating an Entra application registration and an Azure Bot resource in your Azure tenant, configuring the Bot resource to send messages to your exposed REST API endpoint, setting up one or more channels, and finally creating a publishing profile for a Teams app. We will cover how to perform these steps in the next part of the series.</p>
<div class="hn-embed-widget" id="follow-linkedin-button"></div>]]></content:encoded></item></channel></rss>